Spring Core & Boot/@Bean & @Configuration — cơ chế CGLIB proxy và composition
23/41
Bài 23 / 41~12 phútConfiguration & Bean DeclarationMiễn phí lượt xem

@Bean & @Configuration — cơ chế CGLIB proxy và composition

Tại sao @Configuration phải dùng CGLIB proxy, full mode vs lite mode khác nhau ra sao, @Bean method cho class third-party, và @Import để ghép config theo nhóm.

TL;DR: @Bean method là cách khai báo bean cho class third-party (không thể annotate @Component). @Configuration không chỉ là container của @Bean — Spring enhance class này bằng CGLIB subclass để intercept mọi lời gọi @Bean method, đảm bảo method gọi nhau trả cùng singleton instance ("full mode"). Tắt CGLIB bằng proxyBeanMethods = false ("lite mode") để startup nhanh hơn, nhưng phải kỷ luật: mọi dependency qua parameter, không gọi method @Bean khác trong body. @Import cho phép ghép nhiều config class mà không cần @ComponentScanSpring Boot autoconfig dùng cơ chế này.

1. Tại sao cần @Bean — vấn đề với class third-party

@Component yêu cầu bạn annotate class nguồn. Với class third-party như HikariDataSource, ObjectMapper, hay RestTemplate, bạn không sửa được source. @Bean method giải quyết bài toán này: method trả về object, Spring lấy object đó đăng ký vào container.

@Configuration
public class InfrastructureConfig {

    @Bean
    public DataSource dataSource(
            @Value("${db.url}") String url,
            @Value("${db.user}") String user,
            @Value("${db.pass}") String pass) {
        var ds = new HikariDataSource();
        ds.setJdbcUrl(url);
        ds.setUsername(user);
        ds.setPassword(pass);
        ds.setMaximumPoolSize(20);
        return ds;
    }

    @Bean
    public ObjectMapper objectMapper() {
        return new ObjectMapper()
            .registerModule(new JavaTimeModule())
            .disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
    }
}

Phân tích:

  • Bean name mặc định = method name (dataSource, objectMapper).
  • Bean class = return type khai báo.
  • Method parameter được Spring inject@Value resolve từ Environment.
  • Toàn bộ logic cấu hình nằm trong method body: set pool size, register module, disable feature.

Bảng so sánh hai cách khai báo:

CáchĐiều kiệnCơ chế
@Component / stereotypeClass do bạn viếtComponent scan tìm class, gọi constructor
@Bean methodClass bất kỳ — third-party hoặc cần config phức tạpSpring gọi method, lấy return value

2. Vấn đề khi @Bean method gọi nhau — cần hiểu trước khi gặp bug

Giả sử bạn có hai bean: ConnectionPoolTransactionManagerTransactionManager cần đúng instance ConnectionPool đã đăng ký:

// KHONG co @Configuration
public class DatabaseConfig {

    @Bean
    public ConnectionPool connectionPool() {
        return new HikariConnectionPool(/* config */);
    }

    @Bean
    public TransactionManager transactionManager() {
        return new DataSourceTransactionManager(connectionPool()); // goi method
    }
}

Ý định của lập trình viên: transactionManager dùng cùng instance connectionPool đã đăng ký. Thực tế khi không có @Configuration: connectionPool() trong body transactionManager()lời gọi Java thuần — không qua Spring. Kết quả: Spring tạo 1 ConnectionPool bean, rồi transactionManager() tạo thêm 1 ConnectionPool mới khi được gọi. Bạn có hai pool riêng biệt trong app — pool bean không có transaction nào đi qua, pool trong TransactionManager không ai giám sát. Bug cực kỳ khó trace.

Đây chính là lý do @Configuration tồn tại.

3. Cơ chế CGLIB trong @Configuration — full mode

