Backend

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

SSite Admin
1 tháng 08, 2026 4 phút đọc 110 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 Spring — Ngày 14: Bean Validation — @Valid & bộ constraint chuẩn

Ngày 14 của 99 Ngày Spring: Bean Validation khai báo luật ngay trên DTO — @Valid kích hoạt, bộ constraint chuẩn (@NotBlank vs @NotEmpty vs @NotNull, @Size, @Email, @Min/@Max), bẫy int vs Integer, validate object lồng nhau không tự lan, và lỗi 400 gom một lượt.

10 thg 8, 20265 phút0
99 Ngày Java — Ngày 14: Kế thừa (Inheritance)

Ngày 14 của 99 Ngày Java: extends trao gia tài từ lớp cha, override với @Override và super., thứ tự khai sinh cha trước con sau qua super(...), protected trả nợ Ngày 13, và phép thử is-a vs has-a — khi nào nên composition thay vì kế thừa.

10 thg 8, 20266 phút0
99 Ngày Spring — Ngày 13: @RequestBody & DTO — đừng công khai entity ra API

Ngày 13 của 99 Ngày Spring: @RequestBody + Jackson deserialize JSON thành object, DTO là hợp đồng API — chặn lỗ hổng mass assignment chiều vào và rò rỉ dữ liệu chiều ra, request/response DTO khác nhau là bình thường, và Jackson annotations tinh chỉnh hợp đồng JSON.

9 thg 8, 20266 phút9