自动注入和方法注入
在学习自动注入和方法注入之前我们先把之前的依赖注入的一些细节处理一下。
关于依赖注入和属性配置的细节
我们知道可以将 bean 属性和构造函数参数定义为对其他bean的引用。为此,Spring 基于 XML 的配置元数据在其<property/>
和<constructor-arg/>
元素中支持子元素类型。
直接传值(字符串)
<property/>
元素的value
属性将属性或构造函数参数指定为人类可读的字符串表示形式。 Spring会将这些值从String
转换为属性或参数的实际类型。以下示例显示了设置的各种值:
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<!-- results in a setDriverClassName(String) call -->
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
<property name="username" value="root"/>
<property name="password" value="masterkaoli"/>
</bean>
对其他 Bean 的引用
ref
元素(也是用在<constructor-arg/>
或<property/>
元素中的)可以将一个 bean 的指定属性的值设置为对容器管理的其他 bean的引用。引用的 bean 是用来设置其从属bean的属性的依赖关系的,并且在设置属性之前根据需要对其进行初始化。 (如果引用的bean是单例 bean,则它可能已经由容器初始化了)所有引用最终都是对另一个bean对象的引用。
也可以通过**<ref/>
标签**的bean
属性指定目标 bean,并且允许创建对同一容器或父容器中任何 bean 的引用,而不管它是否在同一 XML 文件中(因为容器是确定的)。 bean
属性的值可以与目标 Bean 的id
属性相同,也可以与其name
属性值相同。示例如下:
<ref bean="someBean"/>
通过**parent
属性**指定目标 bean 将创建对当前容器的父容器中的 bean 的引用。 parent
属性的值可以与目标 Bean 的id
属性或name
属性值相同。目标 Bean 必须位于当前容器的父容器中。主要在具有容器层次结构并且要使用与父 bean 名称相同的代理将现有 bean 封装在父容器中时,才会使用parent属性。以下示例显示了如何使用parent
属性:
<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
<!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" class="org.springframework.aop.framework.ProxyFactoryBean">
<!-- bean name is the same as the parent bean -->
<property name="target">
<ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
</property>
<!-- insert other configuration and dependencies as required here -->
</bean>
Inner Beans
<property/>
或<constructor-arg/>
元素内的<bean/>
元素定义了一个内部 bean,如以下示例所示:
<bean id="outer" class="...">
<!-- instead of using a reference to a target bean, simply define the target bean inline -->
<property name="target">
<bean class="com.example.Person"> <!-- this is the inner bean -->
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
</bean>
</property>
</bean>
内部 bean 定义不需要定义 ID 或名称。如果指定,则容器不使用该值作为标识符。容器在创建时也将忽略scope
标志,因为内部 Bean 始终是匿名的,并且始终与外部 Bean 一起创建。不可能独立地访问内部 bean 或将其注入到协作 bean 中而不是封装到封闭 bean 中。
作为一个特例,可以从自定义作用域中接收销毁回调,例如对于单例 bean 中包含的请求范围内的 bean。内部 bean 实例的创建与其包含的 bean 绑定在一起,但是销毁回调使它可以参与请求范围的生命周期。内部 bean 通常只共享其包含 bean 的作用域。
空字符串值和空字符串
Spring 将属性等的空参数视为空字符串,如下配置元数据片段将email
属性设置为空的String
值(“”)。
<bean class="ExampleBean">
<property name="email" value=""/>
</bean>
前面的示例等效于以下 Java 代码:
exampleBean.setEmail("");
<null/>
元素处理null
个值。以下清单显示了一个示例:
<bean class="ExampleBean">
<property name="email">
<null/>
</property>
</bean>
前面的配置等效于下面的 Java 代码:
exampleBean.setEmail(null);
复合属性
设置 bean 属性时,可以使用复合属性名称或嵌套属性名称,只要路径中除最终属性名称之外的所有组件都不是null
即可。考虑以下 bean 定义:
<bean id="something" class="things.ThingOne">
<property name="fred.bob.sammy" value="123" />
</bean>
something
bean 具有fred
属性,该属性具有bob
属性,该属性具有sammy
属性,并且最终的sammy
属性被设置为123
的值。为了使它起作用,在构造 bean 之后,something
的fred
属性和fred
的bob
属性一定不能为null
。否则,将抛出NullPointerException
。
使用依赖
如果一个 bean 是另一个 bean 的依赖,则通常意味着将一个 bean 设置为另一个 bean 的属性。通常使用基于 XML 的配置元数据完成此操作。但有时 bean 之间的依赖不太直接。比如何时需要初始化类中的静态初始值,例如用于数据库驱动程序注册。 depends-on
属性可以在初始化使用此bean之前显式初始化一个或多个 bean。以下示例使用depends-on
属性表示对单个 bean 的依赖关系:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
要表示对多个 bean 的依赖性,只需要添加逗号,空格和分号等有效的分隔符:
<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
<property name="manager" ref="manager" />
</bean>
<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />
depends-on
属性既可以指定依赖的初始化时间,也可以仅在singleton bean 的情况下指定依赖的相应销毁时间,销毁时先销毁定义depends-on
关系的从属 bean即这个属性的引用指向的bean,然后再销毁给定 bean 本身。
懒初始化 bean
默认情况下,作为初始化过程的一部分,ApplicationContext
实现会急于创建和配置所有singleton bean。通常,这种预初始化是可取的,因为与数小时甚至数天后相比,这会帮助我们立即发现配置中的错误。如果不希望预初始化,则可以通过将 bean 定义标记为延迟初始化来防止单例 bean 的预实例化。延迟初始化的 bean 告诉 IoC 容器在首次请求这个bean时进行初始化而不是在启动时创建一个 bean 实例。
在 XML 中,此行为由<bean/>
元素上的lazy-init
属性控制,如以下示例所示:
<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.something.AnotherBean"/>
当ApplicationContext
检测到前面的配置时,ApplicationContext
启动时就不会急于预先实例化lazy
的bean,而not.lazy
Bean 则会进行预先实例化。
但当延迟初始化的 bean 是未延迟初始化的单例 bean 的依赖时,ApplicationContext
将在启动时创建延迟初始化的 bean,因为它必须满足单例bean的依赖关系,这样才能保证延迟初始化的 bean 被注入到其他未延迟初始化的单例 bean 中。
还可以使用<beans/>
元素上的default-lazy-init
属性在容器级别控制延迟初始化,以下示例显示:
<beans default-lazy-init="true">
<!-- no beans will be pre-instantiated... -->
</beans>
但我们最常用的就是直接在类上加注解@Lazy进行懒加载了
自动注入
Spring 容器可以自动装配协作 bean 之间的关系。可以通过检查ApplicationContext
的内容来让 Spring 自动为bean 解决协作者(其他 bean)。自动装配具有以下优点:
- 自动装配可以大大减少指定属性或构造函数参数的需要。
- 随着对象的演变更新,自动装配可以更新配置即自动满足对象更新的依赖。例如,如果需要向类中添加一个依赖,则无需修改配置即可自动满足该依赖。
使用基于 XML 的配置元数据时,可以使用<bean/>
元素的autowire
属性为 bean 定义指定自动注入模型。自动注入功能具有四种模型。可以为每个 bean 指定自动装配,因此可以选择要自动装配的bean。下表描述了四种自动注入模型:
Mode | Explanation |
---|---|
no | (默认)无自动注入。 Bean 引用必须由ref 元素定义。对于大型部署,建议不要更改默认设置,因为明确指定bean的依赖可以提供更清晰的bean定义。在某种程度上,它记录了系统的结构。 |
byName | 按属性名称自动注入。 Spring 寻找与需要自动注入的属性同名的 bean。例如,如果一个 bean 定义被设置为按名称自动注入,并且包含一个master 属性(即它具有setMaster(..) 方法),那么 Spring 将查找一个名为master 的 bean 定义并使用它来设置属性。即必须有setter方法 |
byType | 如果容器中恰好存在一个该属性类型的 bean,则使该属性自动注入。如果存在多个符合要求的bean,则会引发异常,这时不应对这个bean 使用byType 自动装配。如果没有匹配的 bean,则什么也不会发生(未设置该属性)。也必须有setter方法 |
constructor | 类似于byType ,但适用于构造函数参数。如果容器中不存在构造函数参数类型的一个 bean,则将引发错误。 |
自动注入的局限性和缺点
考虑自动注入的局限性和缺点:
property
和constructor-arg
设置中的显式依赖始终会覆盖自动注入。并且我们不能注入基本数据类型,字符串,Class类型(这些数据的数组也不行)。而且这是Spring故意这样设计的- 自动注入不如手动注入精确。
- 容器中的多个bean定义可能会与自动注入的setter方法或构造函数参数指定的类型匹配。对于数组、集合或映射实例,这可能不会产生什么问题。但是,对于期望单个值的依赖项,我们无法随意确定到底有谁进行注入。如果没有唯一的bean定义可用,则会抛出异常
在后一种情况下,您有几种选择:
- 放弃自动注入而采用手动注入
- 将
autowire-candidate
属性设置为false
来避免自动注入 bean 定义。 - 通过基于注解的配置实现更细粒度的控件。
从自动注入中排除 Bean
每一个bean都可以从自动注入中排除一个 bean。如果使用 Spring 的 XML 格式,将<bean/>
元素的autowire-candidate
属性设置为false
即可。容器使特定的 bean 定义对于自动注入不可用(包括注解样式配置,例如@Autowired。
autowire-candidate
属性只会影响基于类型的自动注入。它不会影响按名称的自动注入。因此,如果名称匹配,按名称自动注入仍会注入 Bean。即autowire-candidate=false
这个属性代表的是,这个bean不作为候选bean注入到别的bean中,而不是说这个bean不能接受别的bean的注入。
还可以基于与 Bean 名称的模式匹配来限制自动注入候选。顶级<beans/>
元素在其default-autowire-candidates
属性内接受一个或多个模式。比如要将自动注入候选状态限制为名称以Repository
结尾的任何 bean,则需要提供值*Repository
。要提供多种模式,就需要在以逗号分隔的列表中定义它们。 Bean 定义的autowire-candidate
属性的true
或false
的优先级更高。对于此类 bean,模式匹配规则不适用。
以上内容对于不希望通过自动注入将该bean注入其他 bean非常有用,但这并不意味着排除的 bean 本身不能使用自动注入进行配置。相反,bean 本身不是自动注入其他 bean 的候选对象。
总结如图:
- 从关注的点上来看,自动注入是针对的整个对象,或者一整批对象。比如我们如果将
autoService
这个bean的注入模型设置为byName
,Spring会为我们去寻找所有符合要求的名字(通过set方法)bean并注入到autoService
中。而精确注入这种方式,是我们针对对象中的某个属性,比如我们在autoService
中的dmzService
这个属性字段上添加了@AutoWired
注解,代表我们要精确的注入dmzService
这个属性。而方法注入主要是基于一个方法对对象进行注入。 - 我们通常所说byName,byType**跟我们在前文提到的注入模型中的
byName
,byType
是完全不一样的。通常我们说的byName,byType是Spring寻找bean的手段。比如,当我们注入模型为constructor
时,Spring会先通过名称找对符合要求的bean,这种通过名称寻找对应的bean的方式我们可以称为byName
。我们可以将一次注入分为两个阶段,首先是寻找符合要求的bean,其次再是将符合要求的bean注入。也可以画图如下:
方法注入
在大多数应用场景中,容器中的大多数 bean 是singletons。当单例 Bean 需要与另一个单例 Bean 协作或非单例 Bean 需要与另一个非单例 Bean 协作时,通常可以通过将一个 Bean 定义为另一个 Bean 的属性来处理依赖关系。但当 bean 的生命周期不同时会出现问题。假设单例 bean A 需要使用非单例(原型)bean B,也许在 A 的每个方法调用上都使用。容器仅创建一次单例 bean A,因此只有一次机会来设置属性。每次需要容器提供一个beanB时,容器都无法为 bean A 提供一个新的 bean B 实例。比如下面这个例子:
@Component
public class MyService {
@Autowired
private LuBanService luBanService;
public void test(int a){
luBanService.addAndPrint(a);
}
}
@Component
// 原型对象
@Scope("prototype")
public class LuBanService {
int i;
LuBanService() {
System.out.println("luBan create ");
}
// 每次将当前对象的属性i+a然后打印
public void addAndPrint(int a) {
i+=a;
System.out.println(i);
}
}
public class Main02 {
public static void main(String[] args) {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Config.class);
MyService service = (MyService) ac.getBean("myService");
service.test(1);
service.test(2);
service.test(3);
}
}
在上面的代码中,我们有两个Bean,MyService为单例的Bean,LuBanService为原型的Bean。我们的本意可能是希望每次都能获取到不同的LuBanService,预期的结果应该打印出:
1,2,3
实际输出:
1,3,6
这个结果说明我们每次调用到的LuBanService是同一个对象。当然,这也很好理解,因为在依赖注入阶段我们就完成了LuBanService的注入,之后我们在调用测试方法时,不会再去进行注入,所以我们一直使用的是同一个对象。
我们可以这么说,原型对象在这种情况下,失去了原型的意义,因为每次都使用的是同一个对象。那么如何解决这个问题呢?只要我每次在使用这个Bean的时候都去重新获取就可以了,那么这个时候我们可以通过方法注入来解决。
方法注入的使用
方法注入(LookUp Method跟Replace Method)需要依赖动态代理完成,方法注入是Spring提供给我们用来处理Bean之间协作关系的手段。
通过注入上下文(applicationContext对象)
又分为以下两种方式:
- 实现
org.springframework.context.ApplicationContextAware
接口
@Component
public class MyService implements ApplicationContextAware {
private ApplicationContext applicationContext;
public void test(int a) {
LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
luBanService.addAndPrint(a);
}
@Override
public void setApplicationContext(@Nullable ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
- 直接注入上下文
@Component
public class MyService{
@Autowired
private ApplicationContext applicationContext;
public void test(int a) {
LuBanService luBanService = ((LuBanService) applicationContext.getBean("luBanService"));
luBanService.addAndPrint(a);
}
}
Lookup方法注入
Lookup方法注入是容器重写管理的 Bean 上的方法并返回容器中另一个新对象的能力。Lookup通常涉及原型 bean。Spring 框架通过使用从 CGLIB 库生成字节码来动态生成覆盖该方法的子类来实现此方法注入。
- 为了使此动态代理子类起作用,Spring bean 容器子类的类也不能为
final
,并且要覆盖的方法也不能为final
。 - 对具有
abstract
方法的类进行单元测试需要我们对该类进行子类化,并提供abstract
方法的实现。 - 组件扫描也需要具体方法,这需要具体的类别。
- 另一个关键限制是,Lookup方法不适用于工厂方法,尤其不适用于配置类中的
@Bean
方法,因为在这种情况下,容器不负责创建实例,因此无法创建运行时生成的子类在容器中。
对于前面的代码片段中的CommandManager
类,代码如下:
public class CommandManager implements ApplicationContextAware {
private ApplicationContext applicationContext;
public Object process(Map commandState) {
// grab a new instance of the appropriate Command
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
protected Command createCommand() {
// notice the Spring API dependency!
return this.applicationContext.getBean("command", Command.class);
}
public void setApplicationContext(
ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
Spring 容器将动态覆盖createCommand()
方法的实现。 CommandManager
类没有任何 Spring 依赖项,如重做的示例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
// grab a new instance of the appropriate Command interface
Command command = createCommand();
// set the state on the (hopefully brand new) Command instance
command.setState(commandState);
return command.execute();
}
// okay... but where is the implementation of this method?
protected abstract Command createCommand();
}
在包含要注入的方法(在本例中为CommandManager
)的 Client 端类中,要注入的方法需要以下形式的签名:
<public|protected> [abstract] <return-type> theMethodName(no-arguments);
如果方法是abstract
,则动态生成的子类将实现该方法。否则,动态生成的子类将覆盖原始类中定义的具体方法。考虑以下示例:
<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
<!-- inject dependencies here as required -->
</bean>
<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="myCommand"/>
</bean>
每当需要myCommand
bean 的新实例时,标识为commandManager
的 bean 就会调用其自己的createCommand()
方法。如果确实需要myCommand
bean 作为原型,则必须小心。如果它是singleton,则每次都返回myCommand
bean 的相同实例。
另外,在基于注解的组件模型中,可以通过@Lookup
注解声明一个Lookup方法,如以下示例所示:
public abstract class CommandManager {
public Object process(Object commandState) {
Command command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup("myCommand")
protected abstract Command createCommand();
}
或者可以依赖于目标 bean 根据Lookup方法的声明的返回类型来解析:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
请注意,通常应使用具体的实现声明此类带注解的Lookup方法,以使它们与 Spring 的组件扫描规则兼容,在默认情况下抽象类将被忽略。此限制不适用于显式注册或显式导入的 Bean 类。
在我认为方法注入是依赖注入的特例,尤其是setter方法的注入,它们都是操控bean的工具,我们可以在setter注入之外额外使用一个方法对属性进行注入,好处是它可以调用很多次,而不像setter方法一样只在实例初始化时调用一次。这可能就是方法注入可以获取新实例的原因吧。
感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。