Spring 框架支持三种依赖注入方法:属性注入、构造函数注入以及工厂方法注入。下面,我们将一一予以说明。
1 属性注入
属性注入即通过 setXxx() 方法注入 Bean 的属性值或者依赖对象,由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际项目中最常采用的注入方式 。
1.1 实例
属性注入要求 Bean 提供一个默认的构造函数,并为需要注入的属性提供对应的 Setter 方法 。Spring 先调用 Bean 的默认构造函数实例化 Bean 对象,然后通过反射来调用 Setter 方法注入属性值 。 下面我们举一个例子:
public class Book {
/**
* 书名
*/
private String name;
/**
* 定价
*/
private double price;
/**
* 字数
*/
private int wordNum;
public void setName(String name) {
this.name = name;
}
public void setPrice(double price) {
this.price = price;
}
public void setWordNum(int wordNum) {
this.wordNum = wordNum;
}
...
}
注意:默认构造函数是不带参数的构造函数 。Java 语言规定,如果类中没有定义任何构造函数,则 JVM 会自动为其生成一个默认的构造函数 。 反之,如果类中显式定义了构造函数,那么 JVM 就不会为其生成默认的构造函数 。
属性注入配置:
<bean id="book" class="net.deniro.spring4.bean.Book">
<property name="name">
<value>面纱</value>
</property>
<property name="price">
<value>25.5</value>
</property>
<property name="wordNum">
<value>80000</value>
</property>
</bean>
注意: Spring 只会检查 Bean 中是否有对应的 Setter 方法,至于 Bean 中是否有对应的属性变量则不做要求,因为它底层是采用 Java 的反射方法来设置值的呀O(∩_∩)O哈哈~
单元测试:
ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
Book book= (Book) context.getBean("book");
Assert.assertEquals(book.getName(),"面纱");
1.2 Java Bean 属性命名规范
Java Bean 属性命名规范是 xxx 的属性,它对应着的是 setXxx() 方法。
一般情况下,Java 的属性变量名都以小写字母起头。但也存在特殊情况 , 比如一些具有特定意义的大写英文缩略词(如 JSON、AJAX等)。 Java Bean 也允许大写字母起头的属性变量名 , 不过必须满足 ” 变量的前两个字母要么全部大写 , 要么全部小写 ” 的要求 。如 : IDCard 的属性名就是合法的,但 iDCard 属性名则是非法的,虽然 Spring 框架仍然可以正确注入,但不推荐。
所以,为了简便起见,对于以大写英文缩略词出现的专业术语,一律将其调整为小写形式,如 id 等,这样可以保证命名的统一性。
2 构造函数注入
构造函数注入保证一些必要的属性在 Bean 实例化时就得到设置,这样 Bean 在实例化后就可以使用啦O(∩_∩)O哈哈~
使用构造函数注入的前提是 Bean 必须提供带参的构造函数。
2.1 按类型匹配入参
我们为 “书” 这个类,添加一个带参的构造函数:
public Book(String name, double price) {
this.name = name;
this.price = price;
}
配置:
<bean id="book2" class="net.deniro.spring4.bean.Book">
<constructor-arg type="java.lang.String">
<value>人生的枷锁</value>
</constructor-arg>
<constructor-arg type="double">
<value>35</value>
</constructor-arg>
</bean>
在 <constructor-arg>
的元素中有一个 type 属性,它表示构造函数中参数的类型,这是 spring 用来判断配置项与构造函数入参对应关系的 “桥梁”。
注意:Spring 的配置采用了与元素标签顺序有关的策略,所以上面的多个 <constructor-arg>
位置并会对配置效果产生影响。
2.2 按索引匹配入参
如果 Book 类定义的构造函数具有多个类型相同入参,那么就需要依赖配置顺序咯,如果想定义的更加灵活,那么就可以使用 “ 按索引匹配入参”。
注意: Spring 底层是采用 Java 反射能力来实现依赖注入的,但 Java 反射无法获知构造函数的参数名,所以只能通过入参类型与索引信息来间接地确定构造函数的配置项与入参之间的对应关系。
假设 Book 类中新增了 “出版社” 属性:
/**
* 出版社
*/
private String press;
public Book(String name, String press) {
this.name = name;
this.press = press;
}
配置文件:
<bean id="book3" class="net.deniro.spring4.bean.Book">
<constructor-arg index="0">
<value>人生的枷锁</value>
</constructor-arg>
<constructor-arg index="1">
<value>上海译文出版社</value>
</constructor-arg>
</bean>
构造函数的第一个参数索引为 0,第二个为 1,以此类推。
2.3 联合使用类型匹配入参与索引匹配入参
有时需要联合使用 type 和 index 属性才能确定匹配项和构造函数入参的对应关系:
public Book(String name, String press, double price) {
this.name = name;
this.press = press;
this.price = price;
}
public Book(String name, String press, int wordNum) {
this.name = name;
this.press = press;
this.wordNum = wordNum;
}
这里的 Book 拥有两个重载的构造函数,它们都有三个入参,这时就需要联合使用类型匹配入参与索引匹配入参来确定关系:
<bean id="book4" class="net.deniro.spring4.bean.Book">
<constructor-arg index="0" >
<value>人生的枷锁</value>
</constructor-arg>
<constructor-arg index="1" >
<value>上海译文出版社</value>
</constructor-arg>
<constructor-arg index="2" type="double">
<value>22.5</value>
</constructor-arg>
</bean>
真正引起歧义的地方在于第 3 个入参,所以我们在此仅需明确指定第 3 个入参的类型就可以取消歧义咯O(∩_∩)O哈哈~
注意:对于由于参数数目相同而类型不同所引起的潜在配置歧义问题, Spring 容器可以正确启动但不会给出报错信息,它将随机采用一个匹配的构造函数来实例化 Bean ,而被选择的构造函数可能并不是我们所希望的 。 因此,在配置时必须小心谨慎,以避免发生意外。
建议显式地指定 index 与 type,因为这样可以明确指定 Bean 中的一个构造函数。
2.4 通过自身类型反射匹配入参
如果 Bean 构造函数的入参类型不是基础数据类型,而且入参类型各异,那么可以通过 Java 反射机制获取构造函数的入参类型。
假设在 “书” 中加入了作者实体类:
public Book(String name, Author author) {
this.name = name;
this.author = author;
}
/**
* 作者
*/
private Author author;
Author.java:
public class Author {
public Author(String name) {
this.name = name;
}
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "Author{" +
"name='" + name + '\'' +
'}';
}
}
配置文件如下:
<bean id="author" class="net.deniro.spring4.bean.Author">
<constructor-arg value="毛姆"/>
</bean>
<bean id="book5" class="net.deniro.spring4.bean.Book">
<constructor-arg>
<value>人生的枷锁</value>
</constructor-arg>
<constructor-arg>
<ref bean="author"/>
</constructor-arg>
</bean>
2.5 循环依赖
实例化构造函数配置的 Bean 的前提条件是:这个 Bean 构造函数的入参对象必须已准备就绪(已实例化)。如果存在两个 Bean,它们都使用构造函数的注入方式,并且它们的入参互相引用的话,就会发生循环依赖的问题。
假设 Book 中拥有 Author:
public class Book {
public Book(String name, Author author) {
this.name = name;
this.author = author;
}
/**
* 作者
*/
private Author author;
...
}
而 Author 中拥有 Book:
public class Author {
public Author(String name, Book book) {
this.name = name;
this.book = book;
}
private Book book;
...
}
配置文件中,Book 与 Author 都采用构造函数的依赖注入方式:
<bean id="author10" class="net.deniro.spring4.bean.Author">
<constructor-arg index="0" value="毛姆"/>
<constructor-arg index="1" ref="book10"/>
</bean>
<bean id="book10" class="net.deniro.spring4.bean.Book">
<constructor-arg index="0" value="人生的枷锁"/>
<constructor-arg index="1" ref="author10"/>
</bean>
这时,启动 Spring 容器,就会抛出以下错误:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'author10': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:347)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:223)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:302)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:351)
... 62 more
因为存在循环依赖的问题。
我们可以把它们的注入方式都改为属性注入方式,就可以解析上述的问题啦O(∩_∩)O哈哈~
3 工厂方法注入
工厂方法是被经常使用到的设计模式,它也是控制反转与单实例设计思想的主要实现方式。它已经成为 Spring 框架的底层基础设施的一部分。
3.1 非静态工厂方法
非静态,即必须实例化工厂类之后,才能调用其方法。
public class BookFactory {
public Book create() {
return new Book("面纱", "重庆出版社");
}
}
工厂类负责创建目标类实例,它对外屏蔽了目标类的实例化细节,返回的类型一般是接口或抽象类的形式。
<bean id="bookFactory" class="net.deniro.spring4.bean.BookFactory"/>
<!-- factory-bean:指定工厂类;factory-method:指定工厂方法-->
<bean id="book11" factory-bean="bookFactory" factory-method="create"/>
3.2 静态工厂方法
很多工厂方法是以静态的方式实现的,因为更易使用,直接调用方法就可以啦。
<!--class:指定工厂类; factory-method:指定静态工厂方法 -->
<bean id="book12" class="net.deniro.spring4.bean.BookFactory" factory-method="createBook"/>
4 如何选择
支持构造函数注入的理由有这些:
- 构造函数可以保证一些重要的属性在 Bean 实例化时就设置好,避免因为一些重要属性没有提供而导致一个无用 Bean 实例出现的情况 。
- 不需要为每个属性提供 Setter 方法,减少了类的方法总数 。
- 可以更好的封装变量,不需要为每个属性指定 Setter 方法,避免外部错误的调用 。
更多的开发者更倾向于使用属性注入的方式,他们反对构造函数注入方式的理由是:
- 如果一个类的属性众多,那么构造函数的签名将变成一个庞然大物,可读性与可维护性都很差。
- 灵活性不强,在有些属性是可选的情况下,如果通过构造函数注入,也需要为非必填的参数提供一个 null 值。
- 如果有多个构造函数,那么需要考虑配置文件和具体构造函数匹配歧义的问题,配置上相对复杂。
- 构造函数不利于类的继承和扩展,因为子类需要引用父类复杂的构造函数。
- 构造函数注入有时会造成循环依赖的问题。
建议一般情况下,使用属性注入;必要情况下,使用构造函数注入;只有在不得已的情况下,才使用工厂方法注入。