@Configuration, @Bean, @Component — 3 cách khai báo bean và sự khác biệt
3 annotation trông giống nhau nhưng khác bản chất: @Component cho class bạn sở hữu, @Bean cho bean tạo bằng method (third-party), @Configuration tạo class chứa @Bean. Bài này bóc cơ chế CGLIB enhancement, lite mode vs full mode, stereotype @Service/@Repository/@Controller, @Import, ComponentScan filter, @Conditional* family, và meta-annotation tự định nghĩa.
Spring có 3 cách chính khai báo bean: @Component (và bạn bè @Service/@Repository/@Controller), @Bean, và @Configuration. Newcomer thường confused vì các bài blog dùng lẫn lộn: "đặt @Service", "viết @Bean method", "annotate @Configuration class chứa @Bean". 3 cách hoạt động bên dưới khác nhau hoàn toàn — và sự khác nhau ảnh hưởng tới performance, testability, và behavior subtle.
Bài này phân tích 3 annotation ở mức bản chất, giải thích vì sao @Configuration class bị Spring enhance bằng CGLIB, sự khác biệt giữa "lite mode" và "full mode", @Import cho composition config, filter mode của @ComponentScan, family @ConditionalOn*, và cách tự define meta-annotation cho domain.
1. Bảng tổng kết — 3 cách
| Annotation | Đặt ở đâu | Bean class là gì | Khi nào dùng |
|---|---|---|---|
@Component | Trên class | Chính class đó | Class bạn sở hữu (service, repo, util) |
@Bean | Trên method (trong class config) | Return type của method | Bean third-party hoặc cần config phức tạp |
@Configuration | Trên class chứa @Bean method | Class này không phải bean nghiệp vụ — chỉ là factory | Khi bạn dùng nhiều @Bean cùng nhóm |
Phân biệt nhanh:
- "Tôi có class
OrderServicecủa tôi viết, muốn nó thành bean" →@Service(sub-class của@Component). - "Tôi cần bean
DataSourcetừ Hikari (third-party)" →@Beanmethod trong class@Configuration. - "Tôi muốn nhóm các
@Beanmethod cùng concern" →@Configurationclass.
2. @Component và stereotypes
@Component là annotation gốc — đánh dấu class là Spring-managed bean để component scan tìm thấy.
@Component
public class EmailValidator {
public boolean isValid(String email) { /* ... */ }
}
Spring có 3 alias chuyên biệt theo layer, gọi là stereotype annotations:
| Annotation | Layer | Tính năng đặc biệt |
|---|---|---|
@Service | Service / business logic | Marker thuần — không thêm tính năng. Để rõ intent. |
@Repository | Data access | Marker + exception translation: tự động convert exception JDBC/JPA cụ thể (vd SQLException) thành DataAccessException hierarchy của Spring. |
@Controller | Web MVC | Marker + Spring MVC scan @RequestMapping method. Có biến thể @RestController = @Controller + @ResponseBody. |
flowchart TB
Component["@Component<br/>(generic stereotype)"]
Service["@Service"]
Repository["@Repository"]
Controller["@Controller"]
RestController["@RestController"]
Component --> Service
Component --> Repository
Component --> Controller
Controller --> RestController
style Component fill:#fef3c7Tất cả đều @Component ở mức code (annotation chain). Component scan tìm cả 4 đều gom vào bean.
Quy tắc thực dụng: chọn stereotype rõ nhất theo layer. Đừng dùng @Component trần khi class có vai trò rõ ràng:
@Service
public class OrderService { ... } // dung — business layer
@Repository
public class OrderRepository { ... } // dung — data layer
@Component // dung — utility class generic, khong fit layer nao
public class JsonSchemaValidator { ... }
Lợi ích phụ: developer khác đọc code biết ngay class này thuộc layer nào, không phải đọc body.
2.1 Bean naming conventions
Khi @Component không khai báo name, Spring tự sinh:
| Class | Bean name |
|---|---|
OrderService | orderService (camelCase, chữ đầu lowercase) |
XMLParser | XMLParser (giữ nguyên — 2+ chữ hoa đầu) |
URL | URL |
aBean | aBean |
Spring dùng Introspector.decapitalize để sinh name — chuyển chữ đầu lowercase trừ khi 2 chữ đầu đều uppercase.
Khai báo name explicit:
@Service("orderProcessor") // override default
public class OrderService { ... }
@Bean("dataSource") // override default = methodName
public DataSource buildDataSource() { ... }
Bean name unique trong cùng context. Trùng → từ Spring Boot 2.1 throw BeanDefinitionOverrideException (override mặc định tắt — spring.main.allow-bean-definition-overriding=false).
3. @Bean method — khi class không thuộc về bạn
@Component yêu cầu bạn annotate class. Nhưng nhiều khi bean cần là class third-party — bạn không sửa source được:
@Configuration
public class DataSourceConfig {
@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:
- Method được annotate
@Bean. Method return object — đó là bean. - Bean name mặc định = method name (
dataSource,objectMapper). - Bean class = return type khai báo (
DataSource,ObjectMapper). - Method parameter cũng được Spring inject —
@Valueresolve từ properties.
Use case của @Bean:
| Case | Lý do |
|---|---|
Class third-party (HikariDataSource, RestTemplate, ObjectMapper) | Không thể annotate @Component |
| Cần config phức tạp tại creation (set property, register module) | @Bean method có thể chứa logic |
| Cần tạo nhiều bean cùng class với config khác | 1 method = 1 bean, có thể có 5 method tạo 5 DataSource (read replica, write, archive...) |
3.1 Lifecycle qua @Bean
@Bean cho phép cấu hình lifecycle method:
@Bean(initMethod = "start", destroyMethod = "stop")
public CacheCluster cacheCluster() { return new CacheCluster(); }
@Bean(destroyMethod = "") // tat destroy method tu (inferred)
public DataSource dataSource() { return new HikariDataSource(); }
Spring tự deduce destroyMethod cho:
- Method
close()(Closeable/AutoCloseable). - Method
shutdown().
Nếu bạn không muốn Spring gọi auto, set destroyMethod = "" (empty string).
@Bean cũng support @Lazy, @Scope, @DependsOn, @Primary, @Qualifier cùng vị trí:
@Bean
@Lazy
@Scope("prototype")
@Qualifier("readReplica")
public DataSource readReplica() { ... }
4. @Configuration — không chỉ là container của @Bean
@Configuration là @Component đặc biệt với 1 super-power: Spring enhance class này bằng CGLIB để intercept @Bean method call. Đây là điểm thiết yếu mà 80% dev không biết.
4.1 Vấn đề @Bean trong "lite mode"
public class LiteConfig { // KHONG @Configuration
@Bean
public Foo foo() { return new Foo(); }
@Bean
public Bar bar() {
return new Bar(foo()); // goi method foo() truc tiep
}
}
Đoạn này không có @Configuration. Spring vẫn process @Bean method (lite mode), nhưng không enhance class. Kết quả:
foo()được Spring gọi 1 lần để tạo beanfoo.- Khi
bar()được Spring gọi tạo beanbar, codenew Bar(foo())chạy — nhưngfoo()ở đây là gọi method Java thuần, không qua Spring. Kết quả: tạo 1 instanceFoomới, khác với beanfoođã đăng ký. - Bạn có 2 instance Foo trong app: 1 do Spring quản, 1 nằm bên trong
Bar. Bug subtle.
4.2 @Configuration với CGLIB enhancement
@Configuration
public class FullConfig {
@Bean
public Foo foo() { return new Foo(); }
@Bean
public Bar bar() {
return new Bar(foo()); // call duoc intercept
}
}
Có @Configuration, Spring tạo CGLIB subclass của FullConfig với các @Bean method override. Override logic: kiểm tra container có bean đó chưa — có thì return bean cache, không thì call method gốc.
Kết quả: foo() trong body bar() trả về cùng instance với bean foo đã đăng ký. Chỉ 1 instance Foo trong app.
sequenceDiagram
participant Ctx as Container
participant Cfg as FullConfig$$EnhancerByCGLIB
participant Real as FullConfig (real)
Ctx->>Cfg: bar()
Cfg->>Cfg: foo() invoked
alt foo bean exists in cache
Cfg-->>Cfg: return cached foo
else first time
Cfg->>Real: foo() (super call)
Real-->>Cfg: new Foo()
Cfg->>Ctx: register as bean foo
end
Cfg->>Real: bar() body with foo
Real-->>Ctx: return BarĐây là lý do reference docs Spring luôn khuyến nghị annotate class chứa @Bean bằng @Configuration:
"@Bean methods may also be declared within classes that are not annotated with @Configuration. For example, bean methods may be declared in a @Component class or even in a plain old class. In such cases, @Bean methods are processed in a so-called 'lite' mode."
"In contrast to this, @Bean methods declared in @Configuration classes are recommended for general usage scenarios. They are processed in the 'full' mode where they get the full lifecycle management treatment, including CGLIB-based subclassing."
Nguồn: Spring Framework Reference — Basic Concepts: @Bean and @Configuration
4.3 Hệ quả của CGLIB enhancement
- Class
@Configurationkhông đượcfinal— CGLIB không subclass được class final → throw tại startup. - Method
@Beankhông đượcprivate/final— không override được. - Hơi tốn memory — 1 class CGLIB subclass cho mỗi config class. Negligible với app thường, đáng cân nhắc cho app native image.
- Spring 5.2+ thêm
proxyBeanMethods = false: tắt CGLIB enhancement nếu bạn chắc chắn không gọi method@Beanlẫn nhau:
@Configuration(proxyBeanMethods = false)
public class FastConfig {
@Bean
public Foo foo() { return new Foo(); }
@Bean
public Bar bar(Foo foo) { // inject qua parameter, KHONG goi foo() body
return new Bar(foo);
}
}
Ưu điểm: faster startup (no CGLIB), better cho native image. Nhược: bạn phải kỷ luật — mọi dependency qua method parameter, không qua method call.
Spring Boot 2.2+ tự annotate proxyBeanMethods = false cho hầu hết autoconfig — cho native-friendly.
5. @ComponentScan — chi tiết filter modes
Default @ComponentScan quét mọi class có @Component (hoặc stereotype). Customize qua filter:
@Configuration
@ComponentScan(
basePackages = {"com.olhub.domain", "com.olhub.api"},
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = MyService.class),
@ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Repository.class)
},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*Test.*"),
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Deprecated.class)
},
useDefaultFilters = false // chi dung includeFilters, bo qua @Component default
)
public class AppConfig { }
5 filter type:
| FilterType | Match | Use case |
|---|---|---|
ANNOTATION (default) | Class có annotation | @Component, custom stereotype |
ASSIGNABLE_TYPE | Class hoặc subclass | Match tất cả implement 1 interface |
ASPECTJ | AspectJ pointcut expression | Pattern phức tạp com.olhub..*Service |
REGEX | Regex tên class | Pattern theo tên file |
CUSTOM | Implement TypeFilter | Logic custom |
Default Spring Boot dùng @SpringBootApplication với @ComponentScan không filter — đủ 99% case.
5.1 Khi nào dùng filter
Hiếm — default đủ. Nhưng có vài case:
- Test bypass production bean:
excludeFiltersđể bỏ@Configurationcủa production khi test slice. - Module isolation: lib module muốn scan riêng package — không pull bean từ module khác.
- Plugin architecture: chỉ scan plugin có annotation
@MyPlugin, không bean thường.
6. @Import — composition của config
@Configuration có thể @Import config khác:
@Configuration
@Import({DataSourceConfig.class, SecurityConfig.class, MessagingConfig.class})
public class AppConfig {
// ...
}
Tương đương khai báo 4 config class với @SpringBootApplication. Khác @ComponentScan:
| Aspect | @ComponentScan | @Import |
|---|---|---|
| Phạm vi | Tự động — quét package | Explicit — liệt kê class |
| Tốc độ | Chậm hơn (scan classpath) | Nhanh hơn (no scan) |
| Use case | Application code | Library config, modular setup |
Spring Boot autoconfig không dùng @ComponentScan — nó dùng @Import qua file META-INF/spring/...AutoConfiguration.imports. Lý do: autoconfig phải explicit, không phụ thuộc package layout.
@Import cũng accept ImportSelector (logic chọn class config động) và ImportBeanDefinitionRegistrar (register bean programmatically). Đây là cơ chế của @EnableJpaRepositories, @EnableScheduling, ... — Module 02 đào sâu.
7. Conditional bean — feature mạnh của Spring Boot
Spring Boot mở rộng @Bean với @Conditional* — register bean tuỳ điều kiện:
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
public CacheManager redisCacheManager() {
return new RedisCacheManager(/* ... */);
}
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager fallbackCacheManager() {
return new ConcurrentMapCacheManager();
}
@Bean
@ConditionalOnProperty(name = "feature.audit.enabled", havingValue = "true")
public AuditLogger auditLogger() {
return new DefaultAuditLogger();
}
}
Family @ConditionalOn* đầy đủ:
| Annotation | Condition | Use case |
|---|---|---|
@ConditionalOnClass | Class có trong classpath | Auto-config bật khi user thêm starter |
@ConditionalOnMissingClass | Class không có | Fallback khi không có driver |
@ConditionalOnBean | Bean này đã register | Bean phụ thuộc bean khác |
@ConditionalOnMissingBean | Bean chưa register | Fallback default — user override được |
@ConditionalOnProperty | Property có giá trị | Feature flag, env-specific bean |
@ConditionalOnExpression | SpEL expression true | Logic phức tạp |
@ConditionalOnWebApplication | App là web (servlet/reactive) | Bean chỉ cho web |
@ConditionalOnNotWebApplication | App không web | Bean chỉ cho CLI |
@ConditionalOnResource | Resource tồn tại trên classpath | License file, schema file |
@ConditionalOnJava | Java version match | Compat với multiple Java version |
@ConditionalOnCloudPlatform | Detect cloud platform | Cloud Foundry, K8s specific |
@Profile | Active profile match | Dev/staging/prod toggle |
Đây là cốt lõi của Spring Boot auto-configuration — Module 02 sẽ đào sâu. Bài này chỉ cần biết: @Bean + condition là cách Spring Boot adapt với classpath/config.
7.1 Custom @Conditional
Tự define condition:
public class OnLinuxCondition implements Condition {
public boolean matches(ConditionContext ctx, AnnotatedTypeMetadata md) {
return System.getProperty("os.name").toLowerCase().contains("linux");
}
}
@Configuration
public class FilesystemConfig {
@Bean
@Conditional(OnLinuxCondition.class)
public FileSystemMonitor inotifyMonitor() { ... }
}
Hữu ích cho domain-specific condition (vd "chỉ load bean nếu license valid").
8. Meta-annotation — tự define stereotype
Bạn có thể tạo annotation custom inherit từ @Component:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Component
@Transactional
public @interface DomainService {
@AliasFor(annotation = Component.class) String value() default "";
}
// Su dung:
@DomainService
public class OrderService { ... } // = @Component + @Transactional + naming
Spring nhận diện meta-annotation qua chain. OrderService được scan thành bean (do @Component chain) và auto-tx (do @Transactional chain).
Use case:
- Domain-specific stereotype:
@DomainService,@ApplicationService,@DomainEventcho DDD codebase. - Cross-cutting concern:
@Loggable=@Component+ AOP advice cho logging. - Short-hand: tránh lặp
@Service @Validated @Transactional200 lần.
@AliasFor map attribute từ meta-annotation sang annotation lồng — giữ contract API.
9. Khi nào dùng cái nào — quyết định
flowchart TD
Start[Toi can declare bean]
Q1{Class la cua toi?}
Q2{Co fit layer service/repo/controller?}
UseStereotype["@Service / @Repository / @Controller"]
UseComponent["@Component"]
Q3{Co nhieu bean cung group?}
UseConfiguration["@Configuration class<br/>+ @Bean methods"]
UseBeanInExisting["@Bean method<br/>trong @Configuration co san"]
Start --> Q1
Q1 -->|Co| Q2
Q2 -->|Co| UseStereotype
Q2 -->|Khong| UseComponent
Q1 -->|Khong third-party| Q3
Q3 -->|Co| UseConfiguration
Q3 -->|It, 1-2 bean| UseBeanInExistingQuy tắc rút gọn:
- Class của bạn? → stereotype phù hợp layer. Mặc định
@Servicecho business logic. - Class third-party? →
@Beanmethod. - Nhiều bean cùng group (DataSource + Flyway + JPA config)? → tách
@Configurationclass riêng, ví dụDatabaseConfig. - 1-2 bean? → bỏ vào
@Configurationcó sẵn (vd ngay trong@SpringBootApplicationclass — nó đã là@Configuration).
10. Pitfall tổng hợp
❌ Nhầm 1: Quên @Configuration cho class chứa @Bean gọi nhau.
public class MyConfig { // KHONG @Configuration
@Bean Foo foo() { return new Foo(); }
@Bean Bar bar() { return new Bar(foo()); } // 2 instance Foo trong app
}
✅ Thêm @Configuration, hoặc đổi sang @Bean Bar bar(Foo foo) (param injection).
❌ Nhầm 2: Class @Configuration là final.
@Configuration
public final class C { ... } // CGLIB khong subclass duoc → startup fail
✅ Bỏ final, hoặc dùng proxyBeanMethods = false.
❌ Nhầm 3: @Bean method private/final.
@Configuration class C {
@Bean private Foo foo() { ... } // Spring khong override duoc
}
✅ Method public hoặc protected.
❌ Nhầm 4: Annotate @Service cho class third-party.
@Service // BAN khong sua source HikariDataSource duoc
public class HikariDataSource { ... }
✅ Class third-party → @Bean method trong config.
❌ Nhầm 5: Đặt @Component cho class chỉ chứa method static utility.
@Component
public class StringUtils {
public static String camelCase(String s) { ... }
}
✅ Class util tĩnh không cần Spring quản. Bỏ @Component. Hoặc convert thành bean với instance method nếu cần inject dep.
❌ Nhầm 6: Inject @Configuration class qua @Autowired để gọi @Bean method.
@Service class S {
@Autowired MyConfig cfg;
public void use() { Foo f = cfg.foo(); } // anti-pattern
}
✅ Inject bean Foo thẳng. Không dùng config class như factory runtime.
❌ Nhầm 7: @ComponentScan overlap nhiều basePackages.
@ComponentScan(basePackages = {"com.olhub", "com.olhub.api"}) // bao trum
✅ Chỉ scan rộng nhất — com.olhub đã bao gồm com.olhub.api. Khai báo lồng nhau dư thừa và scan trùng.
❌ Nhầm 8: Quên useDefaultFilters = false khi chỉ muốn includeFilters custom.
@ComponentScan(includeFilters = @Filter(MyMarker.class))
// VAN scan tat ca @Component default + class co MyMarker
✅ Set useDefaultFilters = false để chỉ scan theo includeFilters.
11. 📚 Deep Dive Spring Reference
Reference docs:
- Spring Framework Reference — @Configuration — full mode vs lite mode, CGLIB enhancement.
- Spring Framework Reference — @Bean — name, lifecycle, scope, init/destroy method qua
@Bean. - Spring Framework Reference — Stereotype Annotations —
@Service,@Repository,@Controllerchính thức. - Spring Framework Reference — Composing Java-based Configurations —
@Import,ImportSelector,ImportBeanDefinitionRegistrar. - Spring Framework Reference — Defining Bean Metadata Within Components —
@ComponentScanfilter detail. - Spring Framework Reference — @AliasFor and Annotation Attribute Aliases — meta-annotation,
@AliasFor. - Spring Boot Reference — Auto-configuration — cách
@ConditionalOn*được dùng trong Boot autoconfig. - Spring Boot Reference — Condition Annotations — đầy đủ family
@ConditionalOn*.
Source nên đọc:
org.springframework.context.annotation.ConfigurationClassPostProcessor— BFPP xử lý@Configuration, parse@Beanmethod, áp dụng CGLIB enhancement.org.springframework.context.annotation.ConfigurationClassEnhancer— class thực sự sinh CGLIB subclass.org.springframework.boot.autoconfigure.condition.OnClassCondition— implementation@ConditionalOnClass.
Ghi chú: đọc Javadoc của @Configuration (annotation source) — phần Javadoc rất chi tiết, giải thích full mode vs lite mode trực tiếp từ team Spring.
12. Tóm tắt
@Component+ stereotypes (@Service,@Repository,@Controller): annotate class bạn sở hữu để component scan gom thành bean.@Beanmethod: tạo bean từ method, dùng cho class third-party hoặc cần config phức tạp. SupportinitMethod/destroyMethod/@Lazy/@Scope.@Configurationclass: container của@Beanmethod. Spring enhance bằng CGLIB để@Beanmethod gọi nhau trả cùng instance ("full mode").- Không có
@Configuration→ "lite mode" → method@Beangọi nhau tạo instance mới mỗi lần (bug subtle). @Configurationclass không đượcfinal; method@Beankhông đượcprivate/final.proxyBeanMethods = falsetắt CGLIB cho startup nhanh hơn (Boot autoconfig dùng), với điều kiện không gọi method@Beanlẫn nhau trong body.@Repositoryđặc biệt: thêm exception translation từ JDBC/JPA vềDataAccessException.- Bean name mặc định = class name decapitalize (
OrderService→orderService). Trùng → throwBeanDefinitionOverrideExceptiontừ Spring Boot 2.1+. @ComponentScancó 5 filter type — default đủ 99% case.@Importcho composition explicit — Spring Boot autoconfig dùng@ImportquaAutoConfiguration.imports, không scan.@Conditional*family (12+ annotation) cho phép register bean tuỳ classpath/property/profile — cốt lõi của Spring Boot auto-configuration.- Meta-annotation +
@AliasForcho phép tự define stereotype domain-specific (@DomainService). - Class util tĩnh không cần annotate Spring.
13. Tự kiểm tra
Q1Đoạn sau in 1 hay 2? Vì sao?@Configuration
class Cfg {
@Bean Counter counter() { return new Counter(); }
@Bean Worker worker() { return new Worker(counter()); }
}
@Service
class Auditor {
@Autowired Counter counter;
@Autowired Worker worker;
public int distinctCounters() {
return Set.of(counter, worker.getCounter()).size();
}
}
▸
@Configuration
class Cfg {
@Bean Counter counter() { return new Counter(); }
@Bean Worker worker() { return new Worker(counter()); }
}
@Service
class Auditor {
@Autowired Counter counter;
@Autowired Worker worker;
public int distinctCounters() {
return Set.of(counter, worker.getCounter()).size();
}
}In 1.
Vì class Cfg có @Configuration, Spring enhance bằng CGLIB. Khi worker() chạy, call counter() bị intercept — proxy kiểm tra "bean counter đã có chưa?" → có → return cached instance. Vậy counter field và worker.getCounter() trỏ cùng object.
Nếu bỏ @Configuration đi (chỉ để class trần): "lite mode". worker() body gọi counter() như method Java thuần → tạo Counter mới. Lúc đó in 2 — distinct counters.
Đây là lý do Spring docs khuyến nghị luôn annotate @Configuration cho class chứa @Bean gọi nhau.
Q2@Service, @Repository, @Controller — 3 stereotype. Trên runtime container, chúng hoạt động như nhau (đều là @Component). Vậy có lý do thực sự để phân biệt không, hay chỉ là "code style"?▸
@Service, @Repository, @Controller — 3 stereotype. Trên runtime container, chúng hoạt động như nhau (đều là @Component). Vậy có lý do thực sự để phân biệt không, hay chỉ là "code style"?Có lý do thực sự — không chỉ code style.
@Repositorycó hành vi đặc biệt: Spring tự registerPersistenceExceptionTranslationPostProcessorwrap bean này. Khi method némSQLException,HibernateException, ... → tự động chuyển thànhDataAccessException(subclass) của Spring. Lợi ích: business code chỉ catch 1 hierarchyDataAccessException, không phụ thuộc DB driver cụ thể.@Controllerđược Spring MVC scan riêng: chỉ class có@Controller(hoặc@RestController) mới được DispatcherServlet xét@RequestMapping. Class@Servicevới@RequestMappingtrên method sẽ không tạo route — Spring MVC bỏ qua.@Servicelà marker thuần — không thêm tính năng. Nhưng vẫn nên dùng vì: AOP pointcut có thể match@within(Service), IDE tooling, code reader hiểu intent.
Quy tắc: dùng đúng stereotype cho đúng layer — vừa được tính năng đặc biệt, vừa rõ intent.
Q3Khi nào dùng @Configuration(proxyBeanMethods = false)? Lợi ích cụ thể là gì? Khi nào KHÔNG nên dùng?▸
@Configuration(proxyBeanMethods = false)? Lợi ích cụ thể là gì? Khi nào KHÔNG nên dùng?Khi nào dùng:
- Bạn chắc chắn không gọi
@Beanmethod lẫn nhau trong body — chỉ inject qua method parameter. - App có nhiều config class, optimize startup time.
- Build native image (GraalVM) — CGLIB phát sinh proxy class runtime, không native-friendly.
Lợi ích:
- Startup nhanh hơn (không sinh CGLIB subclass).
- Less memory (no extra proxy class).
- Native image compatibility tốt hơn.
- Spring Boot 2.2+ tự dùng cho mọi
@SpringBootConfigurationautoconfig — startup Boot 3 nhanh hơn Boot 1 đáng kể nhờ điều này.
Khi KHÔNG nên dùng:
- Code legacy có
@Beanmethod gọi nhau — bật flag sẽ gây bug subtle (instance trùng). - Style "tự document" thông qua method gọi method (vd
bar()body cófoo()để diễn đạt dependency).
Quy tắc: dự án mới với code disciplined → bật. Code cũ hoặc dev team không đồng đều → giữ default.
Q4Bạn cần 2 instance DataSource trong app: 1 cho write (primary DB), 1 cho read (replica). Cả 2 là HikariDataSource. Code declaration thế nào? Resolve injection ambiguity ra sao?▸
DataSource trong app: 1 cho write (primary DB), 1 cho read (replica). Cả 2 là HikariDataSource. Code declaration thế nào? Resolve injection ambiguity ra sao?@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("app.datasource.write")
public DataSource writeDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
@Bean
@Qualifier("read")
@ConfigurationProperties("app.datasource.read")
public DataSource readDataSource() {
return DataSourceBuilder.create().type(HikariDataSource.class).build();
}
}Resolve injection:
@Service
public class OrderService {
private final DataSource write;
private final DataSource read;
public OrderService(DataSource write, // @Primary - default
@Qualifier("read") DataSource read) { // explicit qualifier
this.write = write;
this.read = read;
}
}Cách khác: @Qualifier trên cả 2 (không dùng @Primary) — clean, phải khai báo qualifier mọi nơi inject. Hoặc đặt tên bean rõ ràng: @Bean("writeDataSource") rồi inject qua @Qualifier("writeDataSource").
Properties:
app.datasource.write.url=jdbc:postgresql://primary/app
app.datasource.write.username=app
app.datasource.read.url=jdbc:postgresql://replica/app
app.datasource.read.username=app_ro@ConfigurationProperties bind từng prefix vào DataSource tương ứng — không cần manual setter.
Q5Class StringUtils với toàn method static: camelCase, truncate, slugify. Có nên annotate @Component không? Vì sao?▸
StringUtils với toàn method static: camelCase, truncate, slugify. Có nên annotate @Component không? Vì sao?Không.
Lý do:
- Method
statickhông cần object instance để gọi —StringUtils.camelCase("foo")chạy mọi lúc, không cần Spring. - Class util không có dependency để inject → không cần Spring quản.
- Annotate
@Componentthừa: Spring tạo 1 instance không ai dùng (mọi caller gọi static method qua class, không qua bean). Tốn slot bean cache. - Test class util không cần
@SpringBootTest— gọi static method từ mọi test trực tiếp.
Khi nào util CẦN thành bean?
- Util cần inject dependency (vd
JsonValidatorcầnObjectMapper) → convert thành class với instance method và@Component. - Util cần config từ properties (
@Value) → tương tự. - Cần mock trong test (vd
ClockProvider) → bean với interface, mock được. Static method khó mock (cần PowerMock/Mockito-static — anti-pattern).
Quy tắc: stateless + no dep → static utility class, no Spring. Có dep hoặc cần mock → bean instance.
Q6Codebase team bạn có pattern: mọi service domain đều cần @Service @Validated @Transactional. Lặp 200 lần. Có cách nào DRY không?▸
@Service @Validated @Transactional. Lặp 200 lần. Có cách nào DRY không?Có — meta-annotation.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Validated
@Transactional
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}Sử dụng:
@DomainService
public class OrderService { ... } // = @Service @Validated @TransactionalCơ chế: Spring resolve meta-annotation theo chain. @DomainService annotated với @Service → class được scan như @Component. @Validated → kích hoạt validation. @Transactional → AOP wrap proxy cho tx.
@AliasFor map attribute từ meta-annotation sang annotation lồng — cho phép @DomainService("custom-name") set bean name.
Lợi ích:
- DRY — 1 annotation thay 3.
- Codebase consistency — không ai forget
@Transactionaltrên domain service. - Easier refactoring — đổi default behavior 1 chỗ (vd thêm
@Loggable) propagate đến mọi service. - Domain documentation — annotation tên domain (
DomainService) rõ ý nghĩa hơn lặp 3 generic annotation.
Cảnh báo: đừng over-engineer. Chỉ define meta-annotation khi pattern lặp ≥ 5 lần. Codebase nhỏ giữ explicit dễ đọc hơn.
Q7Đoạn sau làm gì? Bean nào sẽ được register? Khi nào?@Configuration
public class CacheConfig {
@Bean
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
@ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = false)
public CacheManager redisCacheManager(RedisConnectionFactory factory) { ... }
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager fallback() {
return new ConcurrentMapCacheManager();
}
}
▸
@Configuration
public class CacheConfig {
@Bean
@ConditionalOnClass(name = "org.springframework.data.redis.core.RedisTemplate")
@ConditionalOnProperty(name = "cache.type", havingValue = "redis", matchIfMissing = false)
public CacheManager redisCacheManager(RedisConnectionFactory factory) { ... }
@Bean
@ConditionalOnMissingBean(CacheManager.class)
public CacheManager fallback() {
return new ConcurrentMapCacheManager();
}
}Logic register theo điều kiện:
redisCacheManagerregister nếu cả 2 điều kiện đúng:- Class
RedisTemplatecó trong classpath (user thêmspring-boot-starter-data-redis). - Property
cache.type=redisset explicit (matchIfMissing = false→ property phải có).
- Class
fallbackregister nếu không beanCacheManagernào khác đã register — tức là Redis condition fail.
Kết quả 4 scenario:
| Scenario | Bean register |
|---|---|
Redis classpath + cache.type=redis | redisCacheManager |
| Redis classpath + không property | fallback (ConcurrentMapCacheManager) |
| Không Redis classpath + property bất kỳ | fallback |
| Không Redis + không property | fallback |
Pattern này phổ biến trong autoconfig: primary bean conditional, fallback default. User opt-in bằng cách thêm starter + set property → primary kích hoạt, fallback skip. Default behaviour vẫn work mà không cần config.
Quan trọng — thứ tự evaluation: @ConditionalOnMissingBean phụ thuộc bean order. Spring Boot dùng @AutoConfigureAfter/@AutoConfigureBefore để đảm bảo Redis evaluate trước fallback. Trong cùng @Configuration class, thứ tự method matter — Spring xử lý method theo source order.
Bài tiếp theo: Mini-challenge — tự build mini IoC container 80 dòng
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...