Spring依赖注入——DI

Spring 依赖注入(DI Dependency Injection),可以自动帮我们解决类与类之间的各种依赖问题。通常公司中的项目都包含很多的类,而这些类与类之间都有各种依赖关系,比如:组合、聚合、依赖等。通过Spring的DI功能,结合IOC容器, 我们可以把这些复杂的关系交由Spring来管理, 我们在使用时,Spring会自动帮我们把依赖的对象注入进来,大大降低开发难度。

Spring中的依赖注入主要包含两种:通过构造方法中的注入和通过Setter方法注入

Setter方法注入:

1.注册匿名内部bean

匿名内部bean不需要写id,无法让外部访问

示例:

<bean id="aa" class="com.lanou3g.spring.bean.Student" >
    <property name="fruit">
        <bean class="com.lanou3g.spring.simple.Apple" />
    </property>
</bean>

使用入口:

public static void main(String[] args) {
        //初始化容器
        ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        ctx.registerShutdownHook();
        Student s=ctx.getBean("aa",Student.class);
        s.getFruit().eatFruit();
}

2.注入集合类型属性

如果你想传递多个值,spring为我们提供了四中类型的集合,List、Set、Map 和 Properties,这四种 几个对应 和 标签来方便我们注入集合的属性

示例:

@Setter
@Getter
public class Java {
    private List<Object> list;
    private Set<Object> set;
    private Map<String,Object> map;
    private Properties properties;
}

XML配置代码

<bean id="aaa" class="com.lanou3g.spring.bean.Java">
        <property name="list">
            <list>
                <value>l章三</value>
                <value>l李四</value>
                <value>l王五</value>
            </list>
        </property>
        <property name="set">
            <set>
                <value>s章三</value>
                <value>s李四</value>
                <value>s王五</value>
            </set>
        </property>
        <property name="map">
            <map>
                <entry key="章三" value="三"></entry>
                <entry key="李四" value="四"></entry>
            </map>
        </property>
        <property name="properties">
            <props>
                <prop key="one">one</prop>
                <prop key="two">two</prop>
            </props>
        </property>
    </bean>

示例入口:

public static void main(String[] args) {
        ApplicationContext  ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Java j=ctx.getBean("aaa",Java.class);
        System.out.println(j.getList());
        System.out.println(j.getSet());
        System.out.println(j.getMap());
        System.out.println(j.getProperties());
}

3.注入null和空字符串属性值

示例:

@Getter
@Setter
public class Null {
    private String name;
    private String nnn;
}

xml配置代码

<bean id="ss" class="com.lanou3g.spring.bean.Null">
        <property name="name" value="">
        </property>
        <property name="nnn">
            <null></null>
        </property>
    </bean>

使用入口:

public static void main(String[] args) {
        ApplicationContext  ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Null n=ctx.getBean("ss",Null.class);
        System.out.println("空字符="+n.getName());
        System.out.println("空字符="+n.getNnn());
}

4.注入复合属性值

例如有一个Java类中有一个Null类型的属性,Null类型中又包含了一个name属性,name属性的值需要设置为’章撒’,我们可以通过下面的方式配置

@Setter
@Getter
public class Java {
    private List<Object> list;
    private Set<Object> set;
    private Map<String,Object> map;
    private Properties properties;
    private Null anull;
}

@Getter
@Setter
public class Null {
    private String name;
}
<bean id="ss" class="com.lanou3g.spring.bean.Java">
        <property name="anull">
            <bean class="com.lanou3g.spring.bean.Null"/>
        </property>
        <property name="anull.name" value="章撒"></property>
    </bean>

5.注入外部properties配置文件

将外部properties文件中的属性注入到bean中,

示例:

@Getter
@Setter
public class Null {
    private String url;
    private String driver;
    private String user;
    private String password;
}
<!-- 引入外部的properties文件开始  在External Libraries里面-->

<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="location" value="classpath:jdbc.properties"/>
</bean>
<bean id="null" class="com.lanou3g.spring.bean.Null">
    <property name="url" value="${jdbc.url}" />
    <property name="driver" value="${jdbc.driver}"/>
    <property name="user" value="${jdbc.user}" />
    <property name="password" value="${jdbc.password}" />
</bean>
public static void main(String[] args) {
        ApplicationContext  ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
       Null n=ctx.getBean(Null.class);
        System.out.println(n.getUrl());
        System.out.println(n.getDriver());
        System.out.println(n.getUser());
        System.out.println(n.getPassword());
}

6.通过p命名空间注入属性

要想使用p命名空间,必须要加相应的p的schema

xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"

通过p命名空间来注入属性

