Spring之旅(3)依赖注入

       依赖注入是什么在中已经讲的很清楚了,这边主要陈述依赖注入的几种方法。

constructor-based Dependency injection

构造函数是具体的类

考虑下面一个例子:

  • Spring-context.xml
<bean id="home" class="bean.Home">
       这里改变顺序也是可以的
        <constructor-arg ref="dog1"/>
        <constructor-arg ref="person1"/>
    </bean>

    <bean id="dog1" class="bean.Dog">
        <property name="name" value="dog1"/>
    </bean>

    <bean id="person1" class="bean.Person">
        <property name="name" value="person1"/>
    </bean>
  • Home.java
public class Home {

    private Dog dog;
    private Person person;

    public Home(Dog dog, Person person) {
        this.dog = dog;
        this.person = person;
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        this.dog = dog;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        this.person = person;
    }

    @Override
    public String toString() {
        return "Home{" +
                "dog=" + dog +
                ", person=" + person +
                '}';
    }
}
  • Person.java
package bean;

public class Person {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                '}';
    }
}
  • Dog.java
package bean;

public class Dog {

    private String name;

    public Dog(String name) {
        System.out.println("通过有参constructor进行创建的bean");
        this.name = name;
    }

    public Dog() {
        System.out.println("通过无参constructor进行创建的bean");
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        System.out.println("通过set方法添加属性");
        this.name = name;
    }

    @Override
    public String toString() {
        return "Dog{" +
                "name='" + name + '\'' +
                '}';
    }
}
  • Test.java
import bean.Dog;
import bean.Home;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        Home home = (Home) context.getBean("home");
        System.out.println(home);
    }
}
  • output
Home{dog=Dog{name='dog1'}, person=Person{name='person1'}}

       分析:在xml中我们利用了构造器的这种方法定义了一个bean home,home中注入了id为dog1和person1的两个bean。

构造函数中是int这种基本类型,<constructor-arg>标签来写
<bean id="demo" class="bean.Demo">
        <constructor-arg type="int" value="13"/>
        <constructor-arg type="java.lang.String" value="xx"/>
    </bean>
package bean;

public class Demo {

    private int age;
    private String name;

    public Demo(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Demo{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        Demo demo = (Demo) context.getBean("demo");
        System.out.println(demo);
    }
}

----output----
  Demo{age=13, name='xx'}
通过index来写xml,index是从0开始的
<bean id="demo" class="bean.Demo">
        <constructor-arg index="0" value="13"/>
        <constructor-arg index="1" value="xx"/>
    </bean>
用构造器中参数的name来写xml
<bean id="demo" class="bean.Demo">
        <constructor-arg name="age" value="13"/>
        <constructor-arg name="name" value="xx"/>
    </bean>

setter-based dependency injection

       在调用了无参数类型的构造器或者静态工厂方法实例化之后,setter-based DI会调用setter方法来进行注入。

<bean id="home" class="bean.Home">
        <property name="dog" ref="dog1"/>
        <property name="person" ref="person1"/>
    </bean>

    <bean id="dog1" class="bean.Dog">
        <property name="name" value="dog1"/>
    </bean>

    <bean id="person1" class="bean.Person">
        <property name="name" value="person1"/>
    </bean>
public class Home {

    private Dog dog;
    private Person person;

    public Home() {
        System.out.println("通过无参数构造器创建的bean:home");
    }

    public Dog getDog() {
        return dog;
    }

    public void setDog(Dog dog) {
        System.out.println("通过set方法设置dog");
        this.dog = dog;
    }

    public Person getPerson() {
        return person;
    }

    public void setPerson(Person person) {
        System.out.println("通过set方法设置person");
        this.person = person;
    }

    @Override
    public String toString() {
        return "Home{" +
                "dog=" + dog +
                ", person=" + person +
                '}';
    }
}

----output----
通过无参数构造器创建的bean:home
通过set方法设置dog
通过set方法设置person
Home{dog=Dog{name='dog1'}, person=Person{name='person1'}}

我们可以看到这个顺序确实是先实例化bean之后再去进行set注入的。

filed注入,也就是字段注入

       这边提前用一个autowired,字段注入是我在项目中看见过最多的,其实也是官方最不建议的(官网这种方法提都没有提),可以先用个例子来说明。

  • spring-context.xml,注意要用autowired标签要先声明这个beanAutowiredAnnotationBeanPostProcessor
<bean id="dog" class="bean.Dog">
        <property name="name" value="dog1"/>
    </bean>

    <bean class="org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor"/>

    <bean id="person" class="bean.Person">
        <property name="name" value="person1"/>
    </bean>
    <bean id="home" class="bean.Home"/>
  • Home.java
public class Home {

    @Autowired
    private Dog dog;

    @Autowired
    private Person person;

    @Override
    public String toString() {
        return "Home{" +
                "dog=" + dog +
                ", person=" + person +
                '}';
    }
}
  • Test.java
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        Home home = (Home) context.getBean("home");
        System.out.println(home);
    }
}
----output----
Home{dog=Dog{name='dog1'}, person=Person{name='person1'}}

       不难看出用autowired自动注入的注解也是可以完成的,并且不用写get和set方法了。

依赖处理的过程

       spring会在容器刚建立的时候对每一个bean的配置进行验证,但是在bean被完全创建好之前不会去设置bean的properties。在容器创建的时候,那些被设置成单例(single-scoped)并且设置为pre-instantiated(默认的)bean们会被创建。否则只有当bean被请求的时候才会被创建。下面还有几段是讲循环依赖的,我们可以放在下一个blog。

三种注入方式对比

