Backend

99 Ngày Spring — Ngày 05: @Component & component scan

SSite Admin
1 tháng 08, 2026 4 phút đọc 185 lượt xem
99 Ngày Spring — Ngày 05: @Component & component scan

Ngày 03 ta biết bean sống trong container, Ngày 04 biết container tiêm bean vào nhau. Còn một mắt xích đầu tiên chưa trả lời kỹ: container tìm thấy các class của bạn bằng cách nào? Câu trả lời là component scan — cùng bộ stereotype annotations (@Component, @Service, @Repository…) và lỗi kinh điển nhất Spring Boot: bean nằm ngoài package được quét.

Sketchnote Ngày 05: Component scan và stereotype annotations

Component scan là gì?

Khi ứng dụng khởi động, Spring quét (scan) các package để tìm class mang annotation đánh dấu bean, đọc thành bean definition rồi đưa vào container — đúng bước "đọc định nghĩa" trong vòng đời bean Ngày 03. Bạn không đăng ký từng bean bằng tay; bạn chỉ dán nhãn lên class và để máy quét làm việc.

Bộ stereotype annotations

@Component   // gốc: "class này là bean, quản lý giúp tôi"public class PriceCalculator { ... }

@Service     // = @Component + ngữ nghĩa "tầng nghiệp vụ"public class OrderService { ... }

@Repository  // = @Component + dịch exception DB sang DataAccessExceptionpublic class OrderRepository { ... }

@RestController // = @Controller + @ResponseBody — tầng web (Ngày 11)public class OrderController { ... }

Về mặt kỹ thuật, cả 4 đều tạo bean như nhau — @Service, @Repository, @Controller đều là @Component chuyên môn hóa. Vậy sao không dùng @Component cho tất cả?

  • Ngữ nghĩa — nhìn annotation biết ngay class thuộc tầng nào: nghiệp vụ, dữ liệu hay web. Code là để người đọc.

  • @Repositoryphép thuật riêng: dịch exception đặc thù của JDBC/JPA thành họ DataAccessException thống nhất của Spring.

  • Các công cụ (AOP, slice test như @WebMvcTest…) lọc bean theo đúng stereotype này.

Quy tắc vàng: bean phải nằm dưới gốc

@SpringBootApplication (Ngày 02) chứa sẵn @ComponentScan không tham số — nghĩa là: quét từ package chứa main class trở xuống. Mọi bean nằm trong "cây" đó được thấy; nằm ngoài là vô hình:

com.example.demo
├── DemoApplication.java   ← @SpringBootApplication ở GỐC
├── order
│   ├── OrderService.java        ✓ được quét
│   └── OrderRepository.java     ✓ được quét
└── payment
    └── PaymentService.java      ✓ được quét

com.example.util
└── LegacyHelper.java            ✗ NGOÀI cây gốc — vô hình!

Triệu chứng của bean vô hình: khởi động fail với required a bean of type ... that could not be found — hoặc tệ hơn, endpoint trả 404 mà không ai báo lỗi. Gặp lỗi "bean biến mất", việc đầu tiên hãy nhìn cây package, đừng vội nghi ngờ code.

Sửa thế nào khi bean nằm ngoài?

// Cách 1 (nên dùng): chuyển package vào dưới gốc// com.example.util → com.example.demo.util

// Cách 2: khai báo quét thêm — nhớ liệt kê CẢ gốc cũ!@SpringBootApplication@ComponentScan(basePackages = {
    "com.example.demo",   // quên dòng này là mất sạch bean chính
    "com.example.util"
})public class DemoApplication { ... }

Cách 1 gần như luôn đúng — cấu trúc chuẩn của Spring Boot là main class ở package gốc, mọi thứ nằm dưới. Cách 2 dành cho tình huống bất khả kháng (thư viện nội bộ, code legacy) và mang theo cái bẫy trứ danh: khai basePackagesthay thế chứ không phải bổ sung — quên liệt kê gốc cũ là toàn bộ bean chính biến mất theo.

Tên bean & lọc quét

@Service("checkout")        // tự đặt tên beanpublic class OrderService { ... }

// Mặc định: tên class viết thường chữ đầu// OrderService → "orderService"

Tên bean hiếm khi phải đụng tới — nó quan trọng khi hai bean cùng kiểu cần phân biệt (chuyện của @Qualifier, ta gặp lại khi bàn về nhiều implementation). @ComponentScan còn có includeFilters/excludeFilters để lọc tinh vi — biết là đủ, thực tế rất hiếm dùng.

Bài tập nhỏ

  • Tạo PriceCalculator với @Component trong package con, tiêm vào một @Service bằng constructor (Ngày 04) và in kết quả.

  • Cố tình chuyển PriceCalculator ra package ngang hàng với gốc, chạy lại và đọc kỹ thông báo lỗi — rồi sửa bằng cả 2 cách.

  • Đổi @Component thành @Service rồi @Repository — chạy lại: có gì khác? (Gợi ý: hôm nay thì chưa — ngữ nghĩa là cho người đọc và công cụ.)

  • In mọi tên bean chứa chữ "price" bằng getBeanDefinitionNames() (Ngày 03) để thấy tên mặc định.

Kết luận

Bạn đã hiểu máy quét của Spring: stereotype annotations dán nhãn class, @ComponentScan quét từ gốc trở xuống, và quy tắc vàng "main class ở gốc, bean nằm dưới". Nhưng còn class của thư viện ngoài — nơi bạn không thể dán @Component lên — thì sao? Ngày 06: @Configuration@Bean — đăng ký bean thủ công. Hẹn gặp lại!

S

Site Admin

Engineer and writer. Building things with TypeScript and distributed systems.

Bình luận (0)

Bạn cần đăng nhập bằng Google để bình luận.

Hãy là người bình luận đầu tiên.

Bài viết liên quan

99 Ngày Java — Ngày 58: Optional

Optional là công cụ cho kiểu trả về — không field, không tham số, không bọc collection; chuỗi map/flatMap/filter thay kim tự tháp if; orElse luôn tính tham số còn orElseGet lười, orElseThrow cho “không có là lỗi” — và các anti-pattern isPresent + get, orElse(null).

23 thg 9, 20269 phút9
99 Ngày Spring — Ngày 58: Spring Boot Actuator

Boot 3 mặc định chỉ lộ health; bản đồ endpoint an toàn và nguy hiểm; HealthIndicator tự viết với liveness tách khỏi readiness cho Kubernetes, info từ build và git, metrics Micrometer với tag hữu hạn — và ba lớp khóa: cổng riêng, SecurityFilterChain với EndpointRequest, exclude env/heapdump/threaddump.

23 thg 9, 202610 phút5
So sánh EasyExcel và Apache Fesod: khác biệt thật nằm ở đâu?

Fesod là hậu duệ của EasyExcel, cùng engine SAX/SXSSF. Khác biệt kiểm chứng được: vòng đời dự án, POI 5.2.5 vs 5.5.1, các API mới (numRows, csv(), headerMergeStrategy) và độ bền với file xấu — không phải hiệu năng gấp nhiều lần.

23 thg 9, 20268 phút1