一、依赖注入
DI(Dependency Injection 依赖注入)等同于IOC控制反转。使用DI让有依赖关系的对象解耦,同时使得代码更加简洁。依赖注入有两种方式:基于构造方法注入 & 基于setter注入。
1. 基于构造方法注入:
基于构造方法注入时,容器调用带参构造方法,将每个参数作为依赖注入给对象。对象并不依赖容器的任何接口,基类和注解,仅仅作为一个普通的POJO
。带参静态工厂方法和构造方法注入同理,参数会被当作依赖注入。
构造参数注入顺序
构造方法注入时,如果参数没有歧义也不存在相互依赖,则注入顺序与参数定义顺序一致。使用方式及配置如下:
public class ExampleBean {
private AnotherBean beanOne;
private int i;
public ExampleBean(
AnotherBean anotherBean, int i) {
this.beanOne = anotherBean;
this.i = i;
}
}
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg>
<ref bean="anotherExampleBean"/>
</constructor-arg>
<constructor-arg type="int" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
构造器参数匹配的三种方式:
- 构造参数类型匹配:
当参数引用的是另一个bean,对于容器这个bean的类型是已知的,因此可以顺利匹配并注入。但如果参数是简单类型,如String
类型的,容器并没有我们想象的智能,这时就需要我们在配置中指定参数的类型。实际编码中IDE也会提示错误信息。

这种情况可以在XML的 <bean/>
定义中,指定参数类型如下:
<bean id="someBody" class="com.example.di.SomeBody">
<constructor-arg type="java.lang.String" value="wang"/>
<constructor-arg type="int" value="25"/>
</bean>
- 构造参数index匹配:
<bean id="someBody" class="com.example.di.SomeBody">
<constructor-arg index="0" value="wang"/>
<constructor-arg index="1" value="25"/>
</bean>
- 构造参数名称匹配:
<bean id="someBody" class="com.example.di.SomeBody">
<constructor-arg name="name" value="wang"/>
<constructor-arg name="age" value="25"/>
</bean>
静态工厂方法注入:
对于静态工厂方法注入和构造注入同理,使用方式及配置如下:
public class ExampleBean {
private ExampleBean(...) {
...
}
public static ExampleBean createInstance (
AnotherBean anotherBean, int i) {
ExampleBean eb = new ExampleBean (...);
return eb;
}
}
<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
<constructor-arg ref="anotherExampleBean"/>
<constructor-arg value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
2. 基于setter方法注入:
setter注入是指容器调用构造或工厂方法实例化bean之后,再通过调用setter
方法来注入依赖。使用方式及配置如下:
public class ExampleBean {
private AnotherBean beanOne;
private int i;
public void setBeanOne(AnotherBean beanOne) {
this.beanOne = beanOne;
}
public void setIntegerProperty(int i) {
this.i = i;
}
}
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne">
<ref bean="anotherExampleBean"/>
</property>
<property name="integerProperty" value="1"/>
</bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
3. 构造方法注入 VS setter注入:
实际中可以结合两种方式,使用构造方法注入强制依赖项,同时使用setter注入可选依赖项。对于强制依赖项的注入,也可以在setter
方法上使用@Required
注解,但仍推荐通过编程式构造方法来达到目的。
Spring推荐使用构造注入,优点有三:
- 使用构造注入保证组件不可变:当属性被
final
修饰时,可以通过构造方法注入,并能保证属性不变; - 使用构造注入保证依赖不为空:构造注入时如果依赖不存在,容器会报错;相较而言
setter
有可能注入空依赖,使用依赖时就需要判断其非空性。 - 使用构造注入保证依赖被完全初始化:向构造传参时要保证参数不为空,所以会先调用依赖的构造方法使其先完成实例化;
setter注入也不是没有优点,比如:
- setter注入可以对类重新注入重新配置;
- 当产生循环依赖时,可以使用setter注入代替构造器注入,避免强制注入循环依赖时产生
BeanCurrentlyInCreationException
异常。
二、使用depends-on
通常我们会通过将需要依赖的bean定义为属性,并通过XML中的ref
属性指定依赖的bean。但是如果两者没有强依赖关系,只是一个要先于另一个初始化,就可以通过depends-on
来指定创建的先后关系。同时先创建的后销毁。
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
三、懒加载beans
spring容器在加载时会检查配置问题,如引用不存在的bean、循环依赖等。默认情况下,容器会在启动时创建和配配置单例bean,这样能尽快发现配置错误。但是当不需要这种预实例化行为时,我们也可以通过将bean定义为懒加载模式使其在被使用时才创建。
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
但是当一个懒加载bean被单例bean依赖时,仍然会在容器启动时先创建依赖项,即使它是懒加载的。
四、自动装配
除了通过ref
手动设置bean依赖项,spring也允许使用autowire
来自动装配bean。好处在于不再需要显示指定依赖,并且自动装配可以随着bean的演变自动更新配置,无需手动修改配置。
1. 自动装配的四种模式
在基于XML的配置中,通过<bean/>
的autowire
属性来指定自动装配的模式。包括四种模式:
-
no
:不自动装配(默认值)。意味着必须手动通过ref
指定依赖; -
byName
:通过属性名自动装配。容器会去查找和属性名称相同的bean将其注入; -
byType
:通过属性类型自动装配。容器会去查找和属性类型相同的bean,如果有唯一可用的bean则注入,如果有多个会抛出异常,如果没有则不设置此属性; -
constructor
:和byType
模式同理,不过是针对构造器参数;
2. 自动装配的缺陷:
- 会被显示装配覆盖;无法注入简单类型属性;
- 不如显示装配精确;
- 自动装配时如果匹配了多个可依赖项,会导致异常;
如果一定要使用自动装配,可以通过以下方式避免多匹配:
给依赖项设置autowire-candidate属性为false,避免被匹配(仅对byType起作用);
给某依赖项设置primary为true,使其优先匹配;
五、方法注入
在依赖注入中假设一种场景:singleton
bean A
依赖 prototype
bean B
,并希望在每次调用A的方法时创建一个新的B实例。但是因为A是单例的所以只会被创建一次,因此只有一次机会设置B属性。
解决这种问题我们需要牺牲一部分控制反转特性。通过让A实现ApplicatiocContextAware
接口使得A可以感知容器,并通过getBean("B")
向容器请求新的B实例。但是这种方式使得代码可以感知容器并且于Spring框架耦合,因此并不是好的实现。
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
查找方法注入:
查找方法注入是解决以上问题的一种方式。通过CGLIB字节码动态生成CommandManager
的子类并重写createCommand
方法。
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
protected abstract Command createCommand();
}
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
</bean>
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
不需要实现ApplicationContextAware
接口,定义一个创建bean的方法,如果是抽象方法,动态生成的子类会实现此方法,如果是具体方法,则会被子类重写;被依赖的bean也需要指定为protoType
的,否则每次会返回同一个对象;如果使用注解代替XML配置,则需要在createCommand()
方法上使用@Lookup("myCommand")
。
网友评论