@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.
flowchart LR
subgraph NoConfig["Khong @Configuration -- lite mode"]
CP1["ConnectionPool bean<br/>(do Spring quan ly)"]
CP2["ConnectionPool instance<br/>(trong TransactionManager)"]
TM["TransactionManager bean"]
end
TM -- "giu ref toi" --> CP2
CP1 -- "bi bo qua" --> CP1Đâ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.
sequenceDiagram
participant Ctx as Container
participant CGLIB as DatabaseConfig$$CGLIB
participant Real as DatabaseConfig (real)
Ctx->>CGLIB: connectionPool()
CGLIB->>Real: super.connectionPool()
Real-->>CGLIB: new HikariConnectionPool
CGLIB->>Ctx: register bean connectionPool
Ctx->>CGLIB: transactionManager()
CGLIB->>CGLIB: intercept connectionPool()
alt bean da co trong cache
CGLIB-->>CGLIB: return cached ConnectionPool
end
CGLIB->>Real: super.transactionManager(cachedPool)
Real-->>Ctx: new DataSourceTransactionManagerConfigurationClassPostProcessor — 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 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);
▸
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);In true.
Class AppConfig có @Configuration, nên Spring sinh CGLIB subclass. Mọi lời gọi method @Bean đều bị intercept bởi proxy. Khi userService() chạy và gọi validator(), proxy kiểm tra container — bean validator đã có chưa? Lần đầu chưa có, gọi method gốc và đăng ký. Khi orderService() chạy và cũng gọi validator(), proxy thấy bean đã có trong singletonObjects, trả cached instance ngay.
Kết quả: userService và orderService giữ cùng một object HibernateValidator. Phép so sánh == trả true.
Nếu bỏ @Configuration (chỉ để class trần, lite mode): hai lời gọi validator() là Java thuần, mỗi lần tạo new HibernateValidator() mới. Kết quả in false — hai validator instance khác nhau, bug tinh tế.
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
}
}
▸
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
}
}Có bug. Lite mode không có CGLIB proxy. Mỗi lời gọi meterRegistry() trong body httpFilter() và dbMetrics() là lời gọi Java thuần — mỗi lần tạo new PrometheusMeterRegistry(...) mới. App sẽ có 3 instance MeterRegistry: 1 bean thật, 1 trong httpFilter, 1 trong dbMetrics. Metrics từ HTTP và database không đi vào cùng registry — dashboard Prometheus trống.
Fix: inject qua method parameter thay vì gọi method:
@Configuration(proxyBeanMethods = false)
public class MetricsConfig {
@Bean
public MeterRegistry meterRegistry() {
return new PrometheusMeterRegistry(PrometheusConfig.DEFAULT);
}
@Bean
public HttpMetricsFilter httpFilter(MeterRegistry registry) { // inject
return new HttpMetricsFilter(registry);
}
@Bean
public DatabaseMetrics dbMetrics(MeterRegistry registry) { // inject
return new DatabaseMetrics(registry);
}
}Spring inject cùng instance meterRegistry bean vào cả hai method. Không cần proxy — dependency rõ ràng qua signature.
Q3Tạ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.▸
@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.CGLIB tạo singleton instance bằng cách **subclass** class của bạn (tạo YourConfig$$EnhancerByCGLIB extends YourConfig) và **override** từng method @Bean để thêm logic intercept.
Hai ràng buộc Java ngăn cản điều này:
- Class
final: Java không cho phép subclass classfinal. CGLIB không thể tạo subclass, nên Spring némBeanDefinitionParsingExceptionngay lúc startup. - Method
privatehoặcfinal: methodprivatekhông thể override (không kế thừa được); methodfinalbị Java chặn override. Cả hai đều khiến CGLIB không thể inject intercept logic. Spring xử lý method đó ở "lite mode" — không có proxy — ngay cả khi class có@Configuration.
Fix: bỏ final khỏi class, dùng public hoặc protected cho method @Bean. Hoặc dùng proxyBeanMethods = false để không cần CGLIB — nhưng phải đảm bảo không gọi method nhau trong body.
Q4Tại sao Spring Boot autoconfig dùng @Import thay vì @ComponentScan để load các autoconfig class?▸
@Import thay vì @ComponentScan để load các autoconfig class?Có ba lý do chính:
- Explicit và an toàn:
@ComponentScanquét theo package — nếu user project có package trùng tên với starter, có thể scan nhầm.@Importliệt kê class cụ thể, không phụ thuộc package layout của user. - Performance: scan classpath để tìm
@Componenttrong jar của tất cả dependency tốn thời gian O(classpath size).@Importqua fileMETA-INF/spring/...AutoConfiguration.importsđọc danh sách có sẵn — O(1) lookup. - Isolation: autoconfig bean chỉ được load khi điều kiện
@ConditionalOn*thoả mãn. Với@ComponentScan, bean sẽ scan và đăng ký vô điều kiện (trừ khi thêm filter phức tạp).@Importkết hợp với@Conditionalcho phép lazy evaluation sạch hơn.
Quy tắc thực dụng: khi viết library hay starter nội bộ, dùng @Import để expose config — đừng yêu cầu user thêm package vào @ComponentScan.
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
}
}
▸
@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
}
}Có hai vấn đề nghiêm trọng:
Vấn đề 1 — Class final: CGLIB không thể subclass final class. App sẽ ném exception lúc startup. Fix: bỏ final.
Vấn đề 2 — Method dataSource() là private: ngay cả khi sửa final, CGLIB không override được method private. Spring xử lý method đó ở lite mode — dataSource() trong body jdbcTemplate() và transactionTemplate() là lời gọi Java thuần, mỗi lần tạo HikariDataSource mới. Kết quả: 3 connection pool trong app, không cái nào share transaction với nhau.
Fix đúng:
@Configuration // bo final
public class AppConfig {
@Bean // doi thanh public hoac protected
public DataSource dataSource() {
return new HikariDataSource();
}
@Bean
public JdbcTemplate jdbcTemplate() {
return new JdbcTemplate(dataSource()); // gio duoc intercept boi CGLIB
}
@Bean
public TransactionTemplate transactionTemplate() {
return new TransactionTemplate(dataSource()); // tra cung pool
}
}Hoặc dùng lite mode với parameter injection nếu muốn native-friendly:
@Configuration(proxyBeanMethods = false)
public class AppConfig {
@Bean
public DataSource dataSource() { return new HikariDataSource(); }
@Bean
public JdbcTemplate jdbcTemplate(DataSource ds) { return new JdbcTemplate(ds); }
@Bean
public TransactionTemplate transactionTemplate(DataSource ds) {
return new TransactionTemplate(ds);
}
}Bài tiếp theo: @Conditional — nền auto-configuration
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