使用 @Configuration 注解 (Using the @Configuration annotation)
@Configuration 是一个类级注解,指示一个对象是 Bean 定义的来源。@Configuration 类通过标注了 @Bean 的方法来声明 Bean。对 @Configuration 类上 @Bean 方法的调用也可以用于定义 Bean 间的依赖关系。有关一般介绍,请参阅核心概念:@Bean 和 @Configuration。
注入 Bean 间的依赖关系 (Injecting Inter-bean Dependencies)
当 Bean 之间存在依赖关系时,表达这种依赖非常简单,只需一个 Bean 方法调用另一个即可,如下例所示:
@Configuration
public class AppConfig {
@Bean
public BeanOne beanOne() {
return new BeanOne(beanTwo());
}
@Bean
public BeanTwo beanTwo() {
return new BeanTwo();
}
}@Configuration
class AppConfig {
@Bean
fun beanOne() = BeanOne(beanTwo())
@Bean
fun beanTwo() = BeanTwo()
}在上面的示例中,beanOne 通过构造器注入接收到了对 beanTwo 的引用。
注意
这种声明 Bean 间依赖关系的方法仅在 @Bean 方法声明在 @Configuration 类内部时有效。你不能使用普通的 @Component 类来声明 Bean 间的依赖关系。
方法查找注入 (Lookup Method Injection)
如前所述,方法查找注入是一项你应该很少使用的高级特性。它在单例作用域的 Bean 依赖于原型作用域(prototype-scoped)的 Bean 的情况下非常有用。在此类配置中使用 Java 为实现此模式提供了自然的手段。以下示例显示了如何使用方法查找注入:
public abstract class CommandManager {
public Object process(Object commandState) {
// 获取合适的 Command 接口的新实例
Command command = createCommand();
// 在(希望是全新的)Command 实例上设置状态
command.setState(commandState);
return command.execute();
}
// 好的... 但是这个方法的实现在哪里?
protected abstract Command createCommand();
}abstract class CommandManager {
fun process(commandState: Any): Any {
// 获取合适的 Command 接口的新实例
val command = createCommand()
// 在(希望是全新的)Command 实例上设置状态
command.setState(commandState)
return command.execute()
}
// 好的... 但是这个方法的实现在哪里?
protected abstract fun createCommand(): Command
}通过使用 Java 配置,你可以创建 CommandManager 的子类,其中抽象的 createCommand() 方法被重写,从而查找一个新的(原型)Command 对象。以下示例展示了如何实现:
@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
AsyncCommand command = new AsyncCommand();
// 根据需要在此注入依赖项
return command;
}
@Bean
public CommandManager commandManager() {
// 返回 CommandManager 的新匿名实现,
// 重写 createCommand() 以返回一个新的原型对象
return new CommandManager() {
protected Command createCommand() {
return asyncCommand();
}
};
}@Bean
@Scope("prototype")
fun asyncCommand(): AsyncCommand {
val command = AsyncCommand()
// 根据需要在此注入依赖项
return command
}
@Bean
fun commandManager(): CommandManager {
// 返回 CommandManager 的新匿名实现,
// 重写 createCommand() 以返回一个新的原型对象
return object : CommandManager() {
override fun createCommand(): Command {
return asyncCommand()
}
}
}关于基于 Java 的配置在内部如何工作的详细信息
考虑以下示例,它显示了一个标注了 @Bean 的方法被调用了两次:
@Configuration
public class AppConfig {
@Bean
public ClientService clientService1() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientService clientService2() {
ClientServiceImpl clientService = new ClientServiceImpl();
clientService.setClientDao(clientDao());
return clientService;
}
@Bean
public ClientDao clientDao() {
return new ClientDaoImpl();
}
}@Configuration
class AppConfig {
@Bean
fun clientService1(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientService2(): ClientService {
return ClientServiceImpl().apply {
clientDao = clientDao()
}
}
@Bean
fun clientDao(): ClientDao {
return ClientDaoImpl()
}
}clientDao() 在 clientService1() 中被调用了一次,在 clientService2() 中也被调用了一次。由于此方法创建了 ClientDaoImpl 的新实例并返回它,你通常会期望有两个实例(每个服务一个)。这肯定是有问题的:在 Spring 中,实例化的 Bean 默认具有 singleton 作用域。这就是神奇之处:所有 @Configuration 类在启动时都会通过 CGLIB 生成子类。在子类中,子方法在调用父方法并创建新实例之前,会先检查容器中是否有任何缓存的(作用域内的)Bean。
注意
根据 Bean 的作用域不同,行为可能会有所不同。我们这里讨论的是单例(singleton)。
注意
没有必要将 CGLIB 添加到你的类路径中,因为 CGLIB 类已重新打包在 org.springframework.cglib 包下,并直接包含在 spring-core JAR 中。
提示
由于 CGLIB 在启动时动态添加特性,因此存在一些限制。特别是,配置类不能是 final 的。但是,配置类上允许使用任何构造函数,包括使用 @Autowired 或为默认注入声明单个非默认构造函数。
如果你希望避免任何由 CGLIB 强加的限制,请考虑在非 @Configuration 类上(例如,在普通的 @Component 类上)声明你的 @Bean 方法,或者通过使用 @Configuration(proxyBeanMethods = false) 标注你的配置类。此时,@Bean 方法之间的跨方法调用不会被拦截,因此你必须完全依赖构造器或方法级别的依赖注入。
补充教学 —— 解密 @Configuration 的“自调用”魔法
在普通的 Java 开发中,如果你调用一个方法两次,方法体内部的代码肯定会执行两次。但在 Spring 的 @Configuration 类中,这个常识被打破了。
1. 为什么 beanA() 调用 beanB() 拿到的永远是同一个对象? 这一切都归功于 CGLIB 代理。当 Spring 发现一个类标注了 @Configuration 时,它会动态生成该类的一个子类。
- 当你调用
beanA()时,实际上是在调用代理对象的beanA()。 - 代理对象会先去容器里找:“名为
beanA的单例对象创建好了吗?” - 如果已经创建,直接返回;如果没创建,才去执行你写的原始方法代码。 这就是为什么
service1和service2内部即便都写了dao(),注入的也是同一个单例的原因。
2. @Configuration vs @Component 内的 @Bean 这是面试常考点,也叫 Full 模式 和 Lite 模式。
- Full 模式 (
@Configuration):有 CGLIB 代理。方法间相互调用会被拦截,保证单例语义。 - Lite 模式 (
@Component或普通类):没有 CGLIB 代理。方法间调用就是纯粹的 Java 方法调用,会产生不同的对象。 避坑指南:如果你在@Component类里写了两个@Bean方法,且一方调用了另一方,你将得到两个不同的实例,这通常会导致难以察觉的 Bug!
3. 何时开启 proxyBeanMethods = false? 在现代 Spring Boot 自动配置中,你经常能看到这个选项。
- 场景:如果你的配置类极其简单,方法之间没有互相依赖,或者你已经习惯了使用方法参数来传递依赖(推崇这种做法),那么可以设为
false。 - 性能:设为
false后,Spring 就不再需要为你的类生成代理对象,启动速度会有细微的提升。
4. 方法查找注入(Lookup)的优雅替代 虽然文中展示了用匿名类重写 abstract 方法的方式来实现 Lookup,但在 Java 8+ 的时代,我们有了更简洁的办法:
@Bean
public CommandManager commandManager() {
return new CommandManager() {
@Override
protected Command createCommand() {
// 如果是在 Full 模式配置类下,这种自调用依然是受控的
return asyncCommand();
}
};
}或者,更现代的做法是直接使用 ObjectProvider<T>:
@Bean
public CommandManager commandManager(ObjectProvider<AsyncCommand> provider) {
return new CommandManager() {
@Override
protected Command createCommand() {
return provider.getObject(); // 每次调用 getObject() 都会从容器请求新实例
}
};
}总结:@Configuration 的核心意义不仅在于定义 Bean,更在于通过 CGLIB 代理维护了配置代码中的单例语义。