Khi class được annotate @Configuration, Spring không dùng class gốc của bạn — nó tạo một CGLIB subclass (tên dạng DatabaseConfig$$EnhancerBySpringCGLIB$$abc123) với các @Bean method được override. Override logic: mỗi lần method @Bean được gọi, proxy kiểm tra container trước — bean đó đã tồn tại chưa? Nếu có, trả cached instance; chưa có thì gọi method gốc và đăng ký kết quả.

@Configuration
public class DatabaseConfig {

    @Bean
    public ConnectionPool connectionPool() {
        return new HikariConnectionPool(/* config */);
    }

    @Bean
    public TransactionManager transactionManager() {
        return new DataSourceTransactionManager(connectionPool()); // bi intercept
    }
}

Bây giờ connectionPool() trong body transactionManager() không phải lời gọi Java thuần. Nó đi qua override method của CGLIB proxy, proxy tra container thấy connectionPool bean đã có, trả về instance cached. Kết quả: TransactionManager và phần còn lại của app đều giữ cùng một ConnectionPool object.

Đặt hai trường hợp cạnh nhau thì thấy rõ: cùng một dòng code bạn viết, ý nghĩa của nó do một annotation quyết định.

Cùng một lời gọi connectionPool trong thân method Bean chẻ thành hai nhánh: class trần tạo thêm pool thứ hai, còn CGLIB subclass trả về đúng bean đã đăng ký

ConfigurationClassPostProcessor — một BeanFactoryPostProcessor — là thành phần xử lý toàn bộ @Configuration class trước khi container tạo bean. Nó parse @Bean method, đăng ký BeanDefinition, và áp dụng CGLIB enhancement. Xem thêm về BeanFactoryPostProcessor tại BeanDefinition & BeanFactoryPostProcessor.

Constraints của CGLIB enhancement

Vì CGLIB cần subclass class của bạn và override method:

  • @Configuration class không được final — CGLIB không subclass được → startup fail với BeanDefinitionParsingException.
  • Method @Bean không được private hoặc final — không override được → method bị xử lý ở "lite mode" ngay cả khi class có @Configuration.
  • Method public hoặc protected đều OK.

4. Lite mode — proxyBeanMethods = false

Spring 5.2 thêm attribute proxyBeanMethods trên @Configuration. Khi đặt false, Spring không sinh CGLIB subclass — class của bạn được dùng trực tiếp. @Bean method vẫn được Spring gọi để tạo bean, nhưng các lời gọi method từ trong body không bị intercept.

@Configuration(proxyBeanMethods = false)
public class WebClientConfig {

    @Bean
    public WebClient.Builder webClientBuilder() {
        return WebClient.builder()
            .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
    }

    @Bean
    public UserApiClient userApiClient(WebClient.Builder builder) { // inject qua parameter
        return new UserApiClient(builder.baseUrl("https://users.api").build());
    }

    @Bean
    public OrderApiClient orderApiClient(WebClient.Builder builder) { // inject qua parameter
        return new OrderApiClient(builder.baseUrl("https://orders.api").build());
    }
}

Để lite mode hoạt động đúng, toàn bộ dependency phải đến qua method parameter — Spring inject vào khi gọi method. Không gọi method @Bean khác trong body.

Lợi ích của lite mode:

  • Startup nhanh hơn — không sinh CGLIB subclass cho mỗi config class.
  • Tương thích tốt hơn với GraalVM native image — CGLIB sinh proxy class tại runtime, không native-friendly.
  • Spring Boot 2.2+ tự annotate proxyBeanMethods = false cho phần lớn autoconfig class.

Bảng so sánh:

Full mode (default)Lite mode (proxyBeanMethods=false)
CGLIB subclassKhông
@Bean gọi nhau trong bodyTrả cùng singletonTạo instance mới mỗi lần
Startup speedChậm hơn một chútNhanh hơn
Native imageKhó hơnThân thiện hơn
Khi nào dùngCode gọi method nhau, legacyCode disciplined — chỉ inject qua param

5. Pitfall lite mode — hai instance tưởng là một

Đây là lỗi phổ biến nhất khi chuyển sang proxyBeanMethods = false mà không refactor dependency:

