Spring 核心技术(5)
接上篇:Spring 核心技术(4)
version 5.1.8.RELEASE
1.4.5 自动装配协作者
Spring 容器可以自动连接协作 bean 之间的关系。你可以让 Spring 通过检查 ApplicationContext
中的内容自动为 bean 解析协作者(其他bean)。自动装配具有以下优点:
- 自动装配可以显着减少指定属性或构造函数参数的需要。(在本章其他地方讨论的其他机制,如bean模板 ,在这方面也很有价值。)
- 自动装配可以随着对象的演变更新配置。例如,如果需要向类添加依赖项,自动装配可以自动满足该依赖项,而无需修改配置。因此,自动装配在开发期间尤其有用,在代码库变得更稳定后也不会取消切换到显式装配的选项。
使用基于 XML 的配置元数据(请参阅依赖注入)时,可以使用 <bean/>
元素的 autowire
属性为 bean 定义指定装配模式。自动装配功能有四种模式。你指定每个 bean 的自动装配,因此可以选择要自动装配哪些。下表描述了四种自动装配模式:
模式 | 说明 |
---|---|
no | (默认)无自动装配。Bean 引用必须由 ref 元素定义。在进行较大部署更新时不建议更改默认设置,因为明确指定协作者可以提供更好的控制和清晰度。在某种程度上,它记录了系统的结构。 |
byName | 按属性名称自动装配。Spring 查找与需要自动装配的属性同名的 bean。例如,如果 bean 定义按名称设置为根据名称进行注入并且它包含一个 master 属性(即,它有一个 setMaster(..) 方法),则 Spring 会查找名为 master 的 bean 定义并使用它来设置属性。 |
byType | 如果容器中只存在一个该属性类型的 bean,则允许属性自动装配。如果存在多个,则抛出致命异常,这表示可能不能对该 bean 使用 byType 自动装配。如果没有匹配的 bean,则不会发生任何事情(该属性未设置)。 |
constructor | 类似于 `byType 但适用于构造函数参数。如果容器中没有构造函数参数类型的一个 bean,则会引发致命错误。 |
使用 byType
或 constructor
自动装配模式,可以装配数组和类型化的集合。在这种情况下,容器中所有与预期类型匹配的自动装配候选者都会被提供以满足依赖性。如果预期的键类型是 String
,则可以自动装配强类型 Map
实例。自动装配 Map
实例的值由与预期类型匹配的所有 bean 实例组成, Map
实例的键包含相应的 bean 名称。
自动装配的局限和缺点
当在整个项目中使用一致的自动装配时,自动装配效果最佳。如果一般不使用自动装配,那么开发人员使用它来装配一个或两个 bean 定义可能会让人感到困惑。
考虑自动装配的局限和缺点:
-
property
和constructor-arg
中设置的显式依赖项始终覆盖自动装配。不能自动装配简单属性,例如基本类型,Strings
,和Classes
(以及此类简单属性的数组)。这是设计中的限制。 - 自动装配不如显式装配精确。虽然如前面的表中所述,但 Spring 会谨慎地避免在带有歧义时进行猜测,这可能出现预料之外的结果。Spring 管理的对象之间的关系不再明确记录。
- 从 Spring 容器中获取文档的工具可能无法获取自动装配信息。
- 容器中的多个 bean 定义可以匹配 setter 方法或构造函数参数指定的类型以进行自动装配。对于数组,集合或
Map
实例,这不一定是个问题。但是,对于期望单个值的依赖关系,这种模糊性不是可以随意解决的。如果没有可用的唯一 bean 定义,则抛出异常。
在后一种情况下,您有几种选择:
- 放弃自动装配,使用持显式装配。
- 如下一节所述,通过将其
autowire-candidate
属性设置为false
,可以避免对bean定义进行自动装配。 - 通过将其
<bean/>
元素的primary
属性设置为true
,将单个 bean 定义指定为主要候选者。 - 基于注解的配置项实现更细粒度的控件,如基于注解的容器配置中所述。
从自动装配中排除 Bean
基于每个 bean,你可以从自动装配中排除 bean。在 Spring 的 XML 格式中,将 <bean/>
元素的 autowire-candidate
属性设置为 false
。容器使指定的 bean 定义对自动装配不可用(包括注解配置,例如 @Autowired
)。
autowire-candidate
属性旨在仅影响基于类型的自动装配。它不会影响根据名称的显式引用,即使指定的 bean 未标记为自动注解的候选人,也会解析它。因此,如果名称匹配,则按名称自动装配会注入 bean。
你还可以根据 bean 名称的表达式匹配来限制自动装配候选项。顶级的 <beans/>
元素在 default-autowire-candidates
属性中接受一个或多个表达式 。例如,要将自动装配候选状态限制为名称以 Repository
结尾的任何 bean,那么就提供值 *Repository
。需要提供多个表达式时,请在列表中定义并使用逗号分隔。bean 定义的 autowire-candidate
属性优先使用显示定义的值 true
或 false
。对于此类 bean,表达式匹配规则并不适用。
这些技术对于永远不希望通过自动装配注入其他 bean 的 bean 非常有用。这并不意味着排除的 bean 本身不能使用自动装配进行配置。相反,bean 本身不是其他 bean 自动装配的候选者。
1.4.6 方法注入
在大多数应用程序场景中,容器中的大多数 bean 都是单例。当单例 bean 需要与另一个单例 bean 协作或非单例 bean 需要与另一个非单例 bean 协作时,通常通过将一个 bean 定义为另一个 bean 的属性来处理依赖关系。当 bean 的生命周期不同时会出现问题。假设单例 bean A 需要使用非单例(原型)bean B,可能是在 A 上的每个方法调用上。容器只创建一次单例 bean A,因此只有一次机会来设置属性。容器不能在 bean A 每次需要时提供一个新的 bean B 的实例。
解决方案是放弃一部分控制反转。你可以通过实现 ApplicationContextAware
接口使 bean A 对容器可见,同时通过让容器调用 getBean("B")
在每次需要时请求 bean B 的实例。以下示例展示了此方法:
// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;
// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
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 Framework。方法注入是Spring IoC容器的一个高级功能,可以更加轻松地处理这种场景。
你可以在此博客中阅读有关方法注入成因的更多信息。
查找方法注入
查找方法注入是容器重写容器管理的 bean 中的方法并为容器中另外一个命名的 bean 返回查找结果的能力。查找通常涉及原型 bean,如上一节中描述的场景。Spring Framework 通过使用 CGLIB 库中的字节码生成来动态生成覆盖该方法的子类来实现此方法注入。
- 要使这个动态子类工作,Spring bean 容器子类不能设置为
final
,要重写的方法也不能设置为final
。- 对具有
abstract
方法的类进行单元测试需要你自己对类进行子类化并提供该abstract
方法的 stub 实现。- 组件扫描也需要具体的方法,这需要具体的类来获取。
- 另一个关键限制是查找方法不适用于工厂方法,特别是
@Bean
配置类中的方法,因为在这种情况下,容器不负责创建实例,因此无法凭空创建出运行时生成的子类。
对于前面代码片段中的 CommandManager
类,Spring 容器动态地覆盖 createCommand()
方法的实现。该 CommandManager
类没有任何 Spring 的依赖,因为重新写的示例如下所示:
package fiona.apple;
// no more Spring imports!
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
),要注入的方法需要以下形式的签名:
<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 部署为最初形态。如果它是单例,则每次返回相同的 myCommand
bean 实例。
或者,在基于注释的组件模型中,可以通过 @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:
public abstract class CommandManager {
public Object process(Object commandState) {
MyCommand command = createCommand();
command.setState(commandState);
return command.execute();
}
@Lookup
protected abstract MyCommand createCommand();
}
请注意,通常应该使用具体的 stub 实现来声明这种带注释的查找方法,以使它们与 Spring 的组件扫描规则兼容,默认情况下抽象类会被忽略。此限制不适用于显式注册或显式导入的 bean 类。
访问不同作用域的目标 bean 的另一种方法是
ObjectFactory
/Provider
注入点。请参阅使用 Scoped Beans 作为依赖关系。你可能还会发现
ServiceLocatorFactoryBean
(在org.springframework.beans.factory.config
包中)有用。
任意方法替换
与查找方法注入相比,一种不太有用的方法注入形式是能够使用另一个方法实现替换托管 bean 中的任意方法。你可以安全地跳过本节的其余部分,直到确实需要此功能。
使用基于 XML 的配置元数据时,可以使用 replaced-method
元素将已存在的方法实现替换为已部署的 bean。考虑以下类,它有一个我们想要覆盖的 computeValue
方法:
public class MyValueCalculator {
public String computeValue(String input) {
// some real code...
}
// some other methods...
}
实现 org.springframework.beans.factory.support.MethodReplacer
接口的类提供新的方法定义,如以下示例所示:
/**
* meant to be used to override the existing computeValue(String)
* implementation in MyValueCalculator
*/
public class ReplacementComputeValue implements MethodReplacer {
public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
// get the input value, work with it, and return a computed result
String input = (String) args[0];
...
return ...;
}
}
部署原始类并指定方法覆盖的 bean 定义类似于以下示例:
<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
<!-- arbitrary method replacement -->
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>String</arg-type>
</replaced-method>
</bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
你可以使用一个或多个 <replaced-method/>
元素的 <arg-type/>
元素来表明被覆盖的方法的方法签名。仅当方法重载且类中存在多个变体时,才需要参数的签名。为方便起见,参数的类型字符串可以是完全限定类型名称的子字符串。例如,以下所有均匹配 java.lang.String
:
java.lang.String
String
Str
因为参数的数量通常足以区分每个可能的选择,所以通过让您只键入与参数类型匹配的最短字符串,此快捷方式可以节省大量的输入。