Spring 组件还可以向容器提供 bean 定义元数据。您可以使用与在 @Configuration 注解类中定义 bean 元数据相同的 @Bean 注解来实现这一点。下面的例子说明了如何这样做:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// 省略了组件方法实现
}
}
前面的类是一个 Spring 组件,在它的 doWork()
方法里有特定的应用代码。然而,它也贡献了一个Bean 定义,它有一个工厂方法,引用了 publicInstance()
方法。@Bean 注解标识了工厂方法和其他 Bean 定义属性,例如通过 @Qualifier
注解标识了限定值。其他可以指定的方法级注解有 @Scope
、@Lazy
和自定义限定符注解。
:::tips
除了在组件初始化中的作用外,你还可以将 @Lazy 注解放在标有 @Autowired 或 @Inject 的注入点上。在这种情况下,它导致了一个懒加载解决的代理的注入。然而,这种代理方法是相当有限的。对于复杂的懒加载交互,特别是与可选的依赖关系相结合,我们推荐使用 ObjectProvider<MyTargetBean>
来代替。
:::
正如前面讨论的,自动装配字段和方法支持 @Bean
方法的自动装配。下面的例子说明了如何这样做:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// 使用自定义限定符和方法参数
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
这个例子的 protectedInstance 方法上的 country 参数, 自动装配到另一个名为 privateInstance 的 bean 上的 age 属性值(简单说就是注入了 privateInstance() 方法产生的对象中的 age 属性的值)。一个 Spring 表达式语言元素通过符号 #{ <表达式> }
定义了该属性的值。对于 @Value
注解,表达式解析器被预设为在解析表达式文本时寻找 Bean 名称。
从 Spring Framework 4.3 开始,你也可以声明一个 InjectionPoint(或其更具体的子类:DependencyDescriptor)类型的工厂方法参数来访问触发创建当前 Bean 的请求注入点。请注意,这只适用于 Bean 实例的实际创建,而不适用于现有实例的注入。因此,这个功能对多例范围的 Bean 最有意义。对于其它作用域,工厂方法只看到在给定作用域中触发创建新 bean 实例的注入点(例如,触发创建 lazy singleton bean 的依赖关系)。在这种情况下,你可以在语义上注意使用所提供的注入点元数据。下面的例子显示了如何使用 InjectionPoint。
@Component
public class FactoryMethodComponent {
@Bean @Scope("prototype")
public TestBean prototypeInstance(InjectionPoint injectionPoint) {
return new TestBean("prototypeInstance for " + injectionPoint.getMember());
}
}
// 这个例子笔者没有运行成功,应该还缺少什么上下文,注入不进来 InjectionPoint,不知道是否需要自己处理 InjectionPoint ?
普通 Spring 组件中的 @Bean 方法与 Spring @Configuration 类中的对应方法的处理方式不同。区别在于,@component 类没有用 CGLIB 来拦截方法和字段的调用。CGLIB 代理是调用 @Configuration 类中@Bean 方法中的方法或字段的方式,它创建了对协作对象的 Bean 元数据引用。这样的方法不是用正常的 Java 语义来调用的,而是通过容器,以便提供 Spring Bean 通常的生命周期管理和代理,即使是在通过编程调用 @Bean 方法来引用其他 Bean 的时候。相比之下,在普通的 @Component 类中调用 @Bean 方法中的方法或字段具有标准的 Java 语义,没有特殊的 CGLIB 处理或其他约束条件适用。
:::tips
上面的描述笔者不是很明白,但是有一个描述或许可以解释在 @Configuration 中的直接调用 @Bean 的方法,返回的居然是从容器中获取的 bean 实例。比如
@Configuration
publlic class Demo{
@Bean
publ DataSource dataSource(){
return new DataSource();
}
@Bean
public XXX xxx(){
this.dataSource() // 这里调用,返回的不是这里表面看到的直接 new 了一个,而是从容器中获取的实例
}
}
就是上面描述的,在 @Configuration 中的 @Bean 方法是被 CGLIB 代理了的
:::
:::info 你可以将 @Bean 方法声明为静态的,这样就可以在不创建其包含的配置类实例的情况下调用它们。在定义 post-processor Bean(例如,BeanFactoryPostProcessor 或 BeanPostProcessor 类型)时,这一点特别有意义,因为这种 bean 在容器生命周期的早期被初始化,并且应该避免在这一点上触发配置的其他部分。
由于技术上的限制,对静态 @Bean 方法的调用永远不会被容器拦截,甚至在 @Configuration 类中也不会(如本节前面所述)。CGLIB 子类只能覆盖非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的 Java 语义,结果是直接从工厂方法本身返回一个独立实例。
@Bean 方法的 Java 语言可见性对 Spring 容器中产生的 Bean 定义没有直接影响。你可以在非@Configuration 类中自由地声明你的工厂方法,也可以在任何地方为静态方法声明。然而,@Configuration 类中的常规 @Bean 方法需要是可重写的—也就是说,它们不能被声明为 pricate 或则 final。
@Bean 方法也可以在特定组件或配置类的基类上发现,也可以在组件或配置类实现的接口中声明的Java 8 默认方法上发现。这使得组成复杂的配置安排有了很大的灵活性,从 Spring 4.2 开始,甚至可以通过 Java 8 默认方法进行多重继承。
最后,一个类可以为同一个 Bean 持有多个 @Bean 方法,作为对多个工厂方法的安排,根据运行时的可用依赖关系来使用。这与在其他配置情况下选择 “最环保 “的构造函数或工厂方法的算法相同。在构造时选择具有最大数量的可满足的依赖关系的变量,类似于容器在多个 @Autowired 构造函数之间进行选择。 :::