Spring Core & Boot
22/41
Bài 22 / 41~12 phútConfiguration & Bean DeclarationLộ trình · chặng 13/21Miễn phí… lượt xem

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ì:

Java
// 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:

Hai cột: pha ComponentScan kết thúc ở một entry trong beanDefinitionMap, pha sau mới là nơi createBean chạy constructor

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:

Java
@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:

AnnotationTầng kiến trúcHành vi runtime đặc biệt
@ComponentChung (Generic)Không có xử lý phụ
@ServiceTầng nghiệp vụ (Business)Marker thuần túy, thể hiện ngữ nghĩa
@RepositoryTầng truy cập dữ liệu (Data)Kích hoạt bộ dịch ngoại lệ cơ sở dữ liệu
@ControllerTầng giao diện web (MVC)Được DispatcherServlet nhận diện để định tuyến
@RestControllerTầng API RESTTươ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:

Hai cột đối chiếu đường đi của exception JPA: không có proxy thì service phải catch PersistenceException, có proxy Repository thì service chỉ catch DataAccessException

Java
@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:

  1. 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();
    }
    
  2. Kích hoạt tự động Exception Translation: Như đã đề cập ở @Repository.
  3. 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:

Java
@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ện TypeFilter để tự viết logic lọc tùy ý.
Cạm bẫy useDefaultFilters

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:

Java
@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 @Bean trong @Configuration.

Tóm tắt

  • @ComponentScan sử dụng thư viện ASM đọc trực tiếp bytecode để tạo BeanDefinition mà 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 đó.
  • @Repository tự động dịch các ngoại lệ cấp thấp của cơ sở dữ liệu thành DataAccessException thô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 @AliasFor giú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

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Tạ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?
  2. Q2
    Điểm khác biệt kỹ thuật quan trọng nhất giữa @Repository và @Service trong Spring là gì?
  3. Q3
    Lợ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ì?
  4. 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?
  5. Q5
    Trong 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.

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

Bài tiếp theo

@Bean & @Configuration: Cơ Chế CGLIB Full Và Lite Mode