@Configuration注解 和 没有 @Configuration注解 ,对@bean的作用?
这是一个非常重要的 Spring 核心概念。让我详细解释 @Configuration 注解对 @Bean 方法的影响:
核心区别
1. 有 @Configuration 注解(完整模式)
JAVA
@Configuration
public class AppConfig {
@Bean
public A beanA() {
return new A();
}
@Bean
public B beanB() {
// 这里调用的是 Spring 代理的方法,返回的是单例 Bean
// 不会创建新的 A 实例,而是返回 beanA() 的 Bean
return new B(beanA()); // 这是单例调用
}
}
关键特性:
- Spring 使用 CGLIB 代理 包装配置类
- 保证
@Bean方法的单例行为 - 方法调用被代理,返回容器中已存在的 Bean
- 支持
@Bean方法之间的依赖注入
2. 没有 @Configuration 注解(Lite 模式)
JAVA
// 注意:没有 @Configuration
@Component
public class LiteConfig {
@Bean
public A beanA() {
return new A();
}
@Bean
public B beanB() {
// 这里调用的是普通 Java 方法
// 每次都会创建新的 A 实例!
return new B(beanA()); // 这是普通方法调用
}
}
关键特性:
- 没有 CGLIB 代理
- 每个
@Bean方法是独立的 - 方法调用是普通 Java 调用,不会返回单例
- 每次调用都会创建新实例
实际演示
示例 1:验证单例行为
JAVA
@SpringBootTest
class ConfigurationTest {
@Test
void testConfigurationProxy() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(ConfigDemo.class);
context.refresh();
ConfigDemo config = context.getBean(ConfigDemo.class);
System.out.println("Config 类型: " + config.getClass());
// 输出: Config 类型: class com.example.ConfigDemo$$EnhancerBySpringCGLIB$$xxxx
// 注意 CGLIB 代理!
}
}
@Configuration
class ConfigDemo {
@Bean
public String str1() {
return "hello";
}
@Bean
public String str2() {
return str1() + " world"; // 这里调用的是代理方法
}
}
示例 2:Lite 模式的问题
JAVA
// 错误的写法 - Lite 模式
@Component
class ProblematicConfig {
private int counter = 0;
@Bean
public ExpensiveService service1() {
counter++;
System.out.println("创建 service1, 计数器: " + counter);
return new ExpensiveService();
}
@Bean
public Client client1() {
// 问题:每次都会创建新的 ExpensiveService!
return new Client(service1(), service1());
// 输出:
// 创建 service1, 计数器: 1
// 创建 service1, 计数器: 2
// Client 中的两个 service1 是不同的实例!
}
}
Spring Cloud OpenFeign 的特殊情况
在你的 FeignClient 场景中:
JAVA
// 即使没有 @Configuration,Feign 也会处理
public class ClientConfig {
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient(); // 会被注册为 Bean
}
}
@FeignClient(name = "service1", configuration = ClientConfig.class)
interface Service1Client {}
关键点:
- 即使没有
@Configuration,@FeignClient(configuration = ...)引用的类也会被 Spring 处理 - 但处理方式与有
@Configuration不同:- 没有代理
- 每个
@Bean方法独立
- 在 FeignContext 中,每个 FeignClient 都会:
- 创建新的
ClientConfig实例 - 执行所有
@Bean方法 - 注册 Bean 到子上下文
- 创建新的
最佳实践建议
场景 1:普通配置类
JAVA
// 总是添加 @Configuration
@Configuration
public class AppConfig {
@Bean
public DataSource dataSource() {
// 这个方法会被代理
return DataSourceBuilder.create().build();
}
}
场景 2:FeignClient 特定配置
JAVA
// 如果有 @Bean 方法间的调用,需要 @Configuration
@Configuration
public class FeignSpecificConfig {
@Bean
public Encoder customEncoder(JacksonEncoder jacksonEncoder) {
// 通过参数注入,而不是方法调用
return new CustomEncoder(jacksonEncoder);
}
}
场景 3:共享配置(最佳实践)
JAVA
// 共享配置必须有 @Configuration
@Configuration
public class SharedFeignConfig {
@Bean
@Primary
public ConnectionPool sharedConnectionPool() {
return new ConnectionPool(200, 5, TimeUnit.MINUTES);
}
@Bean
public OkHttpClient okHttpClient(ConnectionPool pool) {
// 通过参数注入共享的 ConnectionPool
return new OkHttpClient.Builder()
.connectionPool(pool)
.build();
}
}
检测和验证
JAVA
@SpringBootTest
class ConfigModeTest {
@Autowired
private ApplicationContext context;
@Test
void testConfigMode() {
// 1. 检查是否是代理
Object configBean = context.getBean("clientConfig");
System.out.println("是否是 CGLIB 代理: " +
configBean.getClass().getName().contains("$$EnhancerBySpringCGLIB$$"));
// 2. 检查单例
OkHttpClient client1 = context.getBean(OkHttpClient.class);
OkHttpClient client2 = context.getBean(OkHttpClient.class);
System.out.println("是否是同一个实例: " + (client1 == client2));
// 3. 统计 Bean 数量
Map<String, OkHttpClient> clients = context.getBeansOfType(OkHttpClient.class);
System.out.println("OkHttpClient Bean 数量: " + clients.size());
}
}
总结表格
| 特性 | 有 @Configuration |
无 @Configuration |
|---|---|---|
| 代理 | CGLIB 代理 | 无代理 |
| 方法调用 | 代理方法,返回单例 | 普通方法,创建新实例 |
| 性能 | 首次创建慢(代理开销) | 创建快 |
| 内存 | 单例,内存友好 | 可能多实例,内存浪费 |
| 适用场景 | 复杂配置,Bean 间依赖 | 简单配置,独立 Bean |
| FeignClient 中 | 每个子上下文独立实例 | 每个子上下文独立实例 |
| 你的场景 | 313 个独立实例 | 313 个独立实例 + 方法调用问题 |
核心结论:
- 在 FeignClient 配置中,无论是否有
@Configuration,每个 FeignClient 都会创建独立的实例 - 要解决你的问题,必须将共享 Bean 提升到父上下文,而不是纠结于是否有
@Configuration
@Configuration 里的@bean注解 会被自动创建到容器里吗?是单利吗?
简短回答
是的,@Configuration 里的 @Bean 注解方法会被自动创建到容器中,并且默认是单例(Singleton)。
让我详细解释:
1. 自动创建到容器
只要是 Spring 容器管理的类中的 @Bean 方法,都会被注册到容器中,无论是否有 @Configuration
JAVA
// 情况1:有 @Configuration
@Configuration
public class ConfigA {
@Bean
public MyService myService() {
return new MyService(); // 会被注册到容器
}
}
// 情况2:没有 @Configuration,但有 @Component
@Component
public class ConfigB {
@Bean
public MyService myService() {
return new MyService(); // 也会被注册到容器
}
}
// 情况3:被 @Import 或扫描到
@Import(ConfigC.class)
@Configuration
class MainConfig {}
public class ConfigC { // 没有注解
@Bean
public MyService myService() {
return new MyService(); // 也会被注册到容器
}
}
2. 单例行为
默认情况下,所有 @Bean 方法创建的实例都是单例
JAVA
@SpringBootTest
class BeanSingletonTest {
@Test
void testSingletonBehavior() {
AnnotationConfigApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class);
MyService bean1 = context.getBean(MyService.class);
MyService bean2 = context.getBean(MyService.class);
System.out.println(bean1 == bean2); // 输出:true
// 是同一个实例
}
}
@Configuration
class AppConfig {
@Bean
public MyService myService() {
return new MyService();
}
}
3. 关键区别:有无 @Configuration 的影响
虽然都会注册到容器,但代理行为不同:
有 @Configuration(完整模式)
JAVA
@Configuration
public class FullConfig {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
// 这里调用的是 Spring 的代理方法
// 返回容器中已存在的 A 实例(单例)
return new B(a()); // ✅ 正确:使用同一个 A 实例
}
}
无 @Configuration(Lite 模式)
JAVA
@Component
public class LiteConfig {
@Bean
public A a() {
return new A();
}
@Bean
public B b() {
// 这里调用的是普通 Java 方法
// 每次都会创建新的 A 实例
return new B(a()); // ❌ 错误:创建新的 A 实例
}
@Bean
public C c(A a) { // 通过参数注入才是安全的
return new C(a); // ✅ 正确:使用容器中的 A
}
}
4. 你的 FeignClient 场景分析
JAVA
// ClientConfig 被 313 个 FeignClient 引用
public class ClientConfig { // 没有 @Configuration
@Bean
public OkHttpClient okHttpClient() {
return new OkHttpClient(); // 每个子上下文都会执行这个方法
}
}
@FeignClient(name = "service1", configuration = ClientConfig.class)
interface Service1Client {} // 有自己的 FeignContext
@FeignClient(name = "service2", configuration = ClientConfig.class)
interface Service2Client {} // 有自己的 FeignContext
问题根源:
- 每个 FeignClient 都有自己的
FeignContext(子 ApplicationContext) - 每个子上下文都会创建
ClientConfig实例 - 每个子上下文都会执行
@Bean方法 - 结果是:
- 313 个
OkHttpClient实例 - 每个实例都是各自子上下文中的单例
- 但不同子上下文之间有 313 个独立实例
- 313 个
5. 验证示例
JAVA
@Component
class DemoConfig {
private static int counter = 0;
@Bean
public String demoBean() {
counter++;
return "Bean-" + counter; // 每次调用创建不同的字符串
}
}
@FeignClient(name = "client1", configuration = DemoConfig.class)
interface Client1 {}
@FeignClient(name = "client2", configuration = DemoConfig.class)
interface Client2 {}
// 测试
@Test
void testMultipleContexts() {
// 获取 client1 的 FeignContext
FeignContext context1 = getFeignContext("client1");
String bean1 = context1.getBean(String.class); // "Bean-1"
// 获取 client2 的 FeignContext
FeignContext context2 = getFeignContext("client2");
String bean2 = context2.getBean(String.class); // "Bean-2" 是不同的实例
System.out.println(bean1.equals(bean2)); // false
}
6. 作用域控制
@Bean 默认是单例,但可以通过 @Scope 修改:
JAVA
@Configuration
public class ScopeConfig {
@Bean
@Scope("singleton") // 默认,可省略
public SingletonBean singletonBean() {
return new SingletonBean(); // 整个容器中只有一个
}
@Bean
@Scope("prototype")
public PrototypeBean prototypeBean() {
return new PrototypeBean(); // 每次获取都创建新的
}
@Bean
@Scope("request")
public RequestBean requestBean() {
return new RequestBean(); // 每个 HTTP 请求一个
}
@Bean
@Scope("session")
public SessionBean sessionBean() {
return new SessionBean(); // 每个 HTTP 会话一个
}
}
7. 实际代码示例
JAVA
// 正确做法:提取共享配置到主上下文
@Configuration
public class SharedFeignConfiguration {
// 这个 Bean 会在主 ApplicationContext 中创建一次
// 所有 FeignClient 共享同一个实例
@Bean
public ConnectionPool feignConnectionPool() {
return new ConnectionPool(200, 5, TimeUnit.MINUTES);
}
@Bean
public OkHttpClient okHttpClient(ConnectionPool pool) {
return new OkHttpClient.Builder()
.connectionPool(pool)
.build();
}
}
// 每个 FeignClient 的特定配置
public class ClientSpecificConfig {
// 只放这个 Client 特有的配置
// 如自定义的 Interceptor、Encoder 等
@Bean
public RequestInterceptor authInterceptor() {
return requestTemplate -> {
requestTemplate.header("Authorization", "Bearer token");
};
}
}
8. 核心总结
| 特性 | 说明 |
|---|---|
| 注册到容器 | ✅ 是,@Bean 方法都会注册到 Spring 容器 |
| 默认作用域 | ✅ 单例(Singleton) |
有无 @Configuration |
主要影响方法间调用的行为 |
| FeignClient 场景 | 每个 Client 有自己的子上下文,导致跨上下文重复创建 |
| 解决方案 | 将共享 Bean 移到主上下文,让所有子上下文共享 |
9. 验证是否单例
JAVA
@SpringBootTest
class BeanSingletonVerification {
@Autowired
private ApplicationContext context;
@Test
void verifySingleton() {
// 1. 检查 Bean 定义
String[] beanNames = context.getBeanNamesForType(ConnectionPool.class);
System.out.println("ConnectionPool beans: " + Arrays.toString(beanNames));
// 2. 获取 Bean
ConnectionPool pool1 = context.getBean(ConnectionPool.class);
ConnectionPool pool2 = context.getBean(ConnectionPool.class);
// 3. 验证单例
assertSame(pool1, pool2); // 应该是同一个实例
// 4. 验证作用域
BeanDefinition beanDef = context.getBeanFactory()
.getBeanDefinition("feignConnectionPool");
System.out.println("Scope: " + beanDef.getScope()); // 输出: singleton
}
}
结论:是的,@Configuration 里的 @Bean 会自动注册到 Spring 容器,并且默认是单例。但在多上下文场景(如 FeignClient)中,需要在正确的上下文中定义 Bean 才能真正实现共享。
@Bean public MyService myService() { return new MyService(); // 会被注册到容器 } 如果先获取ConfigA 的对象bean, 再调用myService()方法返回MyService 对象,如果有@Configuration每次都是同一个对象,没有@Configuration每次都是新对象?
是的,你的理解完全正确!让我详细解释这个重要的区别:
核心区别演示
JAVA
// 场景1:有 @Configuration
@Configuration
public class ConfigA {
@Bean
public MyService myService() {
System.out.println("创建 MyService 实例");
return new MyService();
}
}
// 场景2:没有 @Configuration
@Component
public class ConfigB {
@Bean
public MyService myService() {
System.out.println("创建 MyService 实例");
return new MyService();
}
}
测试代码
JAVA
@SpringBootTest
class ConfigTest {
@Autowired
private ApplicationContext context;
@Test
void testWithConfiguration() {
// 获取 ConfigA 的 Bean
ConfigA configA = context.getBean(ConfigA.class);
// 获取 ConfigA 的实际类型
System.out.println("ConfigA 的实际类型: " + configA.getClass());
// 输出: com.example.ConfigA$$EnhancerBySpringCGLIB$$xxxx
// 注意:这是 CGLIB 代理类!
// 第1次调用
MyService service1 = configA.myService();
// 输出: 创建 MyService 实例
// 这次会创建并注册到容器
// 第2次调用
MyService service2 = configA.myService();
// 无输出!因为返回的是容器中已存在的 Bean
System.out.println("是否是同一个实例: " + (service1 == service2));
// 输出: true ✅
// 从容器获取验证
MyService fromContainer = context.getBean(MyService.class);
System.out.println("与容器中的相同: " + (service1 == fromContainer));
// 输出: true ✅
}
@Test
void testWithoutConfiguration() {
// 获取 ConfigB 的 Bean
ConfigB configB = context.getBean(ConfigB.class);
// 获取 ConfigB 的实际类型
System.out.println("ConfigB 的实际类型: " + configB.getClass());
// 输出: com.example.ConfigB
// 注意:这是原始类,没有代理!
// 第1次调用
MyService service1 = configB.myService();
// 输出: 创建 MyService 实例
// 第2次调用
MyService service2 = configB.myService();
// 输出: 创建 MyService 实例
// 再次创建新实例!
System.out.println("是否是同一个实例: " + (service1 == service2));
// 输出: false ❌
// 从容器获取验证
MyService fromContainer = context.getBean(MyService.class);
System.out.println("与容器中的相同: " + (service1 == fromContainer));
// 输出: false ❌
// 从容器获取的是另一个实例!
}
}
工作原理图示
有 @Configuration(代理模式)
获取 ConfigA Bean → Spring 容器
↓
返回 CGLIB 代理对象
↓
调用 configA.myService() → 代理拦截 → 检查容器中是否有 MyService Bean
↓
如果有:返回容器中的单例
↓
如果没有:创建并注册到容器,然后返回
无 @Configuration(普通模式)
获取 ConfigB Bean → Spring 容器
↓
返回原始对象(无代理)
↓
调用 configB.myService() → 直接执行普通 Java 方法
↓
每次都创建新对象
↓
Spring 容器管理的 MyService Bean 是另一个实例
更复杂的场景
JAVA
@Configuration
public class FullConfig {
private int counter = 0;
@Bean
public ExpensiveService serviceA() {
counter++;
System.out.println("创建 serviceA, 计数器: " + counter);
return new ExpensiveService();
}
@Bean
public Client clientA() {
// 这里会调用代理方法
ExpensiveService s1 = serviceA(); // 第1次调用
ExpensiveService s2 = serviceA(); // 第2次调用
ExpensiveService s3 = serviceA(); // 第3次调用
System.out.println("s1 == s2: " + (s1 == s2)); // true
System.out.println("s2 == s3: " + (s2 == s3)); // true
return new Client(s1, s2, s3);
}
}
// 输出:
// 创建 serviceA, 计数器: 1
// s1 == s2: true
// s2 == s3: true
// 只创建了一次!
@Component
public class LiteConfig {
private int counter = 0;
@Bean
public ExpensiveService serviceB() {
counter++;
System.out.println("创建 serviceB, 计数器: " + counter);
return new ExpensiveService();
}
@Bean
public Client clientB() {
// 普通方法调用
ExpensiveService s1 = serviceB(); // 第1次调用
ExpensiveService s2 = serviceB(); // 第2次调用
ExpensiveService s3 = serviceB(); // 第3次调用
System.out.println("s1 == s2: " + (s1 == s2)); // false
System.out.println("s2 == s3: " + (s2 == s3)); // false
return new Client(s1, s2, s3);
}
}
// 输出:
// 创建 serviceB, 计数器: 1
// 创建 serviceB, 计数器: 2
// 创建 serviceB, 计数器: 3
// s1 == s2: false
// s2 == s3: false
// 创建了三次!
在 FeignClient 配置中的影响
JAVA
// 你的 ClientConfig
public class ClientConfig { // 没有 @Configuration
@Bean
public ConnectionPool connectionPool() {
System.out.println("创建 ConnectionPool");
return new ConnectionPool(5, 5, TimeUnit.MINUTES);
}
@Bean
public OkHttpClient okHttpClient() {
// 这里调用的是普通方法!
ConnectionPool pool = connectionPool(); // 每次都创建新的 ConnectionPool
return new OkHttpClient.Builder()
.connectionPool(pool) // 每个 OkHttpClient 有自己的连接池
.build();
}
}
// 在 FeignClient 中使用
@FeignClient(name = "service1", configuration = ClientConfig.class)
interface Service1Client {}
@FeignClient(name = "service2", configuration = ClientConfig.class)
interface Service2Client {}
// 结果:
// 每个 FeignClient 都会创建新的 ClientConfig 实例
// 每个 ClientConfig 的 okHttpClient() 会调用 connectionPool()
// 每次调用都会创建新的 ConnectionPool
// 313 个 FeignClient → 313 个 ConnectionPool!
正确的方法注入
JAVA
// 正确做法:通过方法参数注入
@Component
public class LiteConfigCorrect {
@Bean
public ConnectionPool connectionPool() {
return new ConnectionPool(5, 5, TimeUnit.MINUTES);
}
@Bean
public OkHttpClient okHttpClient(
// 通过参数注入,Spring 会从容器中获取
ConnectionPool pool) {
return new OkHttpClient.Builder()
.connectionPool(pool) // 使用容器中的单例
.build();
}
}
验证代理
JAVA
@Test
void verifyProxy() {
ApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(ConfigA.class, ConfigB.class);
ctx.refresh();
// 获取 ConfigA
ConfigA configA = ctx.getBean(ConfigA.class);
System.out.println("ConfigA 是代理: " +
configA.getClass().getName().contains("$$EnhancerBySpringCGLIB$$"));
// 输出: true
// 获取 ConfigB
ConfigB configB = ctx.getBean(ConfigB.class);
System.out.println("ConfigB 是代理: " +
configB.getClass().getName().contains("$$EnhancerBySpringCGLIB$$"));
// 输出: false
// 查看 Bean 定义
System.out.println("ConfigA 的 Bean 定义: " +
ctx.getBeanFactory().getBeanDefinition("configA").getBeanClassName());
// 输出: com.example.ConfigA$$EnhancerBySpringCGLIB$$xxxx
System.out.println("ConfigB 的 Bean 定义: " +
ctx.getBeanFactory().getBeanDefinition("configB").getBeanClassName());
// 输出: com.example.ConfigB
}
总结表格
| 场景 | 有 @Configuration |
无 @Configuration |
|---|---|---|
| 是否被代理 | ✅ 是,CGLIB 代理 | ❌ 否,原始对象 |
| @Bean 方法调用 | 代理拦截,返回容器中的 Bean | 普通方法调用,创建新实例 |
| 多次调用结果 | 返回同一个单例实例 | 每次返回新实例 |
| 容器中的 Bean | 与代理方法返回的相同 | 与方法返回的不同 |
| 内存效率 | ✅ 高,单例复用 | ❌ 低,可能创建多个实例 |
| 适用场景 | Bean 之间有依赖关系的复杂配置 | 简单的、独立的 Bean 定义 |
最佳实践
- 总是使用
@Configuration注解配置类,除非有特殊原因 - 避免在
@Bean方法中直接调用其他@Bean方法,而是通过方法参数注入 - FeignClient 配置中,将共享 Bean 提取到主上下文
- 需要验证时,检查配置类是否是 CGLIB 代理
JAVA
// 最佳实践示例
@Configuration
public class BestConfig {
// 通过参数注入,而不是方法调用
@Bean
public ServiceA serviceA() {
return new ServiceA();
}
@Bean
public ServiceB serviceB(ServiceA serviceA) { // ✅ 正确
return new ServiceB(serviceA);
}
// 如果必须调用,确保在 @Configuration 中
@Bean
public ServiceC serviceC() {
// 在 @Configuration 中,这是安全的
return new ServiceC(serviceA());
}
}