Stereotype Và @ComponentScan: Cơ Chế Quét Và Đăng Ký Bean
Tìm hiểu cơ chế @ComponentScan quét ASM bytecode tạo BeanDefinition, so sánh 4 Stereotype annotations và kỹ thuật tùy biến Filter cùng Meta-annotation.
TL;DR: @Component là annotation đánh dấu class để Spring quét và đưa vào container quản lý. Khi ứng dụng khởi động, @ComponentScan sử dụng thư viện ASM đọc bytecode trên classpath mà không cần nạp class vào JVM, sinh ra các BeanDefinition và đăng ký vào beanDefinitionMap. Trong 4 stereotype chuyên biệt, @Repository sở hữu khả năng tự động dịch ngoại lệ persistence qua AOP proxy, @Controller được Spring MVC nhận diện để định tuyến web, còn @Service đóng vai trò đánh dấu ngữ nghĩa nghiệp vụ.
Hiểu rõ cơ chế quét linh hoạt giúp bạn tổ chức mã nguồn chuẩn kiến trúc nhiều tầng và kiểm soát chính xác phạm vi nạp bean của hệ thống.
1. Cơ chế Component Scanning giải quyết vấn đề cấu hình nào?
Trước khi cơ chế quét tự động ra đời, mọi bean trong ứng dụng đều phải được khai báo thủ công trong các tệp XML hoặc phương thức cấu hình. Trong các hệ thống lớn với hàng trăm service và repository, lập trình viên phải khai báo lặp lại tên class hai lần, gây tốn thời gian và rất khó bảo trì:
// Truoc day: Phai khai bao thu cong tung bean trong file XML
// <bean id="orderService" class="com.olhub.service.OrderService"/>
// <bean id="paymentService" class="com.olhub.service.PaymentService"/>
// Hien tai: Khai bao stereotype truc tiep tren class
@Service
public class OrderService { /* ... */ }
@Service
public class PaymentService { /* ... */ }
Nhờ @Component và @ComponentScan, Spring tự động phát hiện các lớp được đánh dấu và đăng ký vào hệ thống theo quy ước (Convention-over-Configuration).
2. Spring quét ASM Bytecode và tạo BeanDefinition như thế nào?
Để tối ưu hiệu năng và tránh lãng phí bộ nhớ, @ComponentScan không sử dụng Java Reflection truyền thống. Thay vào đó, bộ xử lý ClassPathScanningCandidateComponentProvider sử dụng thư viện ASM để đọc metadata bytecode trực tiếp từ tệp .class mà không hề load class vào JVM:

Khi tìm thấy một class thỏa mãn điều kiện, Spring tạo một đối tượng ScannedGenericBeanDefinition và lưu vào beanDefinitionMap:
@Configuration
@ComponentScan(basePackages = "com.olhub")
public class AppConfig { }
Điều then chốt là sau khi pha scan kết thúc, đối tượng bean thật sự vẫn chưa hề tồn tại trên bộ nhớ heap. Spring chỉ mới ghi nhận metadata. Việc khởi tạo instance diễn ra ở pha sau (pha eager init của ApplicationContext).
3. Bốn Stereotype Annotation khác nhau ra sao về mặt kỹ thuật?
Bốn stereotype phổ biến thực chất đều là các alias mang annotation @Component bên trong annotation chain của chúng:
| Annotation | Tầng kiến trúc | Hành vi runtime đặc biệt |
|---|---|---|
@Component | Chung (Generic) | Không có xử lý phụ |
@Service | Tầng nghiệp vụ (Business) | Marker thuần túy, thể hiện ngữ nghĩa |
@Repository | Tầng truy cập dữ liệu (Data) | Kích hoạt bộ dịch ngoại lệ cơ sở dữ liệu |
@Controller | Tầng giao diện web (MVC) | Được DispatcherServlet nhận diện để định tuyến |
@RestController | Tầng API REST | Tương đương @Controller kết hợp @ResponseBody |
Cơ chế Exception Translation của @Repository
@Repository là stereotype duy nhất khiến bean bị bọc thêm một proxy dịch exception (@Controller cũng có hành vi runtime, nhưng là được Spring MVC nhận diện để đăng ký route, không qua proxy). Mỗi hệ quản trị hay ORM ném ra các ngoại lệ riêng biệt (Hibernate ném HibernateException, JDBC ném SQLException). Spring sử dụng PersistenceExceptionTranslationPostProcessor để bọc bean @Repository trong một Dynamic Proxy:

