Stereotype & @ComponentScan — cơ chế scan biến class thành bean
Bài này bóc đúng một mảnh: @Component và 4 stereotype (@Service, @Repository, @Controller, @RestController) khác nhau ra sao, @ComponentScan quét classpath rồi tạo BeanDefinition như thế nào, vì sao tách stereotype theo layer (intent + AOP target), filter mode để kiểm soát phạm vi scan, và meta-annotation để tự định nghĩa stereotype domain-specific.
TL;DR: @Component là annotation đánh dấu "class này là Spring bean, hãy scan tôi". @ComponentScan đọc classpath, tìm mọi class có @Component (hoặc stereotype kế thừa từ nó), tạo BeanDefinition cho từng class và đăng ký vào beanDefinitionMap — chưa tạo object, chỉ ghi metadata. Trong 4 stereotype, chỉ @Repository có hành vi đặc biệt (exception translation qua PersistenceExceptionTranslationPostProcessor); @Controller được Spring MVC nhận diện để route request; @Service là marker thuần. Tách stereotype có hai lý do thực sự: rõ intent theo layer và AOP pointcut có thể target đúng annotation. Filter mode của @ComponentScan cho phép include/exclude class theo annotation, kiểu, regex, hay custom logic — hữu ích cho test slice và module isolation. Meta-annotation cho phép tự define stereotype như @DomainService = @Component + cross-cutting concern.
1. Vấn đề mà @Component giải quyết
Trước khi có component scan, mọi bean phải khai báo trong XML hoặc @Bean method — tức là mỗi class business phải được đề cập hai lần: một lần viết code, một lần khai báo với container. Với app 500 service/repository, đây là boilerplate không thể bảo trì.
@Component và @ComponentScan giải quyết bằng convention: đặt annotation vào class, Spring tự tìm. Bạn chỉ viết code một lần; container tự biết class đó cần quản lý.
// Truoc annotation-driven: khai bao explicit tung bean
// applicationContext.xml
// <bean id="orderService" class="com.olhub.service.OrderService"/>
// <bean id="paymentService" class="com.olhub.service.PaymentService"/>
// ... 200 dong XML cho 100 class
// Sau @Component + @ComponentScan: khong XML
@Service
public class OrderService { /* ... */ }
@Service
public class PaymentService { /* ... */ }
// Container tu scan va register ca 2
2. Cơ chế bên dưới — scan biến class thành BeanDefinition ra sao
@ComponentScan không scan bytecode raw — nó dùng ClassPathScanningCandidateComponentProvider để đọc metadata annotation mà không load class vào JVM (dùng ASM bytecode reader). Khi tìm thấy class phù hợp, nó tạo ScannedGenericBeanDefinition và đăng ký vào beanDefinitionMap của DefaultListableBeanFactory.
flowchart TB
CS["@ComponentScan(basePackages)"]
Scan["ClassPathScanningCandidateComponentProvider<br/>doc bytecode qua ASM, khong load class"]
Filter{"Co @Component<br/>hoac stereotype?"}
BD["tao ScannedGenericBeanDefinition<br/>(METADATA ONLY: class, scope, name)"]
Map["dang ky vao beanDefinitionMap"]
Skip["bo qua class"]
Inst["createBean() - chi khi getBean() goi<br/>hoac eager singleton luc startup"]
CS --> Scan
Scan --> Filter
Filter -->|"Co"| BD
Filter -->|"Khong"| Skip
BD --> Map
Map --> InstĐiểm cốt lõi: sau khi scan xong, container biết "có bean tên orderService, class OrderService" nhưng object OrderService chưa tồn tại trong heap. Instance chỉ được tạo ở pha sau (eager startup với ApplicationContext, hoặc lazy khi getBean với BeanFactory trần) — xem BeanFactory vs ApplicationContext để rõ hai pha này.
Cơ chế ASM (không load class) có lợi quan trọng: container có thể scan toàn bộ classpath mà không khởi động class với static initializer, không tốn memory heap cho class không được dùng.
2.1 @ComponentScan đặt ở đâu
@Configuration
@ComponentScan(basePackages = "com.olhub")
public class AppConfig { }
Hoặc không cần explicit nếu dùng @SpringBootApplication — annotation đó đã bao gồm @ComponentScan với basePackages mặc định là package của class được annotate. Xem thêm tại Resource & @SpringBootApplication.
3. @Component và 4 stereotype
@Component là annotation gốc. Spring cung cấp 4 alias chuyên biệt theo layer — tất cả đều có @Component trong annotation chain:
flowchart TB
Component["@Component<br/>(annotation goc, generic)"]
Service["@Service<br/>(business layer)"]
Repository["@Repository<br/>(data layer)"]
Controller["@Controller<br/>(web MVC)"]
RestController["@RestController<br/>(@Controller + @ResponseBody)"]
Component --> Service
Component --> Repository
Component --> Controller
Controller --> RestController
style Component fill:#fef3c7
style Repository fill:#dcfce7
style Controller fill:#dbeafeTất cả đều được @ComponentScan gom vào bean — Spring tra annotation chain, không chỉ annotation trực tiếp.
| Annotation | Layer | Hành vi đặc biệt ngoài scan |
|---|---|---|
@Component | Generic | Không |
@Service | Business logic | Marker thuần — không thêm behavior |
@Repository | Data access | Exception translation (xem §3.1) |
@Controller | Web MVC | Spring MVC nhận diện để xử lý @RequestMapping |
@RestController | Web MVC (REST) | = @Controller + @ResponseBody trên mọi method |
3.1 @Repository và exception translation
@Repository là stereotype duy nhất có hành vi runtime đặc biệt ngoài việc làm bean.
Vấn đề: mỗi persistence framework ném exception riêng — Hibernate ném HibernateException, JDBC ném SQLException, JPA ném PersistenceException. Business code catch những exception này sẽ bị coupling chặt với implementation tầng data.
Giải pháp của Spring: PersistenceExceptionTranslationPostProcessor — một BeanPostProcessor được Spring tự đăng ký — wrap mọi bean annotate @Repository trong AOP proxy. Proxy đó intercept method call và chuyển exception JDBC/JPA/Hibernate thành DataAccessException hierarchy của Spring.
@Repository
public class OrderRepository {
public Order findById(Long id) {
// Neu JDBC nem SQLException hoac Hibernate nem HibernateException
// -> proxy @Repository intercept -> convert thanh DataAccessException
return em.find(Order.class, id);
}
}
// Business code chi catch DataAccessException - khong phu thuoc DB driver
@Service
public class OrderService {
private final OrderRepository repo;
public Order getOrder(Long id) {
try {
return repo.findById(id);
} catch (DataAccessException e) {
// Handle loi DB o muc truu tuong - khong biet la JDBC hay JPA
throw new OrderNotFoundException(id, e);
}
}
}
Nếu dùng @Component thay @Repository, exception translation vẫn có thể hoạt động nếu implement PersistenceExceptionTranslator, nhưng mất signal intent rõ ràng. Quy tắc đơn giản: tầng data → luôn dùng @Repository.
3.2 @Controller và Spring MVC
Spring MVC's DispatcherServlet chỉ xét @RequestMapping method trên class có @Controller hoặc @RestController. Class @Service dù có @RequestMapping cũng không tạo route — Spring MVC bỏ qua.
@Controller // dung - DispatcherServlet xu ly route
public class OrderController {
@GetMapping("/orders/{id}")
public String detail(@PathVariable Long id, Model model) { /* ... */ }
}
@Service // SAI cho web route - DispatcherServlet bo qua
public class OrderService {
@GetMapping("/orders/{id}") // annotation nay khong co tac dung
public void wrong() { }
}
@RestController = @Controller + @ResponseBody trên toàn class — mọi method return value tự serialize thành JSON/XML thay vì resolve view name.
4. Vì sao tách stereotype — không chỉ code style
Câu hỏi thường gặp: nếu container treat tất cả stereotype như @Component, tại sao không dùng @Component cho hết?
Có hai lý do kỹ thuật thực sự:
Lý do 1 — AOP pointcut target chính xác theo layer. Annotation là metadata runtime — AOP aspect có thể match annotation cụ thể:
// Chi wrap transaction quanh method cua @Repository bean
@Around("@within(org.springframework.stereotype.Repository)")
public Object auditDataAccess(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
try {
return pjp.proceed();
} finally {
log.debug("Data access took {}ms", System.currentTimeMillis() - start);
}
}
// Chi log vao business boundary - chi service bean
@Before("@within(org.springframework.stereotype.Service)")
public void logServiceEntry(JoinPoint jp) {
log.info("Service call: {}", jp.getSignature());
}
Nếu mọi class dùng @Component, aspect phải match tên class hoặc package — giòn, phụ thuộc naming convention. Match theo stereotype annotation rõ ràng hơn và không phá khi rename package.
Lý do 2 — @Repository có exception translation. Đã giải thích ở §3.1 — đây là hành vi runtime, không chỉ intent.
Lý do 3 — Intent tài liệu cho người đọc code và tool. IDE, static analysis tool, architecture rule checker (ArchUnit) có thể enforce "layer A không import từ layer B" dựa trên stereotype annotation. Dùng @Component trần mất khả năng kiểm tra tự động này.
5. @ComponentScan filter modes
@ComponentScan với default filter scan mọi class có @Component (hoặc stereotype). Customize qua includeFilters/excludeFilters:
@Configuration
@ComponentScan(
basePackages = "com.olhub",
includeFilters = {
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = MyPlugin.class // chi scan class co @MyPlugin
)
},
excludeFilters = {
@ComponentScan.Filter(
type = FilterType.REGEX,
pattern = ".*IntegrationTest" // bo qua class ten ket thuc IntegrationTest
),
@ComponentScan.Filter(
type = FilterType.ANNOTATION,
classes = Deprecated.class // bo qua class @Deprecated
)
},
useDefaultFilters = false // tat scan @Component default
)
public class PluginConfig { }
5 filter type:
| FilterType | Match theo | Use case |
|---|---|---|
ANNOTATION (default) | Class có annotation cụ thể | Stereotype, custom marker |
ASSIGNABLE_TYPE | Class hoặc subtype của kiểu chỉ định | Tất cả implement interface X |
ASPECTJ | AspectJ pointcut expression | Pattern phức tạp như com.olhub..*Service+ |
REGEX | Regex khớp fully-qualified class name | Filter theo tên class/package |
CUSTOM | Implement TypeFilter interface | Logic bất kỳ (đọc annotation khác, kiểm tra bytecode) |
Khi nào dùng filter: 99% app không cần. Các case thực tế:
- Test slice:
excludeFiltersbỏ@Configurationproduction khi viết@WebMvcTesthay@DataJpaTest— Spring Boot tự làm trong các slice annotation, không cần tự config. - Module isolation: library muốn scan package riêng, không pull bean từ consumer package.
- Plugin architecture: chỉ scan class có
@MyPlugin, không scan bean bình thường. useDefaultFilters = falsecần thiết khi chỉ muốnincludeFilterscustom — nếu không set, default filter vẫn scan@Componentsong song, kết quả là scan cả hai.
Khi dùng includeFilters nhưng quên useDefaultFilters = false, container scan cả filter default (@Component) lẫn custom filter — kết quả thường nhiều bean hơn mong đợi. Luôn kiểm tra context bean count nếu dùng custom filter.
6. Meta-annotation — tự định nghĩa stereotype
Spring resolve annotation theo chain — annotation đặt trên annotation khác cũng được tính. Điều này cho phép tự tạo stereotype tổng hợp:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}
// Su dung:
@DomainService
public class OrderService { /* ... */ }
// Tuong duong viet @Service @Transactional tren class
@DomainService có @Service trong chain → được scan như @Component. Có @Transactional trong chain → được AOP proxy wrap cho transaction. @AliasFor map attribute value từ @DomainService sang @Service.value — giữ bean naming convention.
Spring dùng AnnotationUtils.findAnnotation() và MergedAnnotations API để traverse chain annotation bất kỳ độ sâu. Đây là cơ chế cho phép @SpringBootApplication = @SpringBootConfiguration + @EnableAutoConfiguration + @ComponentScan hoạt động như một annotation gộp.
Use case thực tế:
// DDD codebase: phan biet Application Service vs Domain Service
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional(readOnly = true) // query service: default read-only tx
public @interface ApplicationService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional // domain service: full read-write tx
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}
// Su dung:
@ApplicationService
public class OrderQueryService { // chi query, tx read-only
public Order findById(Long id) { /* ... */ }
}
@DomainService
public class OrderCommandService { // co write, tx read-write
public void placeOrder(Order o) { /* ... */ }
}
Quy tắc dùng meta-annotation: chỉ define khi pattern lặp lại từ 5 class trở lên. Codebase nhỏ hơn — explicit annotation dễ đọc hơn.
7. Pitfall thường gặp
Pitfall 1 — Bean ngoài basePackages không được scan.
@SpringBootApplication // @ComponentScan default: package cua class nay
public class OlhubApp { } // vi du: com.olhub.app
// SAI - bean trong package khac khong duoc scan
package com.olhub.plugin; // ngoai com.olhub.app
@Service
public class PluginService { } // KHONG duoc scan - container khong biet co no
// DUNG:
@SpringBootApplication
@ComponentScan(basePackages = {"com.olhub.app", "com.olhub.plugin"})
public class OlhubApp { }
Hoặc di chuyển OlhubApp lên package root com.olhub để mọi subpackage đều trong scan scope.
Pitfall 2 — Nhầm @Service cho class không phải bean Spring.
@Service // THUA - khong co dependency nao can inject
public class MathUtils {
public static int add(int a, int b) { return a + b; }
}
Class chỉ có static method không cần Spring quản. Bỏ annotation — gọi trực tiếp MathUtils.add(1, 2) từ mọi nơi, không cần inject.
Pitfall 3 — @Component cho class third-party.
// KHONG the annotate - ban khong sua source duoc
@Component
public class HikariDataSource { /* ... */ }
Class không thuộc quyền sở hữu của bạn → dùng @Bean method trong @Configuration. Đây là ranh giới chính xác giữa @Component và @Bean — xem @Bean & @Configuration.
Pitfall 4 — Quên @Repository, dùng @Component cho data layer.
@Component // co the dung nhung...
public class OrderJdbcRepository {
public Order findById(Long id) throws SQLException { /* ... */ }
}
// SQLException se KHONG bi translate thanh DataAccessException
// Business code buoc phai catch SQLException - coupling voi JDBC
@Repository // DUNG: exception translation hoat dong
public class OrderJdbcRepository {
public Order findById(Long id) { /* ... */ }
// SQLException tu dong convert -> DataAccessException
}
Liên hệ các bài khác
- BeanFactory vs ApplicationContext: scan tạo
BeanDefinitiontrongbeanDefinitionMap; bài kia giải thíchbeanDefinitionMaplà gì và tại sao nó tách khỏisingletonObjects. Hai bài đọc cùng nhau cho thấy toàn bộ đường đi từ annotation → metadata → object. - Resource & @SpringBootApplication:
@SpringBootApplicationbao gồm@ComponentScanvới basePackages mặc định — bài đó giải thích tại sao đặt main class ở root package là convention quan trọng, không phải tùy chọn. - @Bean & @Configuration: hai cách khai báo bean bổ sung nhau —
@Componentcho class bạn sở hữu,@Beancho class third-party hoặc cần factory logic. Bài tiếp theo đào sâu cơ chế CGLIB của@Configuration.
Tóm tắt
@Componentđánh dấu class để@ComponentScantìm thấy và tạoBeanDefinition(metadata) trongbeanDefinitionMap— chưa tạo object.- Scan dùng ASM đọc bytecode, không load class vào JVM — nhanh và không có side effect static initializer.
- 4 stereotype (
@Service,@Repository,@Controller,@RestController) đều kế thừa@Componentqua annotation chain — đều được scan. - Chỉ
@Repositorycó hành vi runtime đặc biệt:PersistenceExceptionTranslationPostProcessorwrap bean trong AOP proxy để translate exception JDBC/JPA →DataAccessException. @Controllerđược Spring MVC nhận diện để route@RequestMapping;@Servicelà marker thuần.- Tách stereotype có lý do kỹ thuật: AOP pointcut match chính xác theo annotation, exception translation, và tooling enforce layer rule.
@ComponentScancó 5 filter type (ANNOTATION,ASSIGNABLE_TYPE,ASPECTJ,REGEX,CUSTOM) — 99% app không cần, dùng khi test slice hoặc module isolation.- Meta-annotation cho phép tự define stereotype tổng hợp (
@DomainService=@Service+@Transactional); Spring resolve chain annotation bất kỳ độ sâu.
Tự kiểm tra
Q1Giải thích tại sao sau khi @ComponentScan chạy xong, object của OrderService chưa tồn tại trong heap. Điều gì thực sự xảy ra trong pha scan, và object được tạo khi nào?▸
@ComponentScan chạy xong, object của OrderService chưa tồn tại trong heap. Điều gì thực sự xảy ra trong pha scan, và object được tạo khi nào?Pha scan dùng ASM đọc bytecode của mỗi class file mà không load class vào JVM. Khi tìm class có @Component (hoặc stereotype), container chỉ tạo ScannedGenericBeanDefinition — một object metadata chứa tên class, scope, tên bean — và đăng ký vào beanDefinitionMap.
Object thật (OrderService instance) chỉ được tạo ở pha sau. Với ApplicationContext: eager tạo mọi singleton non-lazy lúc khởi động (trong refresh()). Với BeanFactory trần: lazy tạo khi getBean("orderService") được gọi lần đầu.
Lợi ích của tách 2 pha: các BeanFactoryPostProcessor có thể chỉnh sửa metadata (đổi scope, thêm property) trước khi bất kỳ object nào ra đời — đây là nền tảng của autoconfig và @ConditionalOn*.
Q2Tại sao nên dùng @Repository thay vì @Component cho lớp data access, dù container treat cả hai như bean thông thường? Cơ chế cụ thể nào chạy khi bạn dùng @Repository?▸
@Repository thay vì @Component cho lớp data access, dù container treat cả hai như bean thông thường? Cơ chế cụ thể nào chạy khi bạn dùng @Repository?Khi dùng @Repository, Spring tự register PersistenceExceptionTranslationPostProcessor — một BeanPostProcessor — wrap bean đó trong AOP proxy. Proxy intercept mọi method call và convert exception persistence-specific (JDBC SQLException, Hibernate HibernateException, JPA PersistenceException) thành DataAccessException hierarchy của Spring.
Lợi ích kỹ thuật: business code chỉ catch DataAccessException (abstraction Spring) thay vì exception của từng DB driver cụ thể. Đổi từ JDBC sang JPA không phải sửa catch clause trong service layer.
Nếu dùng @Component, exception translation không xảy ra tự động — service phải catch SQLException cứng, coupling chặt với implementation tầng data.
Q3AOP pointcut có thể viết @within(org.springframework.stereotype.Service) để target bean @Service. Tại sao điều này là lý do kỹ thuật để tách stereotype, không chỉ là "code style"? Cho ví dụ concrete.▸
@within(org.springframework.stereotype.Service) để target bean @Service. Tại sao điều này là lý do kỹ thuật để tách stereotype, không chỉ là "code style"? Cho ví dụ concrete.Annotation là metadata runtime (RetentionPolicy.RUNTIME) — có thể đọc qua reflection. AOP pointcut @within(Service) match class có annotation @Service trong chain. Nếu mọi class dùng @Component, pointcut phải match theo tên package hoặc tên class — giòn khi rename.
Ví dụ concrete: team muốn tự động log boundary của business layer — đặt aspect @Before("@within(Service)") thì chỉ method của service bean được log, repository và controller không bị ảnh hưởng. Không cần config thêm khi thêm service mới — annotation tự phân loại.
Tương tự, tool như ArchUnit có thể enforce rule "class trong package repository phải có @Repository" hoặc "service không import trực tiếp @Repository bean mà phải qua interface" — stereotype annotation là "khai báo intent" để tool kiểm tra tự động.
Q4Bạn viết @ComponentScan(includeFilters = @Filter(MyPlugin.class)) để chỉ scan class có @MyPlugin. Sau khi chạy app, container có rất nhiều bean hơn mong đợi. Nguyên nhân và cách sửa?▸
@ComponentScan(includeFilters = @Filter(MyPlugin.class)) để chỉ scan class có @MyPlugin. Sau khi chạy app, container có rất nhiều bean hơn mong đợi. Nguyên nhân và cách sửa?Nguyên nhân: quên set useDefaultFilters = false. Mặc định @ComponentScan luôn bật filter scan @Component (và tất cả stereotype). Khi thêm includeFilters mà không tắt default, container scan cả hai: mọi @Component trong package lẫn mọi class có @MyPlugin.
Cách sửa:
@ComponentScan(
basePackages = "com.olhub",
includeFilters = @Filter(
type = FilterType.ANNOTATION,
classes = MyPlugin.class
),
useDefaultFilters = false // chi scan theo includeFilters
)Với useDefaultFilters = false, chỉ class có @MyPlugin được scan. Nếu config class của bạn cũng trong package đó và dùng @Configuration + @Component gốc, phải thêm ASSIGNABLE_TYPE filter cho chúng, hoặc đặt config class ngoài basePackage.
Q5Team muốn mọi domain service đều có @Transactional và tên annotation rõ intent. Viết meta-annotation @DomainService với yêu cầu: scan được như @Component, có transaction, và @DomainService("customName") đặt bean name được. Giải thích cơ chế @AliasFor ở đây.▸
@Transactional và tên annotation rõ intent. Viết meta-annotation @DomainService với yêu cầu: scan được như @Component, có transaction, và @DomainService("customName") đặt bean name được. Giải thích cơ chế @AliasFor ở đây.Định nghĩa:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}Cơ chế @AliasFor: khi bạn viết @DomainService("orderSvc"), Spring dùng MergedAnnotations API để resolve annotation chain. @AliasFor(annotation = Service.class, attribute = "value") khai báo rằng attribute value của @DomainService là alias cho attribute value của @Service trong chain. Nhờ đó giá trị orderSvc được propagate xuống @Service.value → container dùng làm bean name.
Scan được: @DomainService có @Service trong chain → có @Component trong chain → @ComponentScan tìm thấy và tạo BeanDefinition.
Transaction: @Transactional trong chain → AOP proxy wrap bean cho transaction management.
Nên định nghĩa meta-annotation khi pattern lặp từ 5 class trở lên — codebase nhỏ hơn giữ explicit annotation dễ đọc hơn.
Bài tiếp theo: @Bean & @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