<!--<bean id="yunjie1" class="com.lanou3g.spring.bean.Student">
	<property name="sname" value="张三" />
</bean>-->
<!-- 等效于上面的配置 -->
<bean id="yunjie1" class="com.lanou3g.spring.bean.Student" p:sname="张三" />

构造参数注入

package x.y;

public class ThingOne {
		private int age;
  	private String sname;
  	private int gender;
  	private ThingTwo thingTwo;
    public ThingOne(int age, ThingTwo thingTwo, String sname, int gender) {
        this.age = age;
				this.thingTwo = thingTwo;
      	this.sname = sname;
      	this.gender = gender;
  	    // ...
    }
}


public class ThingTwo {
  // ...
}
<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <!-- 通过此标签注入构造方法中的参数 -->
      	<constructor-arg  name="age" value="18" />
      	<constructor-arg  type="java.lang.String" value="张三" />	
      	<constructor-arg ref="beanTwo"/>
    </bean>

    <bean id="beanTwo" class="x.y.ThingTwo"/>
</beans>

constructor-arg标签属性

属性名

取值

含义

name

字符串

构造参数的名称

value

数值、字符串

构造参数的属性值,只适用于简单类型

type

类的全名

构造参数的类型,如果是简单类型,需要指定对应的封装类型

如果构造参数中有多个命中的类型,按照配置的先后顺序注入(可以通过配合index属性改变默认顺序)

index

数值

构造参数的位置,从0开始

ref

其他在Spring容器中配置的bean的name或者id

将容器中的其他bean作为构造参数注入

通过c命名空间注入构造参数

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- 传统方式注入构造参数 -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="something@somewhere.com"/>
    </bean>

    <!-- c命名空间方式注入构造参数 -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>

方法注入(method Injection)

通过lookup-method方式覆盖目标bean的指定方法

设想一下,如果我们在ioc容器中有个普通的单例类,它有一个方法中需要依赖其他一个非单例的类。由于单例类在整个ioc容器的生命周期中只会被初始化一次,那它依赖的那个非单例的类也就只有一个实例(因为只有初始单例类时注入一次),这显然不满足我们的需求。

Spring除了构造参数注入和setter注入外,还给我们提供了一种方式叫做方法注入,专门用于解决这种场景的问题,具体用法如下

实例

假设有一个果盘,果盘里放了一些水果,比如苹果,香蕉等,我们希望我们每次在果盘里拿到的都是最新鲜的水果。

java代码:

// 定义一个水果类
public class Fruit {
    public Fruit() {
        System.out.println("I got Fruit");
    }
}

// 苹果
public class Apple extends Fruit {
    public Apple() {
        System.out.println("I got a fresh apple");
    }
}

// 香蕉
public class Banana extends Fruit {
    public Banana () {
        System.out.println("I got a  fresh banana");
    }
}

// 水果盘,可以拿到水果
public abstract class FruitPlate{
    // 抽象方法获取新鲜水果
    public abstract Fruit getFruit();
}

spring配置:

<!-- method lookup inject start -->

<!-- 注意这两个bean是prototype -->
<bean id="apple" class="com.john.spring.methodinject.Apple" scope="prototype" />
<bean id="banana" class="com.john.spring.methodinject.Banana" scope="prototype" />

<!-- 苹果果盘 -->
<bean id="applePlate" class="com.john.spring.methodinject.FruitPlate">
  <lookup-method name="getFruit" bean="apple" />
</bean>

<!-- 香蕉果盘 -->
<bean id="bananaPlate" class="com.john.spring.methodinject.FruitPlate">
  <lookup-method name="getFruit" bean="banana" />
</bean>

<!-- method lookup inject end -->

测试代码:

public static void main(String[] args) {
    ApplicationContext app = new ClassPathXmlApplicationContext("classpath:resource/applicationContext.xml");

    FruitPlate fp1= (FruitPlate)app.getBean("fruitPlate1");
    FruitPlate fp2 = (FruitPlate)app.getBean("fruitPlate2");

    fp1.getFruit();
    fp2.getFruit();
}

测试结果:

I got Fruit

I got a fresh apple

I got Fruit

I got a fresh bananer

示例说明:

从上面例子我们可以看到,在代码中,我们没有用到Spring的任何类和接口,实现了与Spring代码的解耦。

其中,最为核心的部分就是lookup-method的配置和FruitPlate.getFruit()方法。上面代码中,我们可以看到getFruit()方法是个抽象方法,我们并没有实现它啊,那它是怎么拿到水果的呢。这里的奥妙就是Srping应用了CGLIB(动态代理)类库。Spring在初始化容器的时候对配置的bean做了特殊处理,Spring会对bean指定的class做动态代理,代理标签中name属性所指定的方法,返回bean属性指定的bean实例对象。每次我们调用fruitPlate1或者fruitPlate2这2个bean的getFruit()方法时,其实是调用了CGLIB生成的动态代理类的方法。关于CGLIB大家可自行在网上查阅。

