Spring Cloud OpenFeign配置优化解决内存浪费

小豆丁 11小时前 ⋅ 3 阅读
聊天记录
头像

313 个 @FeignClient 声明了 configuration = ClientConfig.class Spring Cloud OpenFeign 为每个 FeignClient 创建独立的子 ApplicationContext(FeignContext) 每个子上下文都会实例化 ClientConfig 中的 @Bean 方法 ClientConfig 缺少 @Configuration 注解,导致每个子上下文都重新实例化 影响: 内存浪费:313 个 OkHttpClient + ConnectionPool 同时存在 连接池失效:每个池只服务一个 FeignClient,失去复用价值 启动变慢:313 次 Bean 创建拖慢启动速度 推荐修复方案: 将共享 Bean 提取到新的 @Configuration 类 SharedFeignConfiguration,注册到 spring.factories,使其只在主 ApplicationContext 创建一次,然后从 ClientConfig 中移除这些 Bean 定义。 因为ClientConfig没有@configuration注解,所以这么写的话,每个client都会创建一个config??

头像

你的理解是完全正确的。这是 Spring Cloud OpenFeign 中一个经典的内存浪费问题。让我详细解释一下:

当前问题分析

JAVA
// ClientConfig 没有 @Configuration 注解
public class ClientConfig {
    
    @Bean
    public OkHttpClient okHttpClient(ConnectionPool pool) {
        return new OkHttpClient.Builder()
            .connectionPool(pool)
            .build();
    }
    
    @Bean
    public ConnectionPool connectionPool() {
        return new ConnectionPool(5, 5, TimeUnit.MINUTES);
    }
}

关键点

  1. 即使没有 @Configuration 注解,当在 @FeignClient(configuration = ClientConfig.class) 中引用时,Spring Cloud OpenFeign 会将其视为配置类
  2. 每个 @FeignClient 都会创建一个独立的 FeignContext(子 ApplicationContext)
  3. 每个子上下文都会重新实例化 ClientConfig 中所有的 @Bean 方法
  4. 结果:313 个独立的 OkHttpClient + ConnectionPool 实例

推荐解决方案

方案1:提取共享配置(最佳实践)

JAVA
// 1. 创建共享配置类
@Configuration
public class SharedFeignConfig {
    
    @Bean
    @ConditionalOnMissingBean
    public ConnectionPool feignConnectionPool() {
        return new ConnectionPool(200, 5, TimeUnit.MINUTES); // 调整到合适大小
    }
    
    @Bean
    @ConditionalOnMissingBean
    public OkHttpClient feignOkHttpClient(ConnectionPool pool) {
        return new OkHttpClient.Builder()
            .connectionPool(pool)
            .connectTimeout(10, TimeUnit.SECONDS)
            .readTimeout(30, TimeUnit.SECONDS)
            .writeTimeout(30, TimeUnit.SECONDS)
            .build();
    }
}

// 2. 在 spring.factories 中注册
// resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.yourpackage.SharedFeignConfig

// 3. 修改 ClientConfig,移除共享 Bean
public class ClientConfig {
    // 只保留 FeignClient 特定的配置
    // 如自定义的 Encoder、Decoder、Interceptor 等
    
    @Bean
    public RequestInterceptor customInterceptor() {
        return template -> template.header("X-Custom", "value");
    }
}

方案2:使用原型作用域配置(如果需要独立配置)

JAVA
@Configuration
public class ClientConfig {
    
    // 使用原型作用域,但共享连接池
    @Bean
    @Scope("prototype")
    public OkHttpClient okHttpClient(
        @Qualifier("sharedConnectionPool") ConnectionPool sharedPool) {
        return new OkHttpClient.Builder()
            .connectionPool(sharedPool)  // 共享连接池
            .build();
    }
}

// 主配置中定义共享连接池
@Configuration
public class SharedConfig {
    
    @Bean(name = "sharedConnectionPool")
    public ConnectionPool sharedConnectionPool() {
        return new ConnectionPool(200, 5, TimeUnit.MINUTES);
    }
}

验证修复效果

修复后,可以通过以下方式确认:

JAVA
@SpringBootTest
class FeignConfigTest {
    
    @Autowired
    private ApplicationContext context;
    
    @Test
    void testSingleConnectionPool() {
        // 应该只有一个 ConnectionPool bean
        Map<String, ConnectionPool> pools = context.getBeansOfType(ConnectionPool.class);
        assertEquals(1, pools.size());
        
        // 所有 FeignClient 应该使用同一个连接池
        Map<String, OkHttpClient> clients = context.getBeansOfType(OkHttpClient.class);
        clients.values().forEach(client -> {
            assertSame(pools.values().iterator().next(), client.connectionPool());
        });
    }
}

性能提升预估

指标 修复前 修复后 提升
内存占用 313 × (OkHttpClient + ConnectionPool) 1 × ConnectionPool + 少量额外开销 减少 99%+
启动时间 慢(313 次 Bean 创建) 快(1 次创建) 显著提升
连接复用 313 个独立连接池 1 个共享连接池 最大化复用

注意事项

  1. 连接池大小调整:共享后需要根据总并发量调整连接池参数
  2. 线程安全:确保 OkHttpClient 配置是线程安全的
  3. 监控:添加连接池监控,观察使用情况
JAVA
@Bean
public ConnectionPool feignConnectionPool() {
    return new ConnectionPool(
        200,  // 最大空闲连接数
        5,    // 保持时间(分钟)
        TimeUnit.MINUTES
    );
}

这个问题修复后,你的应用内存占用会大幅减少,连接复用效率会显著提升,启动速度也会加快。

头像

@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 {}

关键点

  1. 即使没有 @Configuration@FeignClient(configuration = ...) 引用的类也会被 Spring 处理
  2. 但处理方式与有 @Configuration 不同:
    • 没有代理
    • 每个 @Bean 方法独立
  3. 在 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

问题根源

  1. 每个 FeignClient 都有自己的 FeignContext(子 ApplicationContext)
  2. 每个子上下文都会创建 ClientConfig 实例
  3. 每个子上下文都会执行 @Bean 方法
  4. 结果是:
    • 313 个 OkHttpClient 实例
    • 每个实例都是各自子上下文中的单例
    • 不同子上下文之间有 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 定义

最佳实践

  1. 总是使用 @Configuration 注解配置类,除非有特殊原因
  2. 避免在 @Bean 方法中直接调用其他 @Bean 方法,而是通过方法参数注入
  3. FeignClient 配置中,将共享 Bean 提取到主上下文
  4. 需要验证时,检查配置类是否是 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());
    }
}

全部评论: 0

    我有话说: