@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 @ComponentScan — Spring 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 —
@Valueresolve 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ện | Cơ chế |
|---|---|---|
@Component / stereotype | Class do bạn viết | Component scan tìm class, gọi constructor |
@Bean method | Class bất kỳ — third-party hoặc cần config phức tạp | Spring 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: ConnectionPool và TransactionManager — TransactionManager 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à 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.

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.
Vì CGLIB cần subclass class của bạn và override method:
@Configurationclass không đượcfinal— CGLIB không subclass được → startup fail vớiBeanDefinitionParsingException.- Method
@Beankhông đượcprivatehoặcfinal— không override được → method bị xử lý ở "lite mode" ngay cả khi class có@Configuration. - Method
publichoặcprotectedđề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 = falsecho phần lớn autoconfig class.
Bảng so sánh:
| Full mode (default) | Lite mode (proxyBeanMethods=false) | |
|---|---|---|
| CGLIB subclass | Có | Không |
@Bean gọi nhau trong body | Trả cùng singleton | Tạo instance mới mỗi lần |
| Startup speed | Chậm hơn một chút | Nhanh hơn |
| Native image | Khó hơn | Thân thiện hơn |
| Khi nào dùng | Code gọi method nhau, legacy | Code 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 vi | Tự động quét package | Liệt kê explicit |
| Tốc độ | Chậm hơn (scan classpath) | Nhanh hơn (không scan) |
| Dùng cho | Application code | Library config, modular setup |
| Spring Boot autoconfig | Không dùng | Dù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.
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 @Bean — initMethod và destroyMethod
@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:
ConfigurationClassPostProcessor—BeanFactoryPostProcessorxử lý@Configuration— parse@Beanmethod, đăng kýBeanDefinition, kích hoạt CGLIB enhancement. Hiểu BFPP giải thích tại sao@Configurationclass đượ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
@Componentvà scan — bài này bổ sung@Beanvà@Configurationcho trường hợp scan không dùng được (third-party class). - @Conditional — nền auto-configuration:
@Bean+@ConditionalOnClass/@ConditionalOnMissingBeanlà cặp đôi của Spring Boot autoconfig. Hiểu@Beantrước, rồi bài tiếp theo đào sâu condition.
Tóm tắt
@Beanmethod 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.@Configurationkhông chỉ là marker — Spring sinh CGLIB subclass (full mode) để intercept@Beanmethod 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 đượcfinal. - 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@Beantrong body. - Pitfall lite mode: gọi method
@Beankhác trong body tạo instance mới (không phải bean cache) → nhiều instance không đồng bộ. @Importkéo config class explicit — không scan, nhanh hơn, dùng cho library config và Spring Boot autoconfig.ConfigurationClassPostProcessor(mộtBeanFactoryPostProcessor) chịu trách nhiệm xử lý toàn bộ@Configurationtrước khi container instantiate bean.
Tự kiểm tra
- Q1Đoạn code sau in ra
truehayfalse? 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); - Q2Bạ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 } } - Q3Tại sao class
@Configurationkhông được khai báofinal, và method@Beankhông đượcprivate? Giải thích từ góc độ cơ chế CGLIB. - Q4Tại sao Spring Boot autoconfig dùng
@Importthay vì@ComponentScanđể load các autoconfig class? - Q5Bạ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
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