lookup-method实现方式说明

说白了lookup-method注入是容器覆盖容器管理的bean上的方法并返回容器中另一个命名bean的能力

如果通过xml方式配置,我们可以这样做:

<bean class="beanClass">
    <lookup-method name="method" bean="non-singleton-bean"/>
</bean>

method是beanClass中的一个方法,beanClass和method是不是抽象都无所谓,不会影响CGLIB的动态代理,根据项目实际需求去定义。non-singleton-bean指的是lookup-method中bean属性指向的必须是一个非单例模式的bean,当然如果不是也不会报错,只是每次得到的都是相同引用的bean(同一个实例),这样用lookup-method就没有意义了。

如果通过注解的方式,我们可以这样做:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    // @Lookup //这里也可以不写参数,让Spring容器根据类型来自动返回匹配此类型的bean(有多于一个匹配返回类型的bean会报错)
    protected abstract Command createCommand();
}
lookup-method的目标method的签名标准
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
  • public|protected要求方法必须是可以被子类重写和调用的;
  • abstract可选,如果是抽象方法,CGLIB的动态代理类就会实现这个方法,如果不是抽象方法,就会覆盖这个方法,所以没什么影响;
  • return-type就是non-singleton-bean的类型咯,当然可以是它的父类或者接口。
  • no-arguments不允许有参数。

自动装配

自动装配允许我们不用显示给某个bean注入依赖的属性或者构造参数,而交由Spring自动帮我们注入所需的依赖。

Spring给我们提供了以下四种选项

自动装配模式

说明

no

(默认值) 禁止Spring帮我们自动装配依赖,所有依赖由开发者显示注入。

byName

按照bean的名称注入。比如beanA有一个属性beanB需要注入,如果beanA配置了自动装配模式是byName,那么Spring会在容器中找bean的名称为beanB的bean,找到后自动帮我们注入到beanA中。如果没找到,则不会注入。

byType

按照bean的类型注入。byType模式需要保证容器中符合注入的类型的bean只有一个,如果匹配的类型有不止一个bean,那么会直接抛出运行时异常。如果没有一个匹配类型的bean,则不会注入。

constructor

类似于byType。但这种模式应用的对象是构造参数。如果构造参数需要注入参数类型的bean有不止一个,同样会抛出运行时异常。

byName方式

Java配置

@Setter
@Getter
public class Ceshi {
    private String bname;
    private Stu stu;
}


@Setter
@Getter
public class Stu {
        private int age;

}

XML配置

<!--   给stu的年龄赋值 -->
    <bean id="stu" class="com.lanou3g.spring.simple.Stu">
        <property name="age" value="17"></property>
    </bean>

    <bean id="ceshi" autowire="byName" class="com.lanou3g.spring.simple.Ceshi">
        <property name="bname" value="历史"/>
    </bean>

使用入口

public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Ceshi ceshi=ctx.getBean("ceshi", Ceshi.class);
        System.out.println(ceshi.getBname());
        System.out.println(ceshi.getStu().getAge());
        }

byType方式

使用方式如上
xml配置``

<!--   给stu的年龄赋值 -->
    <bean id="stu" class="com.lanou3g.spring.simple.Stu">
        <property name="age" value="17"></property>
    </bean>

    <bean id="ceshi" autowire="byType" class="com.lanou3g.spring.simple.Ceshi">
        <property name="bname" value="历史"/>
    </bean>

constructor方式

Java代码

@Setter
@Getter
public class Ceshi {
    private String bname;
    private Stu stu;

    public Ceshi(Stu stu) {
        this.stu = stu;
    }
}

xml配置

<!--   给stu的年龄赋值 -->
    <bean id="stu" class="com.lanou3g.spring.simple.Stu">
        <property name="age" value="17"></property>
    </bean>

    <bean id="ceshi" autowire="constructor" class="com.lanou3g.spring.simple.Ceshi">
        <property name="bname" value="历史"/>
    </bean>

使用入口

public static void main(String[] args) {

        ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
        Ceshi ceshi=ctx.getBean("ceshi", Ceshi.class);
        System.out.println(ceshi.getBname());
        System.out.println(ceshi.getStu().getAge());
        }

将某个bean从自动装配候选中排除

我们可以通过给bean添加autowire-candidate属性,并且设置为false,从而让这个bean不参与自动装配(不会被自动装配到其他bean上)