// ❌ SAI — lite mode nhưng goi method trong body
@Configuration(proxyBeanMethods = false)
public class CacheConfig {

    @Bean
    public CacheKeyGenerator keyGenerator() {
        return new DefaultCacheKeyGenerator();
    }

    @Bean
    public LocalCache localCache() {
        return new LocalCache(keyGenerator()); // tao CacheKeyGenerator MOI, khong phai bean
    }

    @Bean
    public DistributedCache distributedCache() {
        return new DistributedCache(keyGenerator()); // lai tao CacheKeyGenerator MOI
    }
}

Kết quả: app có 3 instance CacheKeyGenerator — 1 bean do Spring quản lý, 1 trong localCache, 1 trong distributedCache. Ba instance này độc lập, state không đồng bộ nếu CacheKeyGenerator có state.

// ✅ DUNG — inject qua parameter
@Configuration(proxyBeanMethods = false)
public class CacheConfig {

    @Bean
    public CacheKeyGenerator keyGenerator() {
        return new DefaultCacheKeyGenerator();
    }

    @Bean
    public LocalCache localCache(CacheKeyGenerator keyGenerator) { // Spring inject
        return new LocalCache(keyGenerator);
    }

    @Bean
    public DistributedCache distributedCache(CacheKeyGenerator keyGenerator) { // cung instance
        return new DistributedCache(keyGenerator);
    }
}

6. @Import — ghép config theo nhóm

@Import cho phép một config class kéo thêm config class khác vào container — không cần component scan:

@Configuration
@Import({DatabaseConfig.class, SecurityConfig.class, MessagingConfig.class})
public class AppConfig {
    // Bean rieng cua app
}

Khác biệt với @ComponentScan:

@ComponentScan@Import
Phạm viTự động quét packageLiệt kê explicit
Tốc độChậm hơn (scan classpath)Nhanh hơn (không scan)
Dùng choApplication codeLibrary config, modular setup
Spring Boot autoconfigKhông dùngDùng — qua AutoConfiguration.imports

Spring Boot không dùng @ComponentScan cho autoconfig. Mỗi starter khai báo config class trong file META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports, và Spring Boot @Import chúng tại startup. Lý do: autoconfig phải explicit và không phụ thuộc package layout của user.

@Import cũng nhận ImportSelector (chọn class config theo điều kiện) và ImportBeanDefinitionRegistrar (đăng ký bean định nghĩa programmatically) — cơ chế của @EnableJpaRepositories, @EnableScheduling, và các @Enable* annotation.

Pattern thực tế

Khi viết library hoặc starter nội bộ: nhóm bean theo concern vào @Configuration riêng biệt (DatabaseConfig, SecurityConfig, MessagingConfig), rồi dùng @Import trong 1 entry config để user chỉ cần import 1 class. Tránh expose nhiều config class ra public API của library.

7. Lifecycle qua @BeaninitMethoddestroyMethod

@Bean cho phép khai báo lifecycle method của class third-party không implement InitializingBean hay DisposableBean:

@Bean(initMethod = "start", destroyMethod = "stop")
public ConnectionPool connectionPool() {
    return new HikariConnectionPool(config);
}

@Bean(destroyMethod = "") // tat auto-infer destroy method
public DataSource managedDataSource() {
    return externallyManagedDataSource; // lifecycle do he thong khac quan
}

Spring tự suy ra destroyMethod cho close() (Closeable/AutoCloseable) và shutdown(). Nếu lifecycle được quản lý bởi hệ thống khác (container, connection registry), đặt destroyMethod = "" để Spring không gọi auto.

