Spring支持两种依赖注入的方式
此外Spring还支持工厂方法注入。 这篇博文我们将了解到不同注入方式的具体配置方法。
属性注入指的是通过setXxx()方法注入Bean的属性值或者依赖对象。
由于属性注入方式具有可选择性和灵活性高的有点,因此属性注入是实际应用中最常用的注入方式。
属性注入的要求
Spring先调用Bean的默认构造函数实例化Bean对象,然后通过反射调用Setter方法注入属性值。
POJO对象
package com.xgj.ioc.inject.set;
public class Plane {
private String brand;
private String color;
private int speed;
/**
* 在没有其他显示构造函数的情况下,默认构造函数可省略,有则必须声明
*/
public Plane(){
}
public Plane(String brand,String color,int speed){
this.brand = brand;
this.color = color;
this.speed = speed;
}
public void setBrand(String brand) {
this.brand = brand;
}
public void setColor(String color) {
this.color = color;
}
public void setSpeed(int speed) {
this.speed = speed;
}
public void introduce() {
System.out.println("Plane brand:" + brand + ",color:" + color
+ ",speed," + speed);
}
}
Bean配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="plane" class="com.xgj.ioc.inject.set.Plane">
<property name="brand">
<value>A380value>
property>
<property name="color">
<value>redvalue>
property>
<property name="speed">
<value>700value>
property>
bean>
beans>
测试类:
package com.xgj.ioc.inject.set;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class SetTest {
public static void main(String[] args) {
// 加载配置文件,实例化bean
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/set/plane.xml");
// 从容器中获取bean
Plane plane = ctx.getBean("plane",Plane.class);
// 调用方法
plane.introduce();
}
}
解读:
上面的代码配置了一个Bean,并为该Bean的3个属性提供了属性值, 具体来说Bean的每一个属性对应一个property标签,name为属性的名称(上述配置较为繁琐,后续介绍p的用法),在Bean实现类中拥有与其对应的Setter方法: 比如 brand对应setBrand()。
有一点需要注意: spring只会检查Bean中是否有对应的Setter方法,至于Bean中是否有对应的属性成员变更则不做要求。
举个例子:
配置文件中
的属性配置项仅要求Plane中拥有setBrand()方法,但Plane类中不一定要拥有brand成员变量。
比如
private String color;
private int speed;
// 仅拥有setBrand方法,但是类中没有brand成员变量
public void setBrand(String brand) {
......
}
但一般情况下,还是按照约定俗成的方式在Bean中提供同名的属性变量
注意:
默认构造函数是不带参数的构造函数。 Java语言规定,如果类中没有定义任何构造函数,JVM会自动为其生成一个默认的构造函数;反之,如果类中显式的定义了构造函数,JVM则不会为其生成默认的构造函数。如果类中显式的声明了其他构造函数,如果未提供一个默认的构造函数,则属性注入时,会抛出异常。
举例:
我们在Plane中增加一个显式构造函数,去掉默认的构造函数
public Plane(String brand,String color,int speed){
this.brand = brand;
this.color = color;
this.speed = speed;
}
运行测试类,
Failed to instantiate [com.xgj.ioc.inject.set.Plane]: No default constructor found; nested exception is java.lang.NoSuchMethodException: com.xgj.ioc.inject.set.Plane.()
Spring配置文件中property元素所指定的属性名和Bean实现类的Setter方法满足Sun JavaBean的属性命名规范: xxx的属性对应setXxx()方法.
一般情况下,Java的属性变量名都以小写字母开头,比如brand,speed等,但也有些特殊情况的存在。考虑到一些特定意义的大写英文缩略字母(比如USA、XML等),Javabean也允许以大写字母开头的属性变量名,不过必须满足 变量的前两个字母要么全部大写,要么全部小写。
比如brand,IDCode、IC等是合法的,而iC、iDCard等是非法的。
比如我们在Plane类中添加属性和setter方法
// 非法的属性变量名,但是Java并不会报错,因为它将iDCard看做普通的变量
private String iDCard;
// 改setter方法对应IDCard属性,而非iDCard属性
public void setIDCard(String iDCard) {
this.iDCard = iDCard;
}
因为xxx的属性对应setXxx()方法. 所以是setIDCard().
bean配置文件
STS中校验都未通过,更加无法运行了。
总结:
以大写字母开头的变量总是显得比较另类,为了规避这种诡异的错误,用户可遵照以下编程的经验:比如QQ、MSN、ID等正常情况下以大写字母出现的专业术语,在Java中一律调整为小写形式,如qq、msn、id等,以保证命名的统一性(变量名称都小写字母开头),减少出错概率。
构造函数注入是除了属性注入之外另外一种常用的注入方式,构造函数注入保证一些必要的属性在Bean实例化的时候得到设置,确保Bean在实例化之后就可以使用
举个例子,假设任何使用Tank对象都必须提供brand和weight,若使用属性注入 方式,这只能人为在配置时候提供保证而无法再语法级提供保证,这时候构造函数注入就可以很好地满足这一个需求。
使用构造函数的前提是Bean必须提供带参的构造函数。
代码演示:
Pojo Bean实现类
package com.xgj.ioc.inject.construct;
public class Tank {
private String brand;
private double weight;
public Tank(String brand, double weight) {
super();
this.brand = brand;
this.weight = weight;
}
public void introduce() {
System.out.println("Tank information: brand:" + brand + ",weight:"
+ weight + "KG");
}
}
配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="tank" class="com.xgj.ioc.inject.construct.Tank">
<constructor-arg type="java.lang.String">
<value>T72value>
constructor-arg>
<constructor-arg type="double">
<value>15000.00value>
constructor-arg>
bean>
beans>
测试类:
package com.xgj.ioc.inject.construct;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConstructInjectTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/tank.xml");
Tank tank = ctx.getBean("tank", Tank.class);
tank.introduce();
}
}
运行结果:
解读:
在constructor-arg的元素中有个type属性,它为Spring提供了判断配置项和构造函数入参对应关系的信息。
一个疑问
另外,Spring的配置文件采用和元素标签顺序无关的策略,一定程度上保证了配置信息的确定性,避免一些似是而非的问题。
<constructor-arg type="java.lang.String">
<value>T72value>
constructor-arg>
<constructor-arg type="double">
<value>15000.00value>
constructor-arg>
这两个参数的位置并不会影响对最终的配置效果产生影响。
众所周知,Java语言通过入参的类型和顺序区分不同的重载方法。
如果Tank类中有两个相同类型的入参,仅仅通过type就无法确定的对应关系了。这是需要通过入参索引的方式进行确定。
POJO对象
package com.xgj.ioc.inject.construct.index;
public class Tank {
private String brand;
private double weight;
private double speed;
/**
*
* @param brand
* @param weight
* @param speed
* 第二个参数和第三个参数同为double类型
*/
public Tank(String brand, double weight, double speed) {
this.brand = brand;
this.weight = weight;
this.speed = speed;
}
public void introduce() {
System.out.println("Tank information: brand:" + brand + ",weight:"
+ weight + "KG,speed:" + speed + "km/h");
}
}
第二个参数和第三个参数同为double类型,所以Spring无法确定type为double到底对应constructor-arg中的哪个,但是通过显示指定参数的索引能消除这种不确定性。
配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="tank" class="com.xgj.ioc.inject.construct.index.Tank">
<constructor-arg index="0" value="T72" />
<constructor-arg index="1" value="20000" />
<constructor-arg index="2" value="300" />
bean>
beans>
构造函数的一个参数索引为0,第二个为1,以此类推
测试类
package com.xgj.ioc.inject.construct.index;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConstructInjectTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/index/tank_index_match.xml");
Tank tank = ctx.getBean("tank", Tank.class);
tank.introduce();
}
}
运行结果:
有时候需要type和 index联合使用才能确定配置项和构造函数入参的对应关系。
POJO 对象
package com.xgj.ioc.inject.construct.joint;
public class Tank {
private String brand;
private double weight;
private double speed;
// 载人数量
private int manned;
/**
*
* @param brand
* @param weight
* @param speed
* 第二个参数和第三个参数同为double类型
*/
public Tank(String brand, double weight, double speed) {
this.brand = brand;
this.weight = weight;
this.speed = speed;
}
/**
*
* @param brand
* @param weight
* @param manned
*/
public Tank(String brand, double weight,int manned){
this.brand =brand;
this.weight = weight;
this.manned = manned;
}
public void introduce() {
System.out.println("Tank information: brand:" + brand + ",weight:"
+ weight + "KG,speed:" + speed + "km/h");
}
public void introduce2() {
System.out.println("Tank information: brand:" + brand + ",weight:"
+ weight + "KG,manned:" + manned + "/person");
}
}
配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="tank" class="com.xgj.ioc.inject.construct.joint.Tank">
<constructor-arg type="java.lang.String" index="0" value="T72" />
<constructor-arg type="double" index="1" value="25000" />
<constructor-arg type="int" index="2" value="3" />
bean>
beans>
如果仅仅根据index来配置,Spring无法知道第三个入参配置的类型究竟是int还是double ,因此需要明确指出第三个入参的类型以消除歧义。
事实上,constructor-arg中前两个的type属性可以去掉、
当然了,也可以直接用type来判断。
测试类
package com.xgj.ioc.inject.construct.joint;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConstructInjectTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/joint/tank_joint_match.xml");
Tank tank = ctx.getBean("tank", Tank.class);
tank.introduce2();
}
}
如果Bean构造函数入参的类型是可辩别的(非基础数据类型且入参类型各不相同),由于Java反射机制可以获取构造函数的入参类型,即使构造函数注入的配置不提供类型和索引的信息,Spring依然可以正确的完成构造函数的注入工作。
例子:
POJO类-Tank
package com.xgj.ioc.inject.construct.reflect;
public class Tank {
public void attack() {
System.out.println("tank begins to attack");
}
}
POJO类-Plane
package com.xgj.ioc.inject.construct.reflect;
public class Plane {
public void attack(){
System.out.println("plane begins to attack");
}
}
配置文件
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="plane" class="com.xgj.ioc.inject.construct.reflect.Plane"/>
<bean id="tank" class="com.xgj.ioc.inject.construct.reflect.Tank"/>
<bean id="commander" class="com.xgj.ioc.inject.construct.reflect.Commander">
<constructor-arg value="XGJ"/>
<constructor-arg ref="plane"/>
<constructor-arg ref="tank"/>
bean>
beans>
解析:
由于plane tank name入参的类型都是可辨别的,所以无需再构造函数注入的配置时指定constructor-arg的type和index,因此可以采用如上的简易配置方式
测试类
package com.xgj.ioc.inject.construct.reflect;
public class Commander {
private Plane plane;
private Tank tank;
private String name;
public Commander(Plane plane, Tank tank, String name) {
super();
this.plane = plane;
this.tank = tank;
this.name = name;
}
public void direct(){
System.out.println("Commamder name:" + name);
System.out.println("Commnader begins to direct the army");
plane.attack();
tank.attack();
}
}
package com.xgj.ioc.inject.construct.reflect;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConstructInjectTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/reflect/beans.xml");
Commander commander = ctx.getBean("commander", Commander.class);
commander.direct();
}
}
运行结果
Spring容器能够对构造函数配置的Bean进行实例化有一个前提:Bean构造函数入参所引用的对象必须已经准备就绪。
鉴于这个机制,如果两个Bean都采用构造函数注入,并且都通过构造函数入参引用对方,就会发生类属于线程死锁的的循环依赖问题。
举个例子说明一下(飞行员和飞机):
package com.xgj.ioc.inject.construct.loop;
public class Pilot {
private String pilotNname;
private Plane plane;
public Pilot(String pilotNname, Plane plane) {
super();
this.pilotNname = pilotNname;
this.plane = plane;
}
public void drivePlane() {
plane.fly();
}
}
package com.xgj.ioc.inject.construct.loop;
public class Plane {
private Pilot pilot;
private String planeBrand;
public Plane(Pilot pilot, String planeBrand) {
super();
this.pilot = pilot;
this.planeBrand = planeBrand;
}
public void fly() {
System.out.println("Plane :" + planeBrand + " is reday to fly");
}
}
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pilot" class="com.xgj.ioc.inject.construct.loop.Pilot">
<constructor-arg ref="plane"/>
<constructor-arg value="F35"/>
bean>
<bean id="plane" class="com.xgj.ioc.inject.construct.loop.Plane">
<constructor-arg ref="pilot"/>
<constructor-arg value="XGJ"/>
bean>
beans>
package com.xgj.ioc.inject.construct.loop;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class ConstructInjectTest {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:com/xgj/ioc/inject/construct/loop/beans.xml");
Pilot pilot = ctx.getBean("pilot", Pilot.class);
pilot.drivePlane();
}
}
运行结果:
Exception in thread "main" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'pilot' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'plane' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'plane' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'pilot' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pilot': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:359)
.......
.......
.......
org.springframework.context.support.ClassPathXmlApplicationContext.(ClassPathXmlApplicationContext.java:83)
at com.xgj.ioc.inject.construct.loop.ConstructInjectTest.main(ConstructInjectTest.java:10)
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'plane' defined in class path resource [com/xgj/ioc/inject/construct/loop/beans.xml]: Cannot resolve reference to bean 'pilot' while setting constructor argument; nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'pilot': Requested bean is currently in creation: Is there an unresolvable circular reference?
at .......
.......
... 29 more
如何解决呢? 构造函数注入修改为 属性注入即可。
如下所示:
只需要修改beans.xml即可
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="pilot" class="com.xgj.ioc.inject.construct.loopsolve.Pilot">
<property name="pilotNname" value="XGJ">property>
<property name="plane" ref="plane">property>
bean>
<bean id="plane" class="com.xgj.ioc.inject.construct.loopsolve.Plane">
<property name="planeBrand" value="F35">property>
<property name="pilot" ref="pilot">property>
bean>
beans>
运行结果
分为非静态工厂方法和静态工厂方法。
对于一个全新开发的应用来说,我们不推荐使用工厂方法的注入方式。因为工厂方法需要额外的类和代码,这些功能和业务并无关系。
仁者见仁 智者见智,并无定论。在合适的场景下使用合适的注入方式。