Constructor-based的优点
构造器可以让你的application components 成为不可变对象,
public class D {

    private final C c;

    public D(C c) {
        this.c = c;
    }

    public C getC() {
        return c;
    }
}
package bean;

public class C {
    private String name;

    public C(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "C{" +
                "name='" + name + '\'' +
                '}';
    }
}
<bean class="bean.C" id="c">
  <constructor-arg type="java.lang.String" value="xx"/>
</bean>

<bean class="bean.D" id="d">
  <constructor-arg name="c" ref="c"/>
</bean>

       根据我搜集的资料和stack overflow上的提问,目前看来是在自己的component上使用final 关键字,这样就无法去改变这个field,也就是说一旦注入进去了就不能再改变了,具体不可变对象immutable object用作什么用处还尚未可知,可能要看具体的场景。

防止NPE也就是所说的空指针异常
public class D {

    private C c;

    public D(C c) {
        System.out.println("D调用构造器注入了c依赖");
        this.c = c;
    }

    public C getC() {
        return c;
    }
}
package bean;

public class C {
    private String name;

    public C(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "C{" +
                "name='" + name + '\'' +
                '}';
    }
}
<bean class="bean.C" id="c">
  <constructor-arg type="java.lang.String" value="xx"/>
</bean>

# 注意这边故意没有将c注入到d里面
<bean class="bean.D" id="d"></bean>
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        D d = (D) context.getBean("d");
        C c = d.getC();
        System.out.println(c);
    }
}
----output----
D调用构造器注入了c依赖
C{name='xx'}

       说明即使没有手动注入,他也会找到c进行注入。那么如果我都没有往容器中注入c会怎么样呢,我们把xml文件修改。

<bean class="bean.D" id="d"></bean>

----output----
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'd' defined in class path resource [spring/spring-context.xml]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'bean.C' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

       直接报错,也就是说,除非它所需要的依赖完全注入进去了,否则不会初始化成功的,就会避免了NPE问题,也就避免了我们写业务逻辑的时候去对他进行空指针检测。

能够准确的看出bean所依赖的对象数目

       考虑一下:如果说我们用构造器注入的时候,一眼就能看出这个bean需要多少其他bean,会很直接地看出它的职责是不是太多了,如果太多可能就要考虑拆分。

由构造器创建的bean是初始状态

       这个优点是看的其他blog上的,说和类加载有关,尚未可知。

完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法,这里不详细展开。)。所以返回来的都是初始化之后的状态。

setter方法注入

主要是和构造器方法对比:

  • 不能成为immutable object 很明显,final 对象不能set进行改变
  • 空指针异常,就需要进行check了
故意没有进行注入
<bean class="bean.D" id="d"></bean>
public class D {

    private C c;

    public C getC() {
        return c;
    }

    public void setC(C c) {
        this.c = c;
    }
}
package bean;

public class C {
    private String name;

    public C(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "C{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        D d = (D) context.getBean("d");
        C c = d.getC();
        System.out.println(c);
    }
}

----output----
null
field方法注入

       精彩绝伦之处!为什么官方对这种方法提都不提呢?因为field式注入存在着诸多问题。

从final 不可变的角度去看问题
<bean class="bean.C" id="c">
  <constructor-arg type="java.lang.String" value="xx"/>
</bean>

<bean class="bean.D" id="d"></bean>
public class D {

    @Autowired
    private final C c = null;

    public D() {
        System.out.println("先通过构造器创建bean");
    }

    public C getC() {
        return c;
    }

}
package bean;

import org.springframework.stereotype.Component;

public class C {
    private String name;

    public C(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "C{" +
                "name='" + name + '\'' +
                '}';
    }
}
public class Test {
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("classpath:spring/spring-context.xml");
        D d = (D) context.getBean("d");
        C c = d.getC();
        System.out.println(c);
    }
}

----output----
先通过构造器创建bean
C{name='xx'}

       这边可以看出,我本来是设置的final 字段,按理说是不可改的,那么事实上启动的时候,确实注入进去了。问题出在哪我没那个本事找出来,如果你知道,教教我吧,我看了半天。。。但是可以看出,final失效了。

NPE角度

       据我所知,field式注入似乎只能通过autowired注解注入,如果像下面这样没有在容器中注入c这个bean,程序启动会报错。

<bean class="bean.D" id="d"></bean>
public class D {

    @Autowired
    private C c;

    public C getC() {
        return c;
    }
}


----output----
  Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'bean.C' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

所以说,autowired式的field似乎并不会出现NPE异常。

bean的数量

       很显然,通过field注入,看似很简单,也不用写其他的东西,但是它是很隐蔽的,你根本不会注意到bean里面到底包含了多少个其他bean。可能就会违背Single Responsibility Principle.

耦合度很高

       用field注入,你可以不用写get和set方法,那么问题就会出现,似乎注入是通过反射到方法进行的,如果你离开了spring容器,你将无法进行测试,也就是说你必须在测试的时候依赖于spring容器,就要在集成的环境下进行测试了,耦合度明显是提高的。

依赖

       用了field注入,这些bean依赖会隐藏起来,既不会在构造器中反应也不会在其他地方看见,到底依赖哪些呢?很难看出来了。

总结

       事实上呢,官方推荐的是构造器注入的方式,5.x的文档上目前看到的地方还没提到field注入,我接触的项目里面大多数都是用的field注入,可能是因为简单吧,或者项目本身不复杂,或者项目的结构欠考虑也说不定。这次总算弄清楚了三种注入的方法的优缺点,如果有误请留言,或者一起讨论,关于几者在循环依赖上的差别,会在之后循环依赖上再进一步研究。