Liên hệ các bài khác

  • BeanDefinition & BeanFactoryPostProcessor: ConfigurationClassPostProcessorBeanFactoryPostProcessor xử lý @Configuration — parse @Bean method, đăng ký BeanDefinition, kích hoạt CGLIB enhancement. Hiểu BFPP giải thích tại sao @Configuration class được xử lý trước khi bất kỳ bean nào được tạo.
  • Stereotypes & @ComponentScan: bài trước giải thích @Component và scan — bài này bổ sung @Bean@Configuration cho trường hợp scan không dùng được (third-party class).
  • @Conditional — nền auto-configuration: @Bean + @ConditionalOnClass / @ConditionalOnMissingBean là cặp đôi của Spring Boot autoconfig. Hiểu @Bean trước, rồi bài tiếp theo đào sâu condition.

Tóm tắt

  • @Bean method khai báo bean từ bất kỳ class nào — giải quyết bài toán class third-party không thể annotate @Component.
  • @Configuration không chỉ là marker — Spring sinh CGLIB subclass (full mode) để intercept @Bean method call, đảm bảo method gọi nhau trả cùng singleton instance.
  • Full mode (proxyBeanMethods = true, mặc định): CGLIB proxy, method gọi nhau an toàn. Class và method không được final.
  • Lite mode (proxyBeanMethods = false): không proxy, startup nhanh hơn, native-friendly. Bắt buộc: mọi dependency qua method parameter, không gọi method @Bean trong body.
  • Pitfall lite mode: gọi method @Bean khác trong body tạo instance mới (không phải bean cache) → nhiều instance không đồng bộ.
  • @Import kéo config class explicit — không scan, nhanh hơn, dùng cho library config và Spring Boot autoconfig.
  • ConfigurationClassPostProcessor (một BeanFactoryPostProcessor) chịu trách nhiệm xử lý toàn bộ @Configuration trước khi container instantiate bean.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Đoạn code sau in ra true hay false? Giải thích cơ chế bên dưới.
    @Configuration
    public class AppConfig {
      @Bean
      public Validator validator() { return new HibernateValidator(); }
    
      @Bean
      public UserService userService() {
          return new UserService(validator()); // goi method
      }
    
      @Bean
      public OrderService orderService() {
          return new OrderService(validator()); // goi method
      }
    }
    
    // Trong main:
    var userValidator = ctx.getBean(UserService.class).getValidator();
    var orderValidator = ctx.getBean(OrderService.class).getValidator();
    System.out.println(userValidator == orderValidator);
  2. Q2
    Bạn chuyển config sau sang proxyBeanMethods = false để tối ưu native image. Code có bug không? Nếu có, fix thế nào?
    @Configuration(proxyBeanMethods = false)
    public class MetricsConfig {
      @Bean
      public MeterRegistry meterRegistry() {
          return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
      }
    
      @Bean
      public HttpMetricsFilter httpFilter() {
          return new HttpMetricsFilter(meterRegistry()); // goi method
      }
    
      @Bean
      public DatabaseMetrics dbMetrics() {
          return new DatabaseMetrics(meterRegistry()); // goi method
      }
    }
  3. Q3
    Tại sao class @Configuration không được khai báo final, và method @Bean không được private? Giải thích từ góc độ cơ chế CGLIB.
  4. Q4
    Tại sao Spring Boot autoconfig dùng @Import thay vì @ComponentScan để load các autoconfig class?
  5. Q5
    Bạn thấy config class sau trong codebase legacy. Phân tích mọi vấn đề tiềm ẩn và đề xuất fix:
    @Configuration
    public final class AppConfig {
    
      @Bean
      private DataSource dataSource() {
          return new HikariDataSource();
      }
    
      @Bean
      public JdbcTemplate jdbcTemplate() {
          return new JdbcTemplate(dataSource()); // goi method
      }
    
      @Bean
      public TransactionTemplate transactionTemplate() {
          return new TransactionTemplate(dataSource()); // goi method
      }
    }

Bài tiếp theo: @Conditional — nền auto-configuration

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Bài này có giúp bạn hiểu bản chất không?

Hỏi đáp về bài này

Chưa có câu hỏi

Đặt câu hỏi

Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).

Đặt câu hỏi đầu tiên

Bài tiếp theo

@Conditional — đăng ký bean có điều kiện và nền tảng auto-configuration