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标签要先声明这个bean
AutowiredAnnotationBeanPostProcessor
<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注入,可能是因为简单吧,或者项目本身不复杂,或者项目的结构欠考虑也说不定。这次总算弄清楚了三种注入的方法的优缺点,如果有误请留言,或者一起讨论,关于几者在循环依赖上的差别,会在之后循环依赖上再进一步研究。