Backend

99 Ngày Spring — Ngày 06: @Configuration & @Bean

SSite Admin
3 tháng 08, 2026 4 phút đọc 47 lượt xem
99 Ngày Spring — Ngày 06: @Configuration & @Bean

Ngày 05 khép lại bằng một câu hỏi treo: máy quét chỉ thấy class của bạn — còn class của thư viện ngoài, nơi không thể mở source ra dán @Component, thì sao? Hôm nay là câu trả lời: @Configuration@Bean — đăng ký bean "thủ công" bằng Java thuần, và cũng là mảnh ghép cuối để mai này đọc hiểu auto-configuration của Spring Boot.

Sketchnote Ngày 06: @Configuration và @Bean — đăng ký bean thủ công

@Bean — công thức nấu bean bằng tay

@Configuration đánh dấu một class là "sổ công thức"; bên trong, mỗi method mang @Bean là một công thức: Spring gọi method và đưa giá trị trả về vào container như mọi bean khác. Còn chính config class thì ai tìm ra? Vẫn là component scan của Ngày 05 — @Configuration cũng là một @Component chuyên môn hóa, nên nó phải nằm dưới package gốc như thường lệ:

@Configuration            // class "sổ công thức" — bản thân nó cũng là beanpublic class AppConfig {

    @Bean                 // tên bean mặc định = tên method: "objectMapper"
    public ObjectMapper objectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.registerModule(new JavaTimeModule());
        return mapper;    // giá trị trả về được đưa vào container
    }
}
  • Tên bean mặc định = tên method (objectMapper) — muốn khác thì @Bean("tenKhac").

  • Với container, bean từ @Bean và bean từ @Component bình đẳng tuyệt đối — tiêm vào nơi khác bằng constructor y như Ngày 04.

  • Method @Bean chạy một lần lúc khởi động (scope singleton mặc định) — không phải mỗi lần ai đó cần bean.

Khi nào cần @Bean?

  • Class của thư viện ngoàiObjectMapper của Jackson, RestClient, DataSource… bạn không sửa được source của họ.

  • Khởi tạo cần logic — builder nhiều bước, đọc cấu hình, chọn implementation theo điều kiện.

  • Nhiều bean cùng kiểu với cấu hình khác nhau — ví dụ 2 RestClient trỏ 2 API.

@Configurationpublic class HttpConfig {

    @Bean
    public RestClient githubClient() {
        return RestClient.builder()            // class của thư viện —
            .baseUrl("https://api.github.com") // không thể mở source ra
            .build();                          // dán @Component được!
    }
}

Quy tắc nhớ nhanh: class của mình → dán nhãn (@Component/@Service… — Ngày 05); class của người khác hoặc cần logic khởi tạo → viết công thức @Bean.

Tiêm phụ thuộc giữa các @Bean

@Configurationpublic class ReportConfig {

    @Bean
    public ReportFormatter formatter() {
        return new ReportFormatter();
    }

    @Bean  // tham số được container TIÊM vào — như constructor injection
    public ReportService reportService(ReportFormatter formatter) {
        return new ReportService(formatter);
    }

    @Bean  // gọi thẳng formatter() cũng được — CGLIB chặn lời gọi
    public ReportPrinter reportPrinter() {
        return new ReportPrinter(formatter()); // → trả ĐÚNG bean singleton,
    }                                          //   không tạo mới!
}

Hai cách nối bean trong config: khai báo tham số cho method — container tự tiêm, đúng tinh thần constructor injection — hoặc gọi thẳng method @Bean khác. Cách thứ hai trông như "new mỗi lần gọi" nhưng không: Spring dựng một subclass CGLIB của config class, chặn mọi lời gọi method @Bean và trả về đúng bean singleton đang nằm trong container.

  • @Configuration(proxyBeanMethods = false) — "lite mode" — tắt phép thuật đó: gọi chéo method lúc này là new thật; Boot dùng nhiều trong auto-configuration để khởi động nhanh hơn.

  • Vì vậy hãy ưu tiên kiểu tham số: rõ ràng, không phụ thuộc proxy, chuyển sang lite mode không vỡ.

@Component vs @Bean — chọn cái nào?

  • @Component và họ hàng: dán lên class của bạn, máy quét tự tìm — mặc định cho code của chính mình.

  • @Bean trong @Configuration: công thức cho class ngoài, khởi tạo phức tạp, hoặc nhiều biến thể cùng kiểu.

  • Trong container hai loại không khác gì nhau: cùng vòng đời, cùng được tiêm, cùng là singleton mặc định.

Và một bí mật nho nhỏ: auto-configuration của Spring Boot (Ngày 02) thực chất là hàng trăm class @Configuration viết sẵn với các @Bean có điều kiện — bạn thêm starter nào, bộ công thức ấy kích hoạt. Ngày 10 ta sẽ mở hộp đen này bằng @Conditional.

Bài tập nhỏ

  • Tạo AppConfig với @Bean trả về ObjectMapper tùy chỉnh, tiêm vào một @Service và serialize thử một object có LocalDate.

  • Khai báo 2 bean RestClient cùng kiểu (2 baseUrl khác nhau) rồi tiêm theo kiểu — đọc kỹ lỗi Spring báo; thử sửa bằng tên bean trùng tên tham số.

  • In getBeanDefinitionNames() (Ngày 03) và tìm các bean sinh từ config — xác nhận tên mặc định đúng bằng tên method.

  • Đổi @Configuration thành @Component rồi gọi chéo 2 method @Bean — đoán xem còn là singleton không? (Gợi ý: @Component chạy lite mode — không có CGLIB.)

Kết luận

Giờ bạn có đủ hai con đường tạo bean: dán nhãn cho class của mình, viết công thức @Bean cho class của người khác — và hiểu vì sao gọi chéo method trong @Configuration vẫn ra singleton. Nhưng singleton là gì mà nhắc mãi? Ngày 07: bean scope & lifecycle — singleton/prototype/request, @PostConstruct & @PreDestroy. 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