Trọn bộ các annotation cơ bản cần phải biết trong Spring Boot (Phần 1)
"Hãy quay ngược thời gian một chút về thời kỳ 'đồ đá' của Java Enterprise. Nếu bạn đã từng phải vật lộn với những dự án di sản (legacy), từng mệt nhoài khi phải gom góp các thư viện tiện ích rồi tự tay đấu nối hàng ngàn dòng cấu hình XML cồng kềnh, bạn sẽ hiểu cảm giác nghẹt thở khi hệ thống phình to. Rồi Spring Boot xuất hiện, mang theo một cuộc cách mạng thực sự. Và thứ tạo nên 'phép màu' lật ngược thế cờ đó, không gì khác, chính là các Annotation.
Chỉ bằng vài ký tự nhỏ bé bắt đầu với dấu @, hàng trăm dòng code boilerplate (mã lặp) bay màu, các Design Pattern phức tạp được kích hoạt tự động, và luồng Dependency Injection tự do chảy trơn tru trong hệ thống. Nhưng sự ma thuật nào cũng có cái giá của nó. Nếu chỉ dùng theo thói quen copy-paste mà không hiểu rõ cơ chế ngầm, hệ thống của bạn sẽ sớm biến thành một 'hộp đen' đầy rẫy những lỗi khởi tạo không lường trước. Hôm nay, hãy cùng mình đi sâu vào 'giải phẫu' trọn bộ các Annotation cốt lõi nhất, để biến phép màu của Spring Boot thành công cụ nằm trong tầm kiểm soát tuyệt đối của bạn."
Phần I. Core & Cấu hình (Cốt lõi)
1. @SpringBootApplication
@SpringBootApplication là annotation quan trọng nhất và mang tính biểu tượng nhất của Spring Boot. Nó thường được đặt ở class chứa hàm main() để khởi động ứng dụng.
Về bản chất, @SpringBootApplication là một meta-annotation (một annotation được tạo thành từ việc gộp nhiều annotation khác lại với nhau). Việc hiểu sâu về nó đòi hỏi chúng ta phải "mổ xẻ" 3 annotation cốt lõi cấu thành nên nó, cũng như cơ chế tùy biến đi kèm.
Nếu xem mã nguồn của @SpringBootApplication, bạn sẽ thấy nó được cấu tạo chủ yếu từ 3 annotation của Spring Framework:
@SpringBootApplication // Annotation này bao gồm luôn 3 annotation bên trên
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
1.1 @Configuration
Khi bạn gắn @Configuration (hoặc @SpringBootConfiguration) lên một class, Spring sẽ biết rằng class này chứa các phương thức trả về các object cần được quản lý (các @Bean).
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class DatabaseConfig {
// Spring sẽ gọi method này, lấy object trả về và lưu vào IoC Container
@Bean
public DataSource dataSource() {
return new DataSource("localhost", 3306, "my_db");
}
@Bean
public UserRepository userRepository() {
// Truyền bean dataSource vào userRepository
return new UserRepository(dataSource());
}
}
Điều kỳ diệu của @Configuration (CGLIB Proxying)
Điểm đặc biệt nhất của @Configuration so với các annotation thông thường là nó sử dụng CGLIB Proxy để bọc class của bạn lại. Điều này đảm bảo tính chất Singleton của các Bean.
Hãy nhìn lại ví dụ trên, ở method userRepository(), chúng ta gọi dataSource().
Nếu đây là Java thuần, mỗi lần gọi dataSource(), một object DataSource mới sẽ được tạo ra. Tuy nhiên, nhờ @Configuration, Spring sẽ can thiệp vào:
- Nó kiểm tra xem Bean
dataSourceđã tồn tại trong Container chưa. - Nếu đã có, nó sẽ trả về instance cũ thay vì chạy lại đoạn code
new DataSource(...). - Nhờ đó, cả ứng dụng của bạn sẽ chỉ dùng chung một object
DataSourceduy nhất.
Những lưu ý bắt buộc (Best Practices)
Để @Configuration hoạt động đúng, class của bạn phải tuân thủ các quy tắc sau:
- Không được là
final: Spring cần tạo class con (subclass) bằng CGLIB để làm Proxy, nên class không được đánh dấu là final. - Không được tạo các method @Bean là private hoặc final: Tương tự, Proxy cần có khả năng ghi đè (override) các method này.
- Phải có constructor mặc định (no-args constructor): Mặc dù các phiên bản Spring mới đã linh hoạt hơn, nhưng việc giữ một constructor rỗng là an toàn nhất để CGLIB có thể khởi tạo.
1.2 @EnableAutoConfiguration
Trong Spring Boot, @EnableAutoConfiguration chính là linh hồn tạo nên sự khác biệt giữa Spring Boot và Spring Framework truyền thống. Nó chịu trách nhiệm cho "phép thuật" tự động cấu hình ứng dụng dựa trên các thư viện (dependencies) mà bạn thêm vào dự án.
Nói một cách đơn giản: Thay vì bạn phải tự tay viết hàng tá các class @Configuration để cài đặt database, web server, hay bảo mật, @EnableAutoConfiguration sẽ "đoán" những gì bạn cần và tự động cấu hình chúng.
Bản chất của "Phép thuật"
Khi ứng dụng khởi chạy, @EnableAutoConfiguration sẽ quét toàn bộ Classpath (các thư viện có trong file pom.xml hoặc build.gradle) để xem bạn đang dùng công nghệ gì.
- Ví dụ 1: Nếu nó thấy thư viện spring-boot-starter-web và tomcat-embed-core trên classpath, nó sẽ tự động cấu hình một Web Server (Tomcat) và cài đặt DispatcherServlet để bạn có thể viết các REST API ngay lập tức.
- Ví dụ 2: Nếu nó thấy spring-boot-starter-data-jpa và thư viện driver của MySQL, nó sẽ tự động tạo ra các Bean như DataSource, EntityManagerFactory, và TransactionManager để bạn kết nối cơ sở dữ liệu mà không cần viết code cấu hình.
Cách hoạt động bên dưới (Under the hood)
Thực chất, @EnableAutoConfiguration không phải là ma thuật, nó tuân theo một quy trình rất logic:
-
Tìm kiếm các cấu hình mặc định: Spring Boot chứa sẵn hàng trăm class @Configuration được viết bởi các kỹ sư của Spring. Nó sẽ tìm danh sách các class này trong file
META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(ở các bản Spring Boot cũ hơn 2.7 là file spring.factories). -
Sàng lọc bằng @Conditional: Không phải class cấu hình nào cũng được nạp. Spring Boot sử dụng các annotation như @ConditionalOnClass (chỉ nạp khi có thư viện này), @ConditionalOnMissingBean (chỉ nạp nếu developer chưa tự định nghĩa Bean này).
-
Tôn trọng cấu hình của bạn: Nguyên tắc của Spring Boot là "Ý của lập trình viên là cao nhất". Nếu bạn tự viết một @Bean cho
DataSource, Spring Boot sẽ lùi lại và không nạpDataSourcetự động của nó nữa.
Cách tắt một Auto-Configuration cụ thể (exclude)
Trong thực tế, ngày nay bạn sẽ rất hiếm khi thấy ai viết @EnableAutoConfiguration một cách độc lập. Lý do như các bạn đã biết vì nó đã được gói gọn bên trong annotation @SpringBootApplication mà chúng ta hay đặt ở class hàm main.
Đôi khi, sự thông minh của Spring Boot lại gây rắc rối. Giả sử bạn thêm thư viện JPA vào project nhưng chưa kịp cài đặt thông tin database trong file application.properties. Khi chạy ứng dụng, Spring Boot sẽ cố gắng tự động kết nối DB và báo lỗi crash app (lỗi Failed to configure a DataSource).
Để giải quyết, bạn có thể "tắt" tính năng tự động cấu hình cho một module cụ thể bằng thuộc tính exclude:
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
// Tắt tự động cấu hình DataSource
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
1.3 @ComponentScan
Trong Spring Boot, @ComponentScan đóng vai trò như một chiếc "radar". Nhiệm vụ của nó là nói cho Spring biết phải đi tìm các class cấu hình và các thành phần (Bean) ở đâu để đưa vào quản lý trong Spring IoC Container.
Nếu @Configuration là nơi bạn tự tay định nghĩa Bean, @EnableAutoConfiguration là nơi Spring tự động nạp Bean từ thư viện, thì @ComponentScan là cách Spring tự động quét và thu thập code do chính bạn viết.
Chiếc radar này tìm kiếm những gì?
Khi được kích hoạt, @ComponentScan sẽ đi qua các thư mục (package) và tìm kiếm các class được gắn một trong các "stereotype annotation" sau để biến chúng thành Bean:
- @Component: Đánh dấu một class Java thông thường là một Bean.
- @Service: Đánh dấu tầng xử lý nghiệp vụ (Business Logic).
- @Repository: Đánh dấu tầng giao tiếp với cơ sở dữ liệu (Data Access Object).
- @Controller / @RestController: Đánh dấu tầng tiếp nhận các request (Web/API).
- @Configuration: Các class chứa cấu hình do bạn viết.
(Lưu ý: Thực chất @Service, @Repository, @Controller đều là các lớp con được kế thừa từ @Component).
Cách hoạt động mặc định (Cực kỳ quan trọng)
Giống như @EnableAutoConfiguration, bạn hiếm khi phải tự viết @ComponentScan vì nó đã được tích hợp sẵn bên trong @SpringBootApplication ở class chạy hàm main.
Theo mặc định, @ComponentScan sẽ quét package chứa class khai báo nó và toàn bộ các sub-packages (package con) nằm bên trong.
Ví dụ về cấu trúc chuẩn:
com.myproject.app
├── MyApplication.java <-- Class chứa @SpringBootApplication (@ComponentScan sẽ quét từ đây)
├── controller/
│ └── UserController.java <-- Sẽ được quét (vì là package con)
├── service/
│ └── UserService.java <-- Sẽ được quét
└── repository/
└── UserRepository.java <-- Sẽ được quét
Lỗi "Kinh điển" của người mới học
Dựa vào quy tắc mặc định ở trên, một lỗi cực kỳ phổ biến mà các lập trình viên mới gặp phải là báo lỗi "Field required a bean of type '...' that could not be found" (Không tìm thấy Bean). Lý do thường là vị trí đặt class main bị sai.
Ví dụ về cấu trúc LỖI:
com.myproject.app
├── controller/
│ └── UserController.java <-- LỖI: Sẽ KHÔNG được quét!
├── service/
│ └── UserService.java <-- LỖI: Sẽ KHÔNG được quét!
└── main/
└── MyApplication.java <-- Class Main nằm "sâu" trong một package riêng. Radar chỉ quét thư mục 'main' và các thư mục con của 'main'.
Cách khắc phục: Luôn đặt class chứa @SpringBootApplication ở package ngoài cùng (root package) của dự án.
Cách cấu hình thủ công @ComponentScan (basePackages)
Nếu bạn bắt buộc phải để các component ở một package hoàn toàn khác, hoặc bạn đang làm việc trong một dự án đa module (multi-module) nơi các class nằm ở ngoài root package, bạn có thể chỉ định rõ ràng cho radar biết cần quét ở đâu bằng thuộc tính basePackages:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ComponentScan;
// Radar sẽ quét package mặc định VÀ thêm một package bên ngoài dự án
@SpringBootApplication
@ComponentScan(basePackages = {"com.myproject.app", "com.external.library.components"})
public class MyApplication {
public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}
2. @Bean
Trong Spring Boot, @Bean là một annotation được sử dụng ở cấp độ phương thức (method-level). Nó mang thông điệp báo cho Spring IoC Container biết rằng: "Hãy chạy phương thức này, lấy object (đối tượng) mà nó trả về và lưu vào Container để quản lý".
Để khai báo @Bean, phương thức của bạn bắt buộc phải nằm bên trong một class cấu hình (được đánh dấu bằng @Configuration hoặc @Component - khai báo bên dưới mỗi annotation này cũng sẽ có sự khác nhau nhất định về cách Spring quản lý Bean, chúng ta sẽ tìm hiểu ở phần sau nhé).
Cú pháp sử dụng cơ bản:
@Configuration
public class NetworkConfig {
// Spring sẽ gọi hàm này, lấy đối tượng RestTemplate và đặt tên Bean là "restTemplate"
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Theo mặc định, tên của phương thức sẽ chính là tên của Bean trong Container (như ví dụ trên, tên Bean là restTemplate).
2.1. Các thuộc tính hữu ích của @Bean
Bản thân annotation @Bean cung cấp một số thuộc tính để bạn kiểm soát chính xác cách Bean đó được tạo ra và quản lý:
a. name (hoặc value) - Đổi tên Bean
Nếu bạn không muốn sử dụng tên phương thức làm tên Bean, hoặc muốn gán nhiều tên (alias) cho cùng một Bean, bạn sử dụng thuộc tính name.
// Đặt một tên mới
@Bean(name = "myCustomClient")
public HttpClient httpClient() {
return new HttpClient();
}
// Đặt nhiều tên (aliases) cho cùng một Bean
@Bean(name = {"databaseConfig", "dbConfig", "primaryDb"})
public DataSource dataSource() {
return new DataSource();
}
b. initMethod - Can thiệp ngay sau khi tạo
Thuộc tính này cho phép bạn chỉ định tên của một phương thức bên trong object. Spring sẽ tự động gọi phương thức này ngay sau khi đối tượng được khởi tạo xong và mọi dependencies đã được truyền vào. Thường dùng để mở kết nối, nạp dữ liệu ban đầu,...
@Bean(initMethod = "startConnection")
public RedisClient redisClient() {
return new RedisClient();
// Sau khi new RedisClient() chạy xong, Spring sẽ gọi tiếp redisClient.startConnection()
}
c. destroyMethod - Can thiệp trước khi hủy
Ngược lại với initMethod, thuộc tính này chỉ định một phương thức sẽ được gọi ngay trước khi Bean bị hủy (thường là lúc ứng dụng Spring Boot tắt). Dùng để đóng kết nối database, giải phóng bộ nhớ, xóa file tạm,...
// Spring sẽ tự động gọi fileManager.closeAllFiles() trước khi app tắt
@Bean(destroyMethod = "closeAllFiles")
public FileManager fileManager() {
return new FileManager();
}
(Lưu ý: Với các class có sẵn hàm close() hoặc shutdown(), Spring thường tự động nhận diện và biến chúng thành destroyMethod mặc định).
2.2 Dependency Injection giữa các @Bean
Điều gì xảy ra nếu object bạn đang tạo cần một đối tượng khác để hoạt động? Bạn chỉ cần khai báo đối tượng cần thiết dưới dạng tham số của phương thức @Bean. Spring sẽ tự động tìm Bean đó trong Container và truyền (inject) vào cho bạn.
@Configuration
public class PaymentConfig {
// Tạo Bean thứ nhất
@Bean
public PaymentGateway paymentGateway() {
return new StripePaymentGateway();
}
// Tạo Bean thứ hai cần sử dụng Bean thứ nhất
@Bean
public PaymentService paymentService(PaymentGateway gateway) {
// Spring tự động truyền (inject) paymentGateway vào tham số gateway
return new PaymentService(gateway);
}
}
3. @PropertySource
Trong Spring Boot, mặc định mọi cấu hình của hệ thống (từ port chạy server, kết nối Database, cho đến khóa bí mật) đều được "nhồi nhét" vào một file duy nhất là application.properties (hoặc application.yml).
Tuy nhiên, khi dự án phình to, file này có thể dài đến hàng ngàn dòng, trở thành một mớ hỗn độn không thể bảo trì. Lúc này, @PropertySource xuất hiện như một vị cứu tinh. Nó cho phép bạn nạp thêm các file cấu hình bên ngoài (Custom Properties Files) vào hệ thống của Spring.
1. Cách sử dụng cơ bản (Chia để trị)
Giả sử bạn muốn tách toàn bộ cấu hình liên quan đến gửi Email ra một file riêng có tên là mail-config.properties (đặt trong thư mục src/main/resources).
Nội dung file mail-config.properties:
mail.host=smtp.gmail.com
mail.port=587
mail.username=admin@example.com
Cách nạp vào Spring Boot:
Bạn tạo một class cấu hình, gắn @Configuration và sử dụng @PropertySource trỏ đến đường dẫn file đó.
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.beans.factory.annotation.Value;
@Configuration
// Dùng tiền tố "classpath:" để báo Spring tìm file trong thư mục resources
@PropertySource("classpath:mail-config.properties")
public class MailConfig {
// Nạp thành công thì bạn có thể dùng @Value để lấy dữ liệu ra như bình thường
@Value("${mail.username}")
private String username;
@Value("${mail.port}")
private int port;
// ...
}
2. Nạp file từ bên ngoài thư mục dự án (External File)
Trong thực tế, các thông tin nhạy cảm (như mật khẩu Database, API Key) hiếm khi được code cứng vào file nằm trong source code (để tránh bị lộ khi đẩy code lên Github). Thay vào đó, file config sẽ được đặt ở một thư mục bí mật trên máy chủ (Server).
Lúc này, bạn đổi tiền tố từ classpath: thành file: để trỏ ra ngoài ổ cứng.
@Configuration
// Trỏ thẳng ra file nằm trên ổ đĩa của Server (Linux/Windows)
@PropertySource("file:/etc/secrets/my-app-db.properties")
public class DatabaseConfig {
// ...
}
3. Hai "cạm bẫy" lớn nhất trong thực chiến
Cạm bẫy 1: Ứng dụng sập vì không tìm thấy file
Mặc định, nếu file bạn chỉ định trong @PropertySource bị xóa hoặc đổi tên, Spring Boot sẽ ném ra lỗi FileNotFoundException và làm sập (crash) ứng dụng ngay khi khởi động.
Để an toàn, nếu đó là một file cấu hình không bắt buộc, bạn hãy bật thuộc tính ignoreResourceNotFound.
// Nếu file không tồn tại, Spring sẽ bỏ qua một cách êm ái, app vẫn chạy bình thường.
@PropertySource(value = "classpath:optional-config.properties", ignoreResourceNotFound = true)
Cạm bẫy 2: Nỗi đau mang tên YAML (.yml)
Trong Spring Boot hiện đại, người ta rất chuộng file .yml vì nó nhìn gọn gàng hơn .properties.
TUY NHIÊN, mặc định @PropertySource KHÔNG HỖ TRỢ file YAML.
Nếu bạn cố tình viết @PropertySource("classpath:config.yml"), ứng dụng vẫn chạy nhưng khi bạn dùng @Value để lấy dữ liệu, nó sẽ báo lỗi không tìm thấy (giá trị trả về null hoặc báo lỗi không thể resolve).
Cách giải quyết: Nếu muốn nạp file YAML bằng @PropertySource, bạn bắt buộc phải tự viết một class kế thừa PropertySourceFactory để dạy Spring cách đọc file YAML. Khá phiền phức! Lời khuyên là: Đã dùng @PropertySource thì hãy dùng đuôi .properties.
4. Lời khuyên: Có nên lạm dụng @PropertySource?
Dù tiện lợi, nhưng trong thế giới Spring Boot hiện đại, bạn hạn chế lạm dụng annotation này nếu không thực sự cần thiết. Lý do:
- Spring Boot đã có sẵn cơ chế đọc tự động cực mạnh bằng Profiles (tự động nạp
application-dev.properties, application-prod.properties dựa trên môi trường). - Nếu dự án có 20 file properties, bạn sẽ phải viết @PropertySource ở khắp nơi, rất khó kiểm soát việc các file có key trùng nhau đè lên nhau.
Hãy ưu tiên dùng @PropertySource khi:
- Đọc file cấu hình của một thư viện bên thứ 3 (Third-party library).
- Đọc file cấu hình bí mật được lưu rải rác trên ổ cứng Server.
4. @Scope
Trong Spring Boot, nếu @Component, @Service là tờ giấy khai sinh của một Bean, thì @Scope chính là bản án định đoạt "Tuổi thọ và Số lượng" (Lifecycle & Visibility) của Bean đó trong hệ thống.
Nói một cách dễ hiểu: Khi bạn yêu cầu Spring lấy ra một Bean (thông qua @Autowired), Spring sẽ đưa cho bạn một "bản dùng chung" với mọi người, hay tạo cho bạn một "bản sao mới toanh"? Câu trả lời nằm ở @Scope.
1. Singleton - Kẻ thống trị (Mặc định)
Nếu bạn không khai báo @Scope, Spring sẽ ngầm định Bean của bạn là Singleton.
-
Bản chất: Trong toàn bộ vòng đời của ứng dụng, Spring IoC Container chỉ tạo ra Duy nhất MỘT đối tượng (instance) của class này. Ai gọi (@Autowired) thì cũng đều nhận về đúng cái object đó.
-
Ứng dụng: Rất hoàn hảo cho các class không chứa trạng thái (Stateless) như @Service, @Repository, @Controller.
-
⚠️ Cảnh báo cực kỳ nguy hiểm: Vì nó dùng chung cho mọi luồng (thread), TUYỆT ĐỐI KHÔNG khai báo các biến toàn cục (instance variables) để lưu dữ liệu của người dùng trong class Singleton. Nếu không, User A có thể nhìn thấy dữ liệu của User B (Lỗi Thread-safety).
@Service
// @Scope("singleton") // Không cần viết vì đây là mặc định
public class OrderService {
// An toàn: Không chứa dữ liệu trạng thái
}
2. Prototype - Kẻ nổi loạn (Tạo mới liên tục)
Ngược lại với Singleton, nếu bạn đánh dấu một Bean là Prototype, Spring sẽ biến thành một nhà máy sản xuất.
-
Bản chất: Cứ mỗi lần bạn @Autowired hoặc gọi lệnh applicationContext.getBean(), Spring sẽ tạo ra một Object mới hoàn toàn, không ai đụng hàng ai.
-
Ứng dụng: Dùng cho các class chứa trạng thái (Stateful), các class thực hiện tính toán một lần rồi vứt đi (ví dụ: mã hóa một file PDF, xử lý một giỏ hàng tạm thời).
import org.springframework.context.annotation.Scope;
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
@Component
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) // Hoặc viết @Scope("prototype")
public class PdfProcessor {
private String fileName; // An toàn vì mỗi lần gọi là 1 object mới
// ...
}
3. Web Scopes (Dành riêng cho Môi trường Web/API)
Khi bạn xây dựng ứng dụng Web (có thư viện spring-boot-starter-web), Spring mở khóa thêm các Scope đặc biệt bám theo vòng đời của giao thức HTTP:
-
Request (@RequestScope): Mỗi lần Client gửi một HTTP Request (gọi API) tới Server, một Bean mới sẽ được tạo ra. Khi Request xử lý xong trả về Response, Bean đó lập tức bị hủy. Rất thích hợp để lưu TraceId, thông tin User đang gọi API.
-
Session (@SessionScope): Mỗi User (trình duyệt) khi kết nối tới Server sẽ có một Session (phiên làm việc) riêng. Spring sẽ tạo một Bean và giữ nó sống chừng nào người dùng chưa đăng xuất hoặc tắt trình duyệt. Thích hợp làm Giỏ hàng (Shopping Cart) trong các web MVC truyền thống.
4. CẠM BẪY TỬ THẦN: Prototype "Mắc kẹt" trong Singleton
Đây là câu hỏi phỏng vấn kinh điển và cũng là nguyên nhân gây ra vô số bug ngầm trên Production.
Kịch bản: Bạn có một OrderService (Mặc định là Singleton). Bên trong Service này, bạn @Autowired thằng PdfProcessor (Đã set là Prototype). Bạn kỳ vọng mỗi lần gọi hàm xuất file, nó sẽ dùng một cái Processor mới?
@Service // Mặc định là Singleton
public class OrderService {
@Autowired
private PdfProcessor pdfProcessor; // Đã đánh dấu là Prototype
public void exportInvoice() {
pdfProcessor.process();
}
}
Sự thật phũ phàng: Mã trên sẽ CHẠY SAI. Bạn sẽ chỉ nhận được đúng 1 cái PdfProcessor dùng đi dùng lại mãi mãi!
Tại sao? Vì OrderService là Singleton, nó chỉ được Spring khởi tạo Đúng 1 lần lúc ứng dụng bật lên. Và lúc nó khởi tạo, nó "tiêm" (inject) PdfProcessor vào. Sự việc tiêm này cũng chỉ diễn ra 1 lần duy nhất. Prototype bị "nhốt" trong Singleton.
3 Cách Giải Cứu:
Cách 1: Dùng @Lookup (Khuyên dùng - Chuẩn Spring)
Spring sẽ tự động ghi đè hàm này để mỗi lần gọi là nó lại đi xin Spring Container một bản mới.
@Service
public abstract class OrderService { // Chuyển thành abstract class hoặc class thường đều được (Spring dùng CGLIB proxy)
@Lookup
public PdfProcessor getPdfProcessor() {
return null; // Spring sẽ tự implement đoạn ruột này, bạn cứ return null
}
public void exportInvoice() {
PdfProcessor processor = getPdfProcessor(); // Mỗi lần gọi là 1 object mới
processor.process();
}
}
Cách 2: Dùng ObjectProvider (Hiện đại - Dễ hiểu)
@Service
public class OrderService {
@Autowired
private ObjectProvider<PdfProcessor> pdfProcessorProvider;
public void exportInvoice() {
// Lấy bản mới toanh
PdfProcessor processor = pdfProcessorProvider.getObject();
processor.process();
}
}
5. @PostConstruct
Trong Spring Boot, @PostConstruct (thuộc chuẩn Java EE / Jakarta EE, không phải của riêng Spring) là một annotation được dùng để đánh dấu một hàm sẽ được tự động thực thi ngay sau khi một Bean được khởi tạo và tiêm (inject) đầy đủ các phụ thuộc.
Nó đóng vai trò như bước "hoàn thiện nội thất" trước khi Bean đó được đưa vào phục vụ người dùng.
Để hiểu rõ sức mạnh của nó, chúng ta phải giải quyết câu hỏi phỏng vấn kinh điển sau:
Tại sao không dùng Constructor luôn cho nhanh?
Nhiều lập trình viên mới thắc mắc: "Nếu muốn chạy logic lúc khởi tạo, tại sao không nhét thẳng nó vào hàm khởi tạo (Constructor) của class?".
Hãy xem kịch bản thảm họa dưới đây:
@Service
public class CacheService {
@Autowired
private UserRepository userRepository;
private List<User> cachedUsers;
// Cách 1: Dùng Constructor (GÂY LỖI SẬP APP)
public CacheService() {
// LỖI KINH HOÀNG: NullPointerException!
// Vì ở thời điểm constructor chạy, Spring chưa kịp "tiêm" userRepository vào.
this.cachedUsers = userRepository.findAll();
}
}
Bản chất vòng đời của một Bean diễn ra theo 3 bước chặt chẽ:
-
Instantiate (Xây nhà): Chạy Constructor để tạo ra Object. Lúc này các biến @Autowired đều đang là null.
-
Dependency Injection (Bê nội thất vào): Spring bắt đầu quét và tiêm các đối tượng (như UserRepository) vào các biến @Autowired.
-
@PostConstruct (Sắp xếp nội thất): Kích hoạt hàm được đánh dấu để bạn thoải mái sử dụng các biến đã được tiêm.
Cách giải quyết đúng chuẩn với @PostConstruct:
import jakarta.annotation.PostConstruct;
import org.springframework.stereotype.Service;
@Service
public class CacheService {
@Autowired
private UserRepository userRepository;
private List<User> cachedUsers;
@PostConstruct
public void initCache() {
// An toàn tuyệt đối! Lúc này userRepository đã được Spring tiêm thành công.
System.out.println("Đang nạp dữ liệu từ DB lên Cache...");
this.cachedUsers = userRepository.findAll();
}
}
2. Ba quy tắc "Sắt đá" của hàm @PostConstruct
Khi định nghĩa một hàm mang annotation này, bạn bắt buộc phải tuân thủ các quy định sau của compiler:
- Không có tham số truyền vào: Hàm phải trống (No-args).
- Kiểu trả về là
void: Vì hệ thống tự động gọi ngầm, nó không có nhu cầu nhận lại kết quả từ hàm này. - Không giới hạn Access Modifier: Hàm có thể là
public,private, hayprotectedđều được (tuy nhiên thường đặt làpublichoặcprivate).
3. Cạm bẫy thực chiến: "Kẻ ngáng đường" lúc khởi động
Đây là lỗi cực kỳ phổ biến trong các hệ thống lớn.
Vì hàm @PostConstruct chạy đồng bộ (synchronous) trong quá trình khởi động ứng dụng (Startup), nếu bạn nhồi nhét những logic quá nặng hoặc chạy mất nhiều thời gian vào đây, toàn bộ hệ thống Spring Boot sẽ bị "đóng băng" chờ nó chạy xong.
Ví dụ: Bạn dùng @PostConstruct để call một API bên thứ 3 lấy tỷ giá ngoại tệ, nhưng mạng bị rớt. Hàm này bị treo (timeout) mất 30 giây -> Ứng dụng Spring Boot của bạn cũng mất thêm 30 giây mới khởi động xong!
Cách xử lý:
- Chỉ dùng @PostConstruct cho các thao tác khởi tạo nhanh, nhẹ (như validate dữ liệu, khởi tạo biến mặc định, map configs).
- Nếu cần làm việc nặng lúc khởi động (đọc file lớn, gọi API ngoài, insert hàng ngàn dòng DB), hãy chuyển sang sử dụng Interface
CommandLineRunnerhoặcApplicationRunnercủa Spring Boot, kết hợp với @Async nếu cần.
6. @PreDestroy
Nếu @PostConstruct là "tiếng khóc chào đời" của một Bean, thì @PreDestroy chính là "lời trăng trối" cuối cùng của nó trước khi bị hệ thống thu hồi bộ nhớ (Garbage Collected).
Annotation này (thuộc jakarta.annotation) được dùng để đánh dấu một hàm sẽ tự động kích hoạt khi Spring IoC Container chuẩn bị đóng lại (thường là lúc bạn tắt ứng dụng Spring Boot).
Đây là chốt chặn cực kỳ quan trọng để thực hiện Graceful Shutdown (Tắt ứng dụng an toàn), đảm bảo không có dữ liệu nào bị mất hay tài nguyên nào bị rò rỉ.
1. Khi nào thực sự cần dùng @PreDestroy?
Trong 80% trường hợp, các thư viện hiện đại của Spring (như HikariCP cho Database, hay Tomcat cho Web) đều tự động dọn dẹp tài nguyên rất tốt. Bạn chỉ cần tự tay viết @PreDestroy trong các kịch bản đặc thù sau:
-
Tắt các luồng chạy ngầm (Custom Thread Pools): Nếu bạn tự tạo ExecutorService mà không dùng cấu hình chuẩn của Spring, bạn phải tự gọi lệnh shutdown().
-
Đóng các kết nối IO (File/Network/Socket): Giải phóng các luồng đọc/ghi file đang mở, hoặc đóng kết nối WebSocket, gRPC thủ công.
-
Bắn tín hiệu "Goodbye": Gửi một bản tin (message) sang hệ thống khác (như Eureka, Zookeeper) để thông báo rằng "Server này sắp sập, đừng gửi request sang đây nữa!".
Ví dụ thực chiến:
import jakarta.annotation.PreDestroy;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class VideoProcessingService {
// Tự tạo một Thread Pool tùy chỉnh không do Spring quản lý
private final ExecutorService executor = Executors.newFixedThreadPool(5);
public void processVideo() {
executor.submit(() -> System.out.println("Đang render video..."));
}
// Hàm dọn dẹp trước khi tắt app
@PreDestroy
public void cleanupAndShutdown() {
System.out.println("CẢNH BÁO: Ứng dụng đang tắt. Đang từ chối các job mới...");
// 1. Ngừng nhận việc mới
executor.shutdown();
// 2. Chờ các việc cũ chạy xong (hoặc ép buộc dừng)
// ... logic dọn dẹp các file video đang xử lý dở dang
System.out.println("Dọn dẹp hoàn tất. An tâm tắt máy!");
}
}
2. Hai "Cạm bẫy" khiến @PreDestroy trở nên vô dụng
Rất nhiều lúc bạn viết hàm @PreDestroy nhưng khi tắt app lại thấy nó không hề chạy. Dưới đây là 2 nguyên nhân kinh điển:
Cạm bẫy 1: Tắt Server quá "bạo lực" (SIGKILL)
Spring Boot chỉ có thể chạy hàm @PreDestroy nếu nó nhận được tín hiệu xin phép tắt một cách tử tế (SIGTERM - Ví dụ: Bấm nút Stop trên IDE, hoặc chạy lệnh kill <PID> trên Linux).
Nếu bạn tắt nóng bằng cách ngắt điện, giật dây mạng, hoặc dùng lệnh kill -9
trên Linux, tiến trình Java sẽ bị hệ điều hành "bắn bỏ" ngay lập tức. Spring không có nổi 1 mili-giây để kịp gọi hàm @PreDestroy của bạn.
Cạm bẫy 2: Nỗi oan của Prototype Scope
Đây là một câu hỏi phỏng vấn cực khó và là một ngoại lệ bạn bắt buộc phải nhớ: @PreDestroy KHÔNG BAO GIỜ HOẠT ĐỘNG với các Bean được cấu hình @Scope("prototype").
Lý do: Với Singleton, Spring ôm Bean đó vào lòng và quản lý từ lúc sinh ra đến lúc chết. Nhưng với Prototype, khi bạn xin một Bean, Spring tạo ra nó, ném cho bạn dùng, rồi... "đem con bỏ chợ" để tiết kiệm RAM. Spring hoàn toàn quên mất sự tồn tại của Prototype đó nên lúc tắt app, nó không biết đường nào mà gọi @PreDestroy.
(Nếu muốn dọn dẹp Prototype, bạn phải tự implement class DisposableBean hoặc tự quản lý vòng đời bằng code tay).
3. Quy tắc cú pháp (Giống hệt @PostConstruct)
Để compiler của Java chấp nhận, hàm của bạn phải thỏa mãn 3 điều kiện:
-
Không có tham số (No arguments).
-
Kiểu trả về là
void(Có return thì hệ thống cũng không ai nhận). -
Tuyệt đối không ném ra Checked Exceptions (không được
throws Exceptiontrên chữ ký hàm). Nếu có lỗi lúc dọn dẹp, phải tự try-catch nuốt lỗi bên trong, vì lúc này app đang sập rồi, quăng lỗi ra ngoài không ai xử lý được nữa.
Phần II: Dependency Injection (Các Stereotype)
1. @Component
Trong Spring Boot, @Component là một annotation cơ bản nhất dùng để đánh dấu một class Java là một Spring Bean. Khi bạn gắn @Component lên đầu một class, bạn đang gửi một thông điệp tới Spring: "Đây là một class quan trọng của ứng dụng, hãy tự động khởi tạo nó (tạo object) và đưa vào IoC Container để quản lý giúp tôi".
import org.springframework.stereotype.Component;
// Đánh dấu class này là một Bean
@Component
public class EmailValidator {
public boolean isValid(String email) {
return email != null && email.contains("@");
}
}
- Khi ứng dụng chạy, Spring sẽ tự động gọi new EmailValidator() và lưu đối tượng đó vào bộ nhớ (Container). Sau này, nếu bất kỳ nơi nào trong ứng dụng cần dùng đến EmailValidator, Spring sẽ tự động lấy đối tượng đã tạo ra và truyền vào (quá trình này gọi là Dependency Injection).
Nếu bạn nhớ lại chiếc "radar" @ComponentScan ở phần trước, thì @Component chính là cái "bảng tên" phát ra tín hiệu để radar đó nhận diện và thu thập.
Như bạn thấy, cả @Bean và @Component đều được sử dụng để tạo Bean, nhưng mục đích sử dụng lại khác nhau. @Bean được sử dụng cho phương thức, @Component được sử cho class (điều này đã được thể hiện rõ ràng ở trong mã nguồn của cả hai, do vậy chúng không thể đổi vị trí cho nhau). Tóm lại "Quy tắc vàng" là:
- Bạn muốn nói với Spring: "Tôi đã viết xong Class này rồi, anh hãy tự khởi tạo nó đi" Đặt @Component (hoặc @Service, @Repository, @Controller) lên đầu Class.
- Bạn muốn nói với Spring: "Anh hãy gọi Phương thức này, object mà phương thức này return ra anh hãy giữ lấy làm Bean nhé" Đặt @Bean lên đầu Phương thức.
Khai báo @Bean trong @Component hay @Configuration
Mặc dù bạn có thể khai báo phương thức @Bean bên trong một @Component (gọi là Lite Mode). nhưng tốt nhất để tránh lỗi ngầm định là luôn đặt @Bean bên trong class @Configuration (gọi là Full Mode)
Còn nếu các Bean của bạn có phụ thuộc gọi chéo lẫn nhau trong cùng một class cấu hình, bắt buộc phải dùng @Configuration. Lí do cho những điều này là bởi vì:
-
@Configuration (Full Mode): Spring sử dụng CGLIB Proxy. Các lời gọi method @Bean bên trong class sẽ luôn đảm bảo trả về Singleton Bean (chỉ một instance duy nhất được tạo ra - cơ chế mà mình đã nhắc ở trên).
-
@Component (Lite Mode): Không có Proxy. Nếu bạn gọi method @Bean từ một method khác trong cùng class, nó sẽ hoạt động như Java thuần (tạo ra object mới), làm phá vỡ tính Singleton và gây rò rỉ bộ nhớ hoặc lỗi logic.
Các "bản sao" của @Component (Stereotype Annotations)
Trong thực tế, bạn sẽ thấy người ta ít khi dùng trực tiếp @Component. Thay vào đó, Spring cung cấp 3 annotation khác là "con" của @Component (gọi chung là stereotype annotations) để phân chia rõ ràng các tầng kiến trúc trong ứng dụng:
1.1 @Controller / @RestController
Trong Spring Boot, @Controller và @RestController đều là các annotation thuộc họ @Component. Chức năng chung của chúng là đánh dấu một class trở thành "Người tiếp tân" - chuyên đứng ở cửa ngõ ứng dụng để tiếp nhận và xử lý các HTTP Request (như GET, POST, PUT, DELETE) từ người dùng hoặc client gửi tới.
@Controller (Kiến trúc MVC truyền thống)
@Controller được thiết kế cho mô hình ứng dụng web truyền thống (Server-Side Rendering), nơi server chịu trách nhiệm tạo ra giao diện HTML và gửi về cho trình duyệt.
Khi một phương thức trong @Controller trả về một chuỗi (String), Spring sẽ không gửi chuỗi đó cho người dùng. Thay vào đó, nó nhờ một thành phần gọi là ViewResolver đi tìm một file giao diện (HTML, Thymeleaf, JSP) có tên tương ứng để hiển thị.
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class WebController {
@GetMapping("/home")
public String showHomePage() {
// Spring sẽ đi tìm file có tên "index.html" (hoặc index.jsp/index.html tùy cấu hình)
// trong thư mục resources/templates hoặc resources/static để trả về cho trình duyệt.
// nếu không thấy sẽ trả về trang 404
return "index";
}
}
@RestController (Kiến trúc RESTful API)
Trong các ứng dụng hiện đại, Backend (Spring Boot) thường tách biệt hoàn toàn với Frontend (React, Vue, Angular, hoặc Mobile App). Lúc này, Backend không trả về giao diện HTML nữa mà chỉ trả về dữ liệu thô (thường là định dạng JSON). Đây chính là đất diễn của @RestController.
Bản chất của @RestController là sự kết hợp của 2 annotation:
- @Controller: Đánh dấu đây là class xử lý request.
- @ResponseBody: Ra lệnh cho Spring rằng: "Bất cứ thứ gì hàm này return, hãy tự động chuyển đổi nó thành JSON (hoặc XML) và gửi thẳng vào phần Body của HTTP Response, KHÔNG đi tìm file HTML nào cả".
(Chính vì thế, với @Controller, nếu bạn muốn trả về dữ liệu JSON thì bắt buộc phải khai báo kèm thêm @ResponseBody [trên class hoặc trên từng hàm]. Còn nếu dùng @RestController thì @ResponseBody đã được tích hợp sẵn bên trong nó rồi, bạn chỉ việc return dữ liệu là xong)
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ApiController {
@GetMapping("/api/user")
public User getUserData() {
User user = new User("John Doe", 25);
// Spring (nhờ thư viện Jackson) sẽ tự động biến object 'user' thành:
// {"name": "John Doe", "age": 25} và gửi về cho client.
return user;
}
}
Bảng so sánh
| Tiêu chí | @Controller | @RestController |
|---|---|---|
| Mục đích chính | Trả về các trang giao diện (Web Pages). | Trả về dữ liệu (RESTful APIs). |
| Giá trị trả về mặc định | Tên của View (giao diện HTML, Thymeleaf, JSP). | Dữ liệu dạng JSON hoặc XML. |
| Cấu tạo | Chỉ là @Controller | Là sự kết hợp của @Controller + @ResponseBody |
| Muốn trả về JSON thì sao? | Phải gắn thêm annotation @ResponseBody lên từng phương thức. | Được mặc định sẵn cho tất cả các phương thức trong class. |
| Ứng dụng thực tế | Các web app kiểu cũ, web nội bộ (admin dashboard) render bằng Thymeleaf. | Làm Backend API cho React, Vue, App Mobile (iOS, Android). |
1.2 @Service
Trong Spring Boot, @Service là một annotation thuộc họ @Component. Nhiệm vụ duy nhất của nó là đánh dấu một class đóng vai trò là Tầng xử lý nghiệp vụ (Business Logic Layer) trong kiến trúc của ứng dụng.
Nếu @Controller là "Người tiếp tân" đón khách, thì @Service chính là "Nhà bếp" hoặc "Bộ phận xử lý" – nơi thực hiện mọi tính toán phức tạp, kiểm tra đúng sai và đưa ra quyết định.
Tại sao không dùng @Component cho nhanh?
Về mặt kỹ thuật, Spring đối xử với class gắn @Service giống hệt 100% so với class gắn @Component (nghĩa là nó cũng khởi tạo Bean và đưa vào Container).
-
Tính ngữ nghĩa (Semantics / Readability): Khi một lập trình viên khác (hoặc chính bạn sau 3 tháng) nhìn vào class có chữ @Service, họ sẽ hiểu ngay đây là não bộ của dự án, nơi chứa các quy tắc kinh doanh.
-
Tương lai mở rộng: Mặc dù hiện tại @Service chưa có tính năng ẩn nào (như @Repository), nhưng Spring Framework hoàn toàn có thể bổ sung các tính năng đặc thù dành riêng cho tầng service trong tương lai dựa trên annotation này.
Quy tắc "Vàng" khi code: Thin Controller - Fat Service
Một lỗi rất phổ biến của người mới học là viết toàn bộ code tính toán, if/else trực tiếp bên trong @RestController. Điều này là cực kỳ cấm kỵ trong thiết kế phần mềm.
Quy tắc chuẩn:
-
Controller phải "ốm" (Thin): Chỉ làm nhiệm vụ check tham số đầu vào, gọi Service, và trả ra HTTP Status (200 OK, 400 Bad Request).
-
Service phải "mập" (Fat): Gánh vác toàn bộ não bộ của ứng dụng. Mọi logic phức tạp, kết nối các hệ thống khác, gửi email,... đều phải nằm ở đây. Điều này giúp bạn dễ dàng viết Unit Test cho logic mà không cần phải giả lập môi trường Web/HTTP.
1.3 @Repository
Trong Spring Boot, @Repository là mảnh ghép cuối cùng trong bộ ba kiến trúc phân tầng cơ bản (cùng với @Controller và @Service). Nó được dùng để đánh dấu một class là Tầng truy cập dữ liệu (Data Access Object - DAO).
Nếu @Controller là "Người tiếp tân" đón khách, @Service là "Nhà bếp" xử lý logic, thì @Repository chính là "Thủ kho" - người duy nhất có chìa khóa để đi vào kho chứa (Cơ sở dữ liệu - Database) để lấy ra, cất vào, hoặc xóa dữ liệu.
Nhiệm vụ duy nhất của class được gắn @Repository là giao tiếp trực tiếp với Database. Tại đây, bạn sẽ viết các câu lệnh SQL hoặc sử dụng các công cụ như Hibernate/JPA để thực hiện các thao tác CRUD (Create, Read, Update, Delete).
import org.springframework.stereotype.Repository;
import org.springframework.jdbc.core.JdbcTemplate;
@Repository
public class UserRepository {
private final JdbcTemplate jdbcTemplate;
public UserRepository(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// Thủ kho thực hiện lệnh: Lấy thông tin user từ DB
public User findById(Long id) {
String sql = "SELECT * FROM users WHERE id = ?";
return jdbcTemplate.queryForObject(sql, new Object[]{id}, new UserRowMapper());
}
}
"Siêu năng lực" ẩn giấu: Exception Translation
Giống như @Service, @Repository về bản chất cũng là một @Component và sẽ được Spring quét để tạo Bean.
Tuy nhiên, KHÔNG BAO GIỜ được dùng @Component để thay thế cho @Repository. Khác với @Service (chỉ mang ý nghĩa phân loại), @Repository được Spring trang bị một tính năng ngầm cực kỳ mạnh mẽ gọi là Exception Translation (Dịch lỗi).
Vấn đề:
Mỗi loại Database (MySQL, PostgreSQL, Oracle) ném ra các lỗi Java (Exceptions) khác nhau khi có sự cố. Ví dụ, khi bạn cố lưu một User có email bị trùng, MySQL có thể ném ra SQLException, Hibernate ném ra ConstraintViolationException. Nếu tầng @Service phải dùng try-catch để bắt hàng tá loại lỗi cụ thể của từng DB này, code sẽ rất rối và bị phụ thuộc chặt chẽ vào loại DB đang dùng.
Cách @Repository giải quyết:
Khi bạn đánh dấu class bằng @Repository, Spring sẽ tự động bắt tất cả các lỗi đặc thù của Database/Hibernate đó và "dịch" (translate) chúng thành một hệ thống lỗi chung chuẩn hóa của riêng Spring gọi là DataAccessException.
Nhờ đó, tầng @Service của bạn chỉ cần biết bắt một loại lỗi chung (DataAccessException), dù ngày mai bạn có đổi Database từ MySQL sang Oracle thì code ở tầng Service cũng không cần phải sửa lại.
Thực tế hiện nay: Spring Data JPA
Nếu bạn đang học Spring Boot phiên bản mới, có thể bạn sẽ thấy lạ vì... bạn hầu như không bao giờ phải viết class và gắn chữ @Repository lên đầu nữa!
Ngày nay, với sự xuất hiện của Spring Data JPA, bạn chỉ cần tạo một interface và kế thừa (extend) JpaRepository. Bạn thậm chí không cần viết code triển khai, cũng không cần gắn @Repository (mặc dù gắn vào cũng không sao).
import org.springframework.data.jpa.repository.JpaRepository;
// Không cần gắn @Repository, Spring Data tự hiểu và tự động tạo code ngầm
public interface ProductRepository extends JpaRepository<Product, Long> {
// Chỉ cần khai báo tên hàm, Spring Boot tự động sinh ra câu lệnh SQL "SELECT * FROM product WHERE name = ?"
List<Product> findByName(String name);
}
Dù bạn không nhìn thấy, nhưng ở bên dưới nền (under the hood), Spring Boot vẫn tự động tạo ra một class triển khai cái interface này, tự động gắn @Repository vào class đó, và áp dụng cơ chế "Dịch lỗi" (Exception Translation) như đã nói ở trên.
2. @Autowired
Trong Spring Boot, @Autowired là trái tim của cơ chế Dependency Injection (Tiêm phụ thuộc).
Nếu @Component, @Service, @Bean làm nhiệm vụ tạo ra đối tượng và ném vào kho chứa (IoC Container), thì @Autowired chính là câu lệnh để lấy đối tượng đó từ kho ra để sử dụng.
Bản chất của @Autowired là tìm kiếm Bean theo kiểu dữ liệu (by Type). Khi thấy annotation này, Spring sẽ lục lọi trong Container xem có Bean nào khớp với kiểu dữ liệu đang yêu cầu hay không để "lắp" vào.
3. @Primary
Trong Spring Boot, @Primary là tấm thẻ "Ưu tiên số 1" dành cho các Bean.Nó được sinh ra để giải quyết một bài toán rất cụ thể trong quá trình Dependency Injection: "Khi có nhiều Bean cùng loại, tôi nên chọn ai?".
Dưới đây là chi tiết về cách hoạt động, cách sử dụng và các trường hợp thực tế nên áp dụng @Primary.
Bài toán: Sự bối rối của Spring (NoUniqueBeanDefinitionException)
Giả sử ứng dụng của bạn có một Interface là MessageService để gửi thông báo cho khách hàng, và bạn có 2 class triển khai (implement) Interface này:
public interface MessageService {
void sendMessage(String msg);
}
@Service
public class EmailService implements MessageService {
// Gửi qua Email
}
@Service
public class SmsService implements MessageService {
// Gửi qua SMS
}
Bây giờ, tại một Controller, bạn yêu cầu Spring tiêm (@Autowired) một MessageService vào:
@RestController
public class NotificationController {
// SPRING BÁO LỖI: NoUniqueBeanDefinitionException
@Autowired
private MessageService messageService;
}
Lý do lỗi: Spring tìm thấy cả EmailService và SmsService đều là MessageService. Nó không thể tự quyết định nên lấy cái nào để tiêm vào, nên nó sẽ đánh sập ứng dụng để bắt bạn phải làm rõ.
Cách giải quyết bằng @Primary
Bạn sử dụng @Primary để đánh dấu một Bean làm mặc định. Khi Spring bị bối rối, nó sẽ ưu tiên lấy Bean có gắn @Primary.
@Service
@Primary // Đánh dấu Email là phương thức gửi mặc định
public class EmailService implements MessageService { ... }
@Service
public class SmsService implements MessageService { ... }
Lúc này, ở NotificationController, khi bạn gọi @Autowired MessageService, Spring sẽ không hỏi lại nữa mà tự động tiêm EmailService vào.
Dùng @Primary với phương thức @Bean
Không chỉ dùng được trên các class (@Component, @Service), @Primary còn dùng được trên các phương thức @Bean bên trong class @Configuration.
Điều này cực kỳ hữu ích khi bạn phải cấu hình nhiều kết nối Cơ sở dữ liệu (Database) trong cùng một dự án.
@Configuration
public class DatabaseConfig {
// Cấu hình Database chính (Master - Ghi dữ liệu)
@Bean
@Primary // Bắt buộc phải có nếu bạn khai báo 2 DataSource
public DataSource masterDataSource() {
return new HikariDataSource(...);
}
// Cấu hình Database phụ (Slave - Chỉ đọc)
@Bean
public DataSource replicaDataSource() {
return new HikariDataSource(...);
}
}
Nhờ có @Primary, tất cả các phần trong Spring Data JPA (trừ khi được chỉ định rõ ràng) sẽ tự động sử dụng masterDataSource để làm việc.
Khi nào thì dùng @Primary trong thực tế?
-
Chiến lược mặc định (Default Strategy): Khi bạn có một Interface có nhiều cách triển khai, nhưng 90% thời gian hệ thống sẽ dùng 1 cách cụ thể. Hãy gắn @Primary cho cái 90% đó để code ở nơi khác đỡ phải viết @Qualifier nhiều lần.
-
Ghi đè cấu hình mặc định của Spring Boot: Spring Boot tự động tạo ra rất nhiều Bean (ví dụ: ObjectMapper để parse JSON). Nếu bạn muốn tạo một ObjectMapper của riêng mình với định dạng ngày tháng khác, bạn chỉ cần tạo @Bean và gắn @Primary, Spring sẽ vứt bỏ Bean mặc định của nó và dùng Bean của bạn.
4. @Qualifier
Trong Spring Boot, nếu @Primary là tấm thẻ "Ưu tiên mặc định", thì @Qualifier chính là "Ngón tay chỉ đích danh".
Annotation này được sử dụng trong quá trình Dependency Injection để nói với Spring một cách chính xác tuyệt đối rằng: "Giữa một rừng Bean cùng kiểu dữ liệu, tôi muốn lấy chính xác cái Bean có cái tên này!".
Vấn đề: Khi @Primary không giải quyết được tất cả
Hãy tưởng tượng bạn đang xây dựng một hệ thống thanh toán có hỗ trợ nhiều cổng khác nhau. Bạn có một Interface chung và 2 class triển khai:
public interface PaymentService {
void pay();
}
@Service // Tên Bean ngầm định của class này là "momoPayment"
@Primary // Đặt Momo làm mặc định cho toàn hệ thống
public class MomoPayment implements PaymentService { ... }
@Service // Tên Bean ngầm định của class này là "vnpayPayment"
public class VnpayPayment implements PaymentService { ... }
Tại một module đặc biệt (ví dụ: VipCheckoutService), bạn bắt buộc phải dùng VNPay để thanh toán. Nếu bạn chỉ dùng @Autowired, Spring sẽ tự động lấy MomoPayment truyền vào (vì nó có @Primary).
Đây chính là lúc bạn phải dùng đến @Qualifier để "ghi đè" (override) luật mặc định đó.
Cách sử dụng @Qualifier
Bạn sẽ đặt @Qualifier đi kèm với nơi bạn yêu cầu tiêm Bean (thường là ở Constructor). Tham số truyền vào @Qualifier chính là tên của Bean mà bạn muốn lấy.
Lưu ý: Nếu bạn không tự đặt tên cho Bean (bằng thuộc tính name hoặc value), Spring sẽ tự động lấy tên class và viết thường chữ cái đầu tiên để làm tên Bean.
Sử dụng với Constructor Injection (Khuyên dùng):
@Service
public class VipCheckoutService {
private final PaymentService paymentService;
// Ép Spring phải lấy Bean có tên là "vnpayPayment", bỏ qua mọi quy tắc @Primary
@Autowired
public VipCheckoutService(@Qualifier("vnpayPayment") PaymentService paymentService) {
this.paymentService = paymentService;
}
public void process() {
paymentService.pay(); // Chắc chắn sẽ chạy code của VNPay
}
}
Sử dụng với Field Injection
@Service
public class StandardCheckoutService {
@Autowired
@Qualifier("vnpayPayment") // Tiêm trực tiếp vào biến
private PaymentService paymentService;
}
Dùng @Qualifier kết hợp với @Bean
@Qualifier cực kỳ hữu dụng khi bạn làm việc với các class @Configuration có cấu hình nhiều Bean cùng loại (ví dụ: kết nối 2 database, cấu hình 2 RestTemplate khác nhau).
Bước 1: Khai báo các Bean và đặt tên rõ ràng
@Configuration
public class NetworkConfig {
@Bean(name = "internalRestTemplate")
public RestTemplate internalClient() {
return new RestTemplate(); // Cấu hình timeout ngắn cho mạng nội bộ
}
@Bean(name = "externalRestTemplate")
public RestTemplate externalClient() {
return new RestTemplate(); // Cấu hình proxy, timeout dài cho mạng bên ngoài
}
}
Bước 2: Chỉ đích danh khi sử dụng
@Service
public class ThirdPartyApiService {
private final RestTemplate restTemplate;
// Yêu cầu lấy đúng Bean giao tiếp ra bên ngoài
public ThirdPartyApiService(@Qualifier("externalRestTemplate") RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
}
Tóm tắt Quyền lực của @Qualifier
- Độ ưu tiên cao nhất: Khi một Bean được chỉ định bởi @Qualifier, nó sẽ bỏ qua mọi thiết lập của @Primary.
- Khớp theo tên (By Name): Trong khi @Autowired tìm Bean theo kiểu dữ liệu (By Type), thì @Qualifier thu hẹp phạm vi lại, ép Spring phải tìm chính xác theo tên (By Name).
5. @Lazy
Trong Spring Boot, theo mặc định, tất cả các Spring Bean (Singleton) đều được khởi tạo ngay lập tức (Eager Initialization) khi ứng dụng vừa khởi động.
Điều này giống như việc một nhà hàng nấu sẵn tất cả các món trong menu ngay từ lúc mở cửa. Tuy nhiên, đôi khi có những món rất ít khách gọi, việc nấu sẵn sẽ gây lãng phí bộ nhớ và làm chậm thời gian mở cửa.
@Lazy được sinh ra để giải quyết vấn đề này. Khi sử dụng @Lazy, Spring sẽ trì hoãn (delay) việc tạo Bean cho đến khi có một đoạn code nào đó thực sự gọi đến nó lần đầu tiên.
Dưới đây mình sẽ đề cập 2 công dụng lớn nhất và cách sử dụng @Lazy trong thực tế.
Ứng dụng số 1: Tối ưu thời gian khởi động (Startup Time)
Nếu ứng dụng của bạn có những Bean chứa logic khởi tạo cực kỳ nặng (như đọc hàng ngàn dòng file Excel, kết nối đến các hệ thống bên ngoài chậm chạp) nhưng lại ít khi được sử dụng, bạn có thể gắn @Lazy để bỏ qua nó lúc khởi động.
Cách 1: Gắn trên class (Tạo bằng @Component / @Service)
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@Service
@Lazy // Spring sẽ bỏ qua class này lúc khởi động
public class HeavyReportService {
public HeavyReportService() {
System.out.println("Đang khởi tạo HeavyReportService... Mất 10 giây!");
}
public void generateReport() {
// ...
}
}
Cách 2: Gắn trên phương thức (Tạo bằng @Bean)
@Configuration
public class AppConfig {
@Bean
@Lazy
public PdfGenerator pdfGenerator() {
return new PdfGenerator();
}
}
💡 Lưu ý: Nếu một Bean bình thường (không Lazy) gọi (@Autowired) một Bean @Lazy, thì Bean Lazy đó vẫn sẽ bị khởi tạo ngay từ đầu. Để @Lazy có tác dụng thực sự trong trường hợp này, bạn phải gắn thêm @Lazy vào ngay vị trí tiêm (chỗ gọi @Autowired hoặc tham số Constructor)
Giải thích rõ hơn tại sao lại như vậy:
Giả sử bạn có HeavyReportService đã được đánh dấu là @Lazy trên đầu class. Mục đích của bạn là muốn nó "ngủ yên" lúc khởi động.
Nhưng ở một class khác là DashboardController (một class bình thường, khởi động ngay lập tức), bạn lại gọi nó vào:
@RestController
public class DashboardController {
@Autowired
private HeavyReportService reportService;
// VẤN ĐỀ Ở ĐÂY: Vì Controller này khởi động ngay lập tức,
// nó bắt buộc phải "lôi" cái ReportService dậy để nạp vào biến này.
// Thế là công sức gắn @Lazy ở class ReportService đổ sông đổ biển.
}
Cách sửa chuẩn xác: Bạn phải gắn @Lazy ở cả chỗ gọi nó.
@RestController
public class DashboardController {
@Autowired
@Lazy // Dặn Spring: "Đừng tạo object thật vội, cứ quăng cho tôi 1 cái vỏ (Proxy) vào đây là được"
private HeavyReportService reportService;
}
Khi bạn thêm @Lazy vào vị trí này, Spring Boot sẽ chỉ nạp một "bản nháp" (Proxy) vào biến reportService. Cái HeavyReportService thật sự vẫn sẽ được trì hoãn khởi tạo cho đến khi bạn chạy một hàm thực tế như reportService.generate()
Ứng dụng số 2: "Cứu cánh" cho lỗi Vòng lặp phụ thuộc (Circular Dependency)
Đây là trường hợp bạn sẽ sử dụng @Lazy nhiều nhất trong các dự án thực tế.
Vấn đề: OrderService cần gọi PaymentService để tính tiền. Ngược lại, PaymentService lại cần gọi OrderService để cập nhật trạng thái đơn hàng.
- Spring cố tạo
OrderService, thấy thiếuPaymentService. - Spring chạy đi tạo
PaymentService, lại thấy thiếuOrderService. - Spring bị kẹt trong vòng lặp vô tận và đánh sập ứng dụng (Lỗi
BeanCurrentlyInCreationExceptionhoặc cảnh báoThe dependencies of some of the beans in the application context form a cycle).
Cách giải quyết bằng @Lazy:
- Bạn chỉ cần gắn @Lazy vào vị trí Tiêm (Injection) ở một trong hai class. Khi thấy @Lazy ở tham số truyền vào, Spring sẽ không đi tìm class thật nữa. Thay vào đó, nó tạo ra một Proxy (đối tượng thế thân ảo) để truyền vào cho OrderService dùng tạm, qua đó phá vỡ vòng lặp. Đến khi nào OrderService thực sự gọi một hàm của PaymentService, object thật mới được tạo ra.
@Service
public class OrderService {
private final PaymentService paymentService;
// Gắn @Lazy ở phía Constructor để phá vòng lặp
public OrderService(@Lazy PaymentService paymentService) {
this.paymentService = paymentService;
}
}
@Service
public class PaymentService {
private final OrderService orderService;
public PaymentService(OrderService orderService) {
this.orderService = orderService;
}
}
Cấu hình Lazy toàn hệ thống (Global Lazy)
Nếu dự án của bạn quá lớn, mất đến 3-5 phút để chạy xong ứng dụng ở máy local, bạn có thể bật chế độ @Lazy cho toàn bộ hệ thống. Điều này giúp ứng dụng khởi động chỉ trong vài giây (cực kỳ hữu ích cho môi trường Dev).
# Yêu cầu Spring áp dụng @Lazy cho tất cả các Bean
spring.main.lazy-initialization=true
Khi nào KHÔNG nên dùng @Lazy?
Dù có vẻ rất tuyệt vời để tăng tốc độ bật ứng dụng, nhưng các chuyên gia khuyến cáo không nên lạm dụng @Lazy trên môi trường Production.
| Tiêu chí | Khởi tạo Eager (Mặc định) | Khởi tạo Lazy (@Lazy) |
|---|---|---|
| Phát hiện lỗi (Fail-fast) | Tốt. Nếu bạn cấu hình sai Bean, thiếu kết nối Database, ứng dụng sẽ sập ngay lúc khởi động để bạn biết mà sửa. | Kém. Ứng dụng khởi động thành công, nhưng đến khi khách hàng bấm vào tính năng đó thì app mới sập. |
| Hiệu năng khi sử dụng | Yêu cầu đầu tiên của người dùng sẽ được phản hồi cực nhanh vì mọi thứ đã sẵn sàng. | Yêu cầu đầu tiên của người dùng sẽ bị chậm (độ trễ cao) vì hệ thống lúc đó mới lạch cạch đi tạo đối tượng. |
Tạm kết
"Đến đây, bài viết cũng đã khá dài và chúng ta vừa cùng nhau đi qua những 'viên gạch' nền móng quan trọng nhất của Spring Boot: từ nghệ thuật điều hướng Dependency Injection, kiểm soát dòng chảy dữ liệu của RESTful API, cho đến các annotation cực tiện ích đến từ nhà Lombok. Nắm vững bộ công cụ này, bạn đã hoàn toàn tự tin để xây dựng một kiến trúc backend chuẩn mực và an toàn.
Tuy nhiên, hành trình tối ưu hóa hệ thống vẫn chưa dừng lại. Khi ứng dụng phình to, làm thế nào để cấu trúc code linh hoạt giữa các môi trường, quản lý bảo mật khắt khe, hay xử lý mượt mà các bài toán đa luồng (multi-threading) phức tạp? Tất cả những annotation nâng cao về cấu hình (Configuration), bộ lọc (Filter) và kiến trúc hướng sự kiện (Event-driven) sẽ được giải mã chi tiết trong Phần 2.
Tạm thời, hãy để não bộ nghỉ ngơi và 'tiêu hóa' lượng kiến thức đồ sộ này nhé. Nếu có bất kỳ thắc mắc nào trong lúc áp dụng, đừng ngại để lại bình luận phía dưới. Hẹn gặp lại mọi người ở Phần 2!"
All rights reserved