Skip to content

使用 @Configuration 注解 (Using the @Configuration annotation)

@Configuration 是一个类级注解,指示一个对象是 Bean 定义的来源。@Configuration 类通过标注了 @Bean 的方法来声明 Bean。对 @Configuration 类上 @Bean 方法的调用也可以用于定义 Bean 间的依赖关系。有关一般介绍,请参阅核心概念:@Bean 和 @Configuration

注入 Bean 间的依赖关系 (Injecting Inter-bean Dependencies)

当 Bean 之间存在依赖关系时,表达这种依赖非常简单,只需一个 Bean 方法调用另一个即可,如下例所示:

java
@Configuration
public class AppConfig {

	@Bean
	public BeanOne beanOne() {
		return new BeanOne(beanTwo());
	}

	@Bean
	public BeanTwo beanTwo() {
		return new BeanTwo();
	}
}
kotlin
@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 为实现此模式提供了自然的手段。以下示例显示了如何使用方法查找注入:

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();
}
kotlin
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 对象。以下示例展示了如何实现:

java
@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();
		}
	};
}
kotlin
@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 的方法被调用了两次:

java
@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();
	}
}
kotlin
@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 的单例对象创建好了吗?”
  • 如果已经创建,直接返回;如果没创建,才去执行你写的原始方法代码。 这就是为什么 service1service2 内部即便都写了 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+ 的时代,我们有了更简洁的办法:

java
@Bean
public CommandManager commandManager() {
    return new CommandManager() {
        @Override
        protected Command createCommand() {
            // 如果是在 Full 模式配置类下,这种自调用依然是受控的
            return asyncCommand(); 
        }
    };
}

或者,更现代的做法是直接使用 ObjectProvider<T>

java
@Bean
public CommandManager commandManager(ObjectProvider<AsyncCommand> provider) {
    return new CommandManager() {
        @Override
        protected Command createCommand() {
            return provider.getObject(); // 每次调用 getObject() 都会从容器请求新实例
        }
    };
}

总结@Configuration 的核心意义不仅在于定义 Bean,更在于通过 CGLIB 代理维护了配置代码中的单例语义

Based on Spring Framework.