Spring 支持两种依赖注入方式,分别是属性注入和构造函数注入。 除此之外,Spring 还支持工厂方法注入方式。
属性注入
属性注入指通过 setXxx() 方法注入 Bean 的属性值或依赖对象。由于属性注入方式具有可选择性和灵活性高的优点,因此属性注入是实际应用中最常采用的注入方式。
1. 属性注入实例
属性注入要求 Bean 提供一个默认的构造函数,并为需要注入的属性提供对应的 Setter 方法,Spring 先调用 Bean 的默认构造函数实例化 Bean 对象,然后通过反射的方式调用 Setter 方法注入属性值。来看一个简单的例子。
package com.smart.ditype; public class Car { public String brand; private double price; private int maxSpeed; public void setBrand(String brand) { this.brand = brand; } public void setMaxSpeed(int maxSpeed) { this.maxSpeed = maxSpeed; } public void setPrice(double price) { this.price = price; } ... }
<bean id="car" class="com.smart.ditype.Car">
<property name="brand" value="红旗CA72"/>
<property name="maxSpeed" value="200"/>
<property name="price" value="20000.00"/>
</bean>
上述代码配置了一个 Bean,并为该 Bean 的3个属性提供了属性值。具体来说,Bean 的每一个属性对应一个 <property> 标签,name 为属性的名称,在 Bean 实现类中拥有与其对应的 Setter 方法:maxSpeed 对应 setMaxSpeed(),brand 对应 setBrand()。
需要指出的是,Spring 只会检査 Bean 中是否有对应的 Setter 方法,至于 Bean 中是否有对应的属性成员变更则不做要求。举个例子,文件中 <property name="brand"> 的属性配置项仅要求 Car 类中拥有 setBrand() 方法,但Car类不一定要拥有 brand 成员变量,如下:
public Class Car{ private int maxSpeed; private double price; //①仅拥有setBrand()方法,但类中没有brand成员变量 public void setBrand(String brand){ System.out.println("设置brand属性"); } }
虽然如此,但在一般情况下,仍然按照约定俗成的方式在 Bean 中提供同名的属性变量。
2. JavaBean 关于属性命名的特殊规范
Spring 配置文件中 <property> 元素所指定的属性名和 Bean 实现类的 Setter方法满足 Sun JavaBean 的属性命名规范: xxx 的属性对应 setXxx() 方法。
—般情况下,Java 的属性变量名都以小写字母开头,如 maxSpeed、brand 等,但也存在特殊的情况。考虑到一些特定意义的大写英文缩略词(如 USA、XML 等),JavaBean 也允许以大写字母开头的属性变量名,不过必须满足"变置的前两个字母要么全部大写,要么全部小写"的要求,如 brand、IDCode、IC、ICCard 等属性变量名是合法的,而 iC、iCcard、iDCode 等属性变量名则是非法的。这个并不广为人知的 JavaBean 规范条款引发了众多让人闲惑的配置问题。
构造函数注入
构造函数注入是除属性注入外的另一种常用的注入方式,它保证一些必要的属性在 Bean 实例化时就得到设罝,确保 Bean 在实例化后就可以使用。
1. 按类型匹配入参
如果任何可用的 Car 对象都必须提供 brand 和 price 的值,若使用属性注入方式,则只能人为地在配置时提供保证而无法在语法级提供保证,这时通过构造函数注入就可以很好地满足这一要求。使用构造函数注入的前提是Bean必须提供带参的构造函数。下面为 Car 提供一个可设置 brand 和 price 属性的构造函数。
public class Car{ ... public Car(String brand, double price){ this.brand = brand; this.price = price; } }
构造函数注入的配置方式和属性注入的配置方式有所不同,下面在 Spring 配置文件中使用构造函数注入的配罝方式装配这个 car Bean。代码如下所示。
<bean id="carl" class="com.smart.ditype.Car"> <constructor-arg type="java.lang.String">① <value>红旗CA72</value> </constructor-arg> <constructor-arg type="double">② <value>20000</value> </constructor-arg> </bean>
在 <constructor-arg> 的元素中有一个 type 属性,它为 Spring 提供了判断配置项和构造函数入参对应关系的"信息"。细心的读者可能会提出以下疑问:配罝文件中 <bean> 元素的 <constructor-arg> 声明顺序难道不能用于确定构造函数入参的顺序吗?在只有一个构造函数的情况下当然是可以的,但如果在 Car 中定义了多个具有相同入参的构造函数,这种顺序标识方法就失效了。此外,Spring 的配置文件采用和元素标签顺序无关的策略,这种策略可以在一定程度上保证配罝信息的确定性,避免一些似是而非的问题。因此,①和②处的 <constructor-arg> 位置并不会对最终的配罝效果产生影响。
2. 按索引匹配入参
我们知道 Java 语言通过入参的类型及顺序区分不冋的重载方法。对于上面代码中的 Car 类,Spring 仅通过 type 属性指定的参数类型就可以知道 "红旗CA72" 对应 String 类型的 brand 入参,而 "20000" 对应 double 类型的 price 入参。但是如果 Car 构造函数有两个类型相同的入参,那么仅通过 type 就无法确定对应关系了,这时需要通过入参索引的方式进行确定。
为了更好地演示按索引匹配入参的配置方式,我们特意对 Car 构造函数进行了以下调整:
//①该构造函数第一、第二入参都是String类型 public Car(String brand,String corp,double price){ this.brand = brand; this.corp = corp; this.price = price; }
因为 brand 和 corp 的入参类型都是 String,所以 Spring 无法确定 type 为 String 的 <constructor-arg> 到底对应的是 brand 还是 corp,但是通过显式指定参数的索引能够消除这种不确定性,代码清单如下。
<bean id="car2" class="com.smart.ditype.Car">
<!--①注意索引从0开始-->
<constructor-arg index="0" value="红旗CA72"/>
<constructor-arg index="1" value="中国一汽"/>
<constructor-arg index="2" value="20000"/>
</bean>
构造函数的第一个参数索引为 0,第二个为 1,以此类推,因此很容易知进"红旗CA72"对应 brand 入参,而 "中国一汽" 对应 corp 入参。
3. 联合使用类型和索引匹配入参
有时需要 type 和 index 联合使用才能确定配置项和构造函数入参的对应关系,来看下面的例子。
入参数目相同的构造函数
public Car(String brand,String corp,double price){ this.brand = brand; this.corp = corp; this.price = price; } public Car(String brand,String corp,int maxSpeed){ this.brand = brand; this.corp = corp; this.maxSpeed = maxSpeed; }
这里,Car 拥有两个重载的构造函数,它们都有两个入参。按照入参位罝索引的配罝方式针对这种情况又难以满足要求了,这时需要联合使用 <constructor-arg> 的 type 和 index 才能解决问题,如下所示。
通过入参类型和位置索引确定对应关系
<bean id="car3" class="com.smart.ditype.Car"> <constructor-arg index="0" type="java.lang.String"> <value>红旗CA72</value> </constructor-arg> <constructor-arg index="1" type="java.lang.String"> <value>中国一汽</value> </constructor-arg> <constructor-arg index="2" type="int"> <value>200</value> </constructor-arg> </bean> <bean id="car4" class="com.smart.ditype.Car"> <constructor-arg index="0"> <value>红旗CA72</value> </constructor-arg> <constructor-arg index="1"> <value>中国一汽</value> </constructor-arg> <constructor-arg index="2" type="double"> <value>200</value> </constructor-arg> </bean>
对于上面两个构造函数的情况,如果仅通过 index 进行配置,那么 Spring 将无法确定第三个入参配置项究竞是对应 int 的 maxSpeed 还是 double 的 price,所以在采用索引匹配配罝时,真正引起歧义的地方是第三个入参,因此仅需要明确指定第三个入参的类型就可以取消歧义。
对于因参数数目相同而类型不同引起的潜在配罝歧义问题,Spring 容器可以正确启动且不会给出报错信息,它将随机采用一个匹配的构造函数实例化 Bean,而被选择的构造函数可能并不是用户所期望的那个。因此,必须特别谨慎,以避免潜在的错误。
4. 通过自身类型反射匹配入参
当然,如果 Bean 构造函数入参的类型是可辨别的(非基础数据类型且入参类型各异),由于Java 反射机制可以获取构造函数入参的类型,即使构造函数注入的配罝不提供类型和索引的信息。Spring 依旧可以正确地完成构造函数的注入工作。下面 Boss 类构造函数的入参就是可以辨别的:
public Boss(String name, Car car, Office office){ this.nane = name; this.car = car; this.office = office; }
由于car、office 和 name 入参的类型都是可辨别的,所以无须在构造函数注入的配置时指定 <constructor-arg> 的类型和索引,因此我们可以采用如下简易的配置方式 :
<bean id="boss1" class="com.smart.ditype.Boss"> <!--①没有设置 type 和 index属性,通过入参值的类型完成匹配映射--> <constructor-arg> <value>John</value> </constructor-arg> <constructor-arg> <ref bean="car" /> </constructor-arg> <constructor-arg> <ref bean="office" /> </constructor-arg> </bean> <bean id="office" class="com.smart.ditype.Office" />
但是为了避免潜在配置歧义引起的张冠李載的情况,如果 Bean 存在多个构造函数,那么使用显示指定 index 和 type 属性不失为一种良好的配罝习惯。
5. 循环依赖问题
Spring 容器能对构造函数配置的 Bean 进行实例化有一个前提,即 Bean 构造函数入参引用的对象必须己经准备就绪。由于这个机制的限制,如果两个 Bean 都采用构造函数注入,而且都通过构造函数入参引用对方,就会发生类似于线程死锁的循环依赖问题。来看一个发生循环依赖问题的例子:
public class Car { //①构造函数依赖于一个boss实例 public Car(String brand, Boas boss)( this.brand = brand; this.boss = boss; } } public class Boss { //②构造函数依赖于一个car实例 public Boss(String name, Car car){ this.name = name; this.car = car; } }
假设在 Spring 配置文件中按照以下构造函数注入方式进行配置
<bean id="car" class="com.smart.cons.Car"> <constructor-arg index="0" value="红旗CA72"> <!--①引用②处的boss --> <constructor-arg index="1" ref="boss"> </bean> <bean id="boss" class="com.smart.cons.Boss"> <constructor-arg index="0" value="John"> <!--②引用①处的car --> <constructor-arg index="1" ref="car"> </bean>
当启动 Spring IoC 容器时,因为存在循环依赖问题,Spring 容器将无法成功启动。如何解决这个问题呢?用户只需修改 Bean 的代码,将构造函数注入方式调整为属性注入方式就可以了。
工厂方法注入
工厂方法是在应用中被经常使用的设计模式,它也是控制反转和单实例设计思想的主要实现方法。由于 Spring IoC 容器以框架的方式提供工厂方法的功能,并以透明的方式开放给开发者,所以很少需要手工编写基于工厂方法的类。正是因为工厂方法己经成为底层设施的一部分,因此工厂方法对于实际编码的重要性就降低了。不过在一些遗留系统或第三方类库中,我们还会遇到工厂方法,这时可以使用 Spring 工厂方法注入的方式进行配置。
1. 非静态工厂方法
有些工厂方法是非静态的,即必须实例化工厂类后才能调用工厂方法。下面为 Car 提供一个非静态的工厂类,如下所示。
CarFactory: 非静态工厂方法
public class CarFactory { public Car createHongQiCar(){ Car car = new Car(); car.setBrand("红旗CA72"); return car; } public static Car createCar(){ Car car = new Car(); return car; } }
工厂类负责创建一个或多个目标类实例,工厂类方法一般以接口或抽象类变量的形式返回目标类实例。工厂类对外屏蔽了目标类的实例化步骤,调用者甚至无须知道具体的目标类是什么。CarFactory 工厂类仅负责创建 Car 类型的对象,下面的配置片段使用 CarFactory 为 Car 提供工厂方法的注入,如下所示。
通过工厂类注入 Bean
<!--①工厂类Bean-->
<bean id="carFactory" class="com.smart.ditype.CarFactory" />
<!--factory-bean指定①处的工厂类Bean:factory-method指定工厂类Bean创建该Bean的工厂方法-->
<bean id="car5" factory-bean="carFactory" factory-method="createHongQiCar"></bean>
由于 CarFactory 工厂类的工厂方法不是静态的,所以首先需要定义一个工厂类的Bean,然后通过 factory-bcan 引用工厂类实例,最后通过 factory-method 指定对应的工厂类方法。
2. 静态工厂方法
很多工厂类方法都是静态的,这意味着用户在无须创建工厂类实例的情况下就可以调用工厂类方法,因此,静态工厂方法比非静态工厂方法更易使用。下面对 CarFactory 进行改造,将其 createHongQiCar() 方法调整为静态的,如下所示。
CarFactory:静态工厂方法
public class CarFactory { //①工厂类方法是静态的 public static Car createHongQiCar(){ ... } }
当使用静态工厂类型的方法后,用户就无须在配罝文件中定义工厂类的Bean只需按以下方式进行配置即可:
<bean id="car6" class="com.smart.ditype.CarFactory" factory-method="createCar"></bean>
直接在 <bean> 中通过 class 属性指定工厂类,然后再通过 factory-method 指定对应的工厂方法。
选择注入方式的考量
Spring 提供了3种可供选择的注入方式,在实际应用中,究竟应该选择哪种注入方式呢?对于这个问题,仁者见仁,智者见智,并没有统一的标准。
下面是支持使用构造函数注入的理由:
1)构造函数可以保证一些重要的属性在 Bean 实例化时就设置好,避免因为一些重要属性没有提供而导致一个无用 Bean 实例的情况。
2)不需要为每个属性提供 Setter 方法,减少了类的方法个数。
3)可以更好地封装类变量,不需要为每个属性指定 Setter 方法,避免外部错误的调用。
更多的开发者可能倾向于使用属性注入方式,他们反对构造函数注入的理由如下:
1)如果一个类属性众多,那么构造函数的签名将变成一个庞然大物,可读性很差。
2)灵活性不强,在有些属性是可选的情况下,如果通过构造函数注入,也需要为可选的参数提供一个null值。
3)如果有多个构造函数,则需要考虑配罝文件和具体构造函数匹配歧义的问题,配置上相对复杂。
4)构造函数不利于类的继承和扩展。因为子类需要引用父类复杂的构造函数。
5)构造函数注入有时会造成循环依赖的问题。
其实构造函数注入和属性注入各有自己的应用场景,Spring 并没有强制用户使用哪一种方式,用户完全可以根据个人偏好做出选择,在某些情况下使用构造函数注入,而在另一些情况下使用属性注入,对于一个全新开发的应用来说。我们不推荐使用工厂方法的注入方式,因为工厂方法需要额外的类和代码,这些功能和业务是没有关系的。既然 Spring 容器己经以一种更优雅的方式实现了传统工厂模式的所有功能,那么我们大可不必再去做这项重复性的工作。