@Repository
public class OrderRepository {
public Order findById(Long id) {
// Neu JPA/Hibernate nem PersistenceException -> Proxy tu dong convert thanh DataAccessException
return entityManager.find(Order.class, id);
}
}
Nhờ đó, tầng Service chỉ cần bắt ngoại lệ DataAccessException của Spring mà không bị phụ thuộc vào thư viện kết nối cơ sở dữ liệu cụ thể.
4. Vì sao việc phân chia Stereotype theo tầng lại mang tính sống còn?
Nhiều lập trình viên cho rằng chỉ cần dùng @Component cho tất cả mọi class. Tuy nhiên, việc phân tách mang lại ba giá trị kỹ thuật cốt lõi:
- AOP Pointcut chính xác theo tầng: Cho phép gắn các Aspect quản lý transaction hoặc ghi log thanh tra chỉ riêng cho các bean nghiệp vụ:
Java
@Around("@within(org.springframework.stereotype.Service)") public Object monitorServicePerformance(ProceedingJoinPoint joinPoint) throws Throwable { // Chi chan cac class duoc danh dau @Service return joinPoint.proceed(); } - Kích hoạt tự động Exception Translation: Như đã đề cập ở
@Repository. - Kiểm thử kiến trúc tĩnh (Architecture Linter): Các công cụ như ArchUnit có thể tự động kiểm tra quy tắc: "Lớp thuộc Controller không được gọi trực tiếp Repository mà bắt buộc phải qua Service".
5. Làm thế nào để kiểm soát phạm vi quét bằng Filter Modes?
Mặc định @ComponentScan sẽ quét tất cả các lớp có @Component. Bạn có thể tùy biến hành vi này thông qua bộ lọc includeFilters và excludeFilters:
@Configuration
@ComponentScan(
basePackages = "com.olhub",
includeFilters = {
@ComponentScan.Filter(type = FilterType.ANNOTATION, classes = CustomPlugin.class)
},
excludeFilters = {
@ComponentScan.Filter(type = FilterType.REGEX, pattern = ".*TestService")
},
useDefaultFilters = false // Tat bo loc mac dinh @Component
)
public class CustomScanConfig { }
Spring hỗ trợ 5 kiểu bộ lọc:
ANNOTATION: Khớp theo annotation trên class.ASSIGNABLE_TYPE: Khớp theo class cụ thể hoặc interface kế thừa.REGEX: Khớp theo biểu thức chính quy của tên class.ASPECTJ: Khớp theo biểu thức pointcut của AspectJ.CUSTOM: Hiện thực giao diệnTypeFilterđể tự viết logic lọc tùy ý.
Khi sử dụng includeFilters, nếu bạn không cấu hình useDefaultFilters = false, Spring vẫn sẽ quét toàn bộ các bean @Component thông thường song song với bộ lọc mới của bạn.
6. Meta-Annotation hoạt động ra sao và ứng dụng thế nào trong thực tế?
Cơ chế phân giải của Spring cho phép bạn đặt annotation lên trên một annotation khác để tạo ra một Stereotype tổng hợp cho dự án:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Service
@Transactional
public @interface DomainService {
@AliasFor(annotation = Service.class, attribute = "value")
String value() default "";
}
Khi sử dụng @DomainService trên một class, Spring sử dụng MergedAnnotations để duyệt cây annotation, nhận diện class vừa là một @Component (được quét vào container), vừa tự động áp dụng proxy giao dịch từ @Transactional.
7. Những cạm bẫy thường gặp nhất khi sử dụng Component Scanning?
- Bean nằm ngoài basePackages: Nếu bạn đặt class ở package ngang hàng hoặc cha của class chứa
@SpringBootApplication, Spring sẽ bỏ qua class đó. Hãy chuyển class chính về root package hoặc chỉ định rõbasePackages. - Gắn stereotype cho các Utility Class: Các class tiện ích chỉ chứa các phương thức static không có trạng thái hay phụ thuộc thì không nên đưa vào Spring Container quản lý.
- Dùng @Component cho thư viện bên thứ ba: Bạn không thể chỉnh sửa mã nguồn của thư viện ngoài để thêm annotation. Với các đối tượng này, hãy sử dụng phương thức
@Beantrong@Configuration.
Tóm tắt
@ComponentScansử dụng thư viện ASM đọc trực tiếp bytecode để tạoBeanDefinitionmà không cần nạp class vào JVM.- Quá trình quét chỉ tạo metadata trong
beanDefinitionMap; đối tượng bean thật chỉ được sinh ra trong pha khởi tạo sau đó. @Repositorytự động dịch các ngoại lệ cấp thấp của cơ sở dữ liệu thànhDataAccessExceptionthông qua AOP proxy.- Phân tách Stereotype giúp xác định rõ ranh giới kiến trúc, hỗ trợ AOP pointcut và công cụ kiểm tra tự động.
- Meta-annotation kết hợp
@AliasForgiúp gom nhóm cấu hình và tạo ra các định danh rõ nghĩa theo từng domain.
Tự kiểm tra
- Q1Tại sao sau khi @ComponentScan hoàn tất việc quét classpath, các đối tượng Service vẫn chưa hề tồn tại trong bộ nhớ Heap?
- Q2Điểm khác biệt kỹ thuật quan trọng nhất giữa @Repository và @Service trong Spring là gì?
- Q3Lợi ích lớn nhất của việc phân chia rõ ràng các Stereotype (@Service, @Repository, @Controller) thay vì chỉ dùng duy nhất @Component là gì?
- Q4Điều gì xảy ra nếu bạn chỉ định includeFilters trong @ComponentScan nhưng quên thiết lập useDefaultFilters = false?
- Q5Trong tình huống nào sau đây bạn bắt buộc phải dùng phương thức @Bean thay vì gắn @Component lên trên class?
Bài tiếp theo: @Bean & @Configuration: Cơ Chế CGLIB Full Và Lite Mode
Bài này có giúp bạn hiểu bản chất không?
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.
Tổng Quan Module 04: Cấu Hình Và Khai Báo Spring Bean
Bài tiếp@Bean & @Configuration: Cơ Chế CGLIB Full Và Lite Mode
Lộ trình Junior Java Backend Developer
Chặng 13/21 · Spring IoC, DI & Bean — bài 14/17 của chặng
Thảo luận & góp ý
Hỏi đáp về bài này