Ôn lại — Từ Java main() đến Web Server đang chạy
Trong Bài 1, chúng ta đã tạo một ứng dụng Spring Boot với HelloController đơn giản. Hãy nhanh chóng ôn lại những gì xảy ra khi bạn chạy ứng dụng:
@SpringBootApplication
public class BlogApiApplication {
public static void main(String[] args) {
SpringApplication.run(BlogApiApplication.class, args);
}
}
Khi SpringApplication.run() thực thi, nó thực hiện một loạt các bước phía sau:
- Tạo Application Context — một container chứa và quản lý tất cả các đối tượng (gọi là “bean”) trong ứng dụng.
- Quét Component (Component Scanning) — quét các package để tìm class được đánh dấu với
@Component,@Service,@Controller,@Repository, và các annotation khác. Nó tạo instance của các class này và đăng ký chúng vào Application Context. - Auto-Configuration — kiểm tra classpath (các thư viện trong
pom.xml) và tự động cấu hình các thứ như server Tomcat nhúng, JSON serialization, và nhiều hơn nữa. - Khởi động web server nhúng — Tomcat bắt đầu lắng nghe trên cổng 8080.
Trong Bài 1, chúng ta coi tất cả như là phép thuật. Trong bài này, chúng ta sẽ mở hộp ra và hiểu hai khái niệm quan trọng nhất làm cho Spring hoạt động: Inversion of Control và Dependency Injection.
Đây không chỉ là các khái niệm của Spring — chúng là các nguyên tắc thiết kế phần mềm cơ bản sẽ giúp bạn trở thành lập trình viên giỏi hơn bất kể bạn sử dụng framework nào.
Inversion of Control (IoC) là gì?
Cách truyền thống: Bạn kiểm soát mọi thứ
Trong code Java bạn đã viết trước đây, bạn có thể đã tạo đối tượng như thế này:
public class OrderService {
// Bạn tự tạo các dependency
private EmailService emailService = new EmailService();
private OrderRepository orderRepository = new OrderRepository();
private PaymentGateway paymentGateway = new StripePaymentGateway();
public void placeOrder(Order order) {
orderRepository.save(order);
paymentGateway.charge(order.getTotal());
emailService.sendConfirmation(order);
}
}
Trong code này, OrderService kiểm soát hoàn toàn. Nó quyết định:
- Sử dụng
EmailServicenào - Sử dụng
OrderRepositorynào - Sử dụng implementation
PaymentGatewaynào (StripePaymentGateway)
Điều này có vẻ tự nhiên, nhưng nó tạo ra nhiều vấn đề.
Vấn đề 1: Liên kết chặt (Tight Coupling)
OrderService bị liên kết chặt với StripePaymentGateway. Nếu bạn muốn chuyển sang PayPal, bạn phải sửa code của OrderService:
// Bạn phải thay đổi OrderService chỉ vì muốn dùng nhà cung cấp thanh toán khác
private PaymentGateway paymentGateway = new PayPalPaymentGateway();
Nếu 20 class khác cũng sử dụng StripePaymentGateway? Bạn phải thay đổi cả 20.
Vấn đề 2: Khó kiểm thử
Làm thế nào bạn kiểm thử OrderService mà không thực sự gửi email, tính phí thẻ tín dụng, và ghi vào cơ sở dữ liệu thật? Bạn không thể dễ dàng thay thế các dependency này bằng mock object vì chúng được hardcode bên trong class.
// Bạn muốn test placeOrder(), nhưng nó sẽ:
// - Cố kết nối đến cơ sở dữ liệu thật (OrderRepository)
// - Cố tính phí thẻ tín dụng thật (StripePaymentGateway)
// - Cố gửi email thật (EmailService)
// Điều này làm việc kiểm thử chậm, không đáng tin cậy, và có thể tốn kém!
Vấn đề 3: Dependency ẩn
Nhìn class OrderService từ bên ngoài, bạn không biết nó phụ thuộc vào gì. Các dependency bị chôn bên trong class. Một lập trình viên sử dụng OrderService không biết rằng nó cần email service, repository, và payment gateway cho đến khi họ đọc phần implementation.
Cách IoC: Container kiểm soát mọi thứ
Inversion of Control đảo ngược trách nhiệm. Thay vì class tự tạo dependency của mình, một thực thể bên ngoài (IoC container, trong Spring chính là Application Context) tạo các dependency và cung cấp chúng cho class.
public class OrderService {
// Các dependency được khai báo, nhưng KHÔNG được tạo ở đây
private final EmailService emailService;
private final OrderRepository orderRepository;
private final PaymentGateway paymentGateway;
// Các dependency được cung cấp từ bên ngoài thông qua constructor
public OrderService(EmailService emailService,
OrderRepository orderRepository,
PaymentGateway paymentGateway) {
this.emailService = emailService;
this.orderRepository = orderRepository;
this.paymentGateway = paymentGateway;
}
public void placeOrder(Order order) {
orderRepository.save(order);
paymentGateway.charge(order.getTotal());
emailService.sendConfirmation(order);
}
}
Bây giờ OrderService không còn kiểm soát việc tạo dependency nữa. Nó chỉ đơn giản khai báo những gì nó cần, và thứ khác cung cấp chúng. Quyền kiểm soát đã được đảo ngược — vì thế có tên “Inversion of Control.”
Một phép so sánh
Hãy tưởng tượng bạn đang ở nhà hàng:
- Không có IoC (Truyền thống): Bạn vào bếp, tìm nguyên liệu, tự nấu ăn, rửa bát, rồi mới ăn. Bạn kiểm soát mọi thứ.
- Có IoC (Spring): Bạn ngồi vào bàn, nói cho phục vụ biết bạn muốn gì, và bếp chuẩn bị cho bạn. Bạn chỉ tập trung vào việc ăn — nhà hàng kiểm soát việc chuẩn bị.
Trong phép so sánh này:
- Bạn là
OrderService - Nguyên liệu là các dependency (
EmailService,PaymentGateway, v.v.) - Nhà hàng/bếp là Spring IoC Container
- Phục vụ mang đồ ăn là Dependency Injection
Giải thích Dependency Injection
Dependency Injection (DI) là cơ chế mà IoC container cung cấp dependency cho một class. Đó là “cách thức” của Inversion of Control.
Có ba loại Dependency Injection trong Spring:
Constructor Injection (Khuyến nghị)
Các dependency được cung cấp thông qua constructor của class:
@Service
public class OrderService {
private final EmailService emailService;
private final OrderRepository orderRepository;
// Spring thấy constructor này, tìm bean kiểu EmailService
// và OrderRepository trong Application Context, và truyền chúng vào.
public OrderService(EmailService emailService,
OrderRepository orderRepository) {
this.emailService = emailService;
this.orderRepository = orderRepository;
}
public void placeOrder(Order order) {
orderRepository.save(order);
emailService.sendConfirmation(order);
}
}
Đặc điểm chính:
- Các dependency được khai báo là trường
final— chúng không thể thay đổi sau khi khởi tạo. - Đối tượng luôn ở trạng thái hợp lệ — tất cả dependency được đảm bảo có mặt khi constructor hoàn thành.
- Nếu class chỉ có một constructor, Spring tự động sử dụng nó — bạn thậm chí không cần
@Autowired.
Setter Injection
Các dependency được cung cấp thông qua phương thức setter:
@Service
public class OrderService {
private EmailService emailService;
private OrderRepository orderRepository;
// Spring gọi các setter này sau khi tạo đối tượng
@Autowired
public void setEmailService(EmailService emailService) {
this.emailService = emailService;
}
@Autowired
public void setOrderRepository(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
}
Cách tiếp cận này ít phổ biến trong Spring hiện đại. Vấn đề chính là đối tượng có thể tồn tại ở trạng thái chưa hoàn chỉnh — giữa thời điểm khởi tạo và setter injection, các trường là null.
Field Injection
Các dependency được inject trực tiếp vào các trường sử dụng @Autowired:
@Service
public class OrderService {
// Spring inject trực tiếp sử dụng reflection —
// không cần constructor hay setter
@Autowired
private EmailService emailService;
@Autowired
private OrderRepository orderRepository;
}
Cách này trông đơn giản nhất, nhưng có những nhược điểm nghiêm trọng:
- Các trường không thể là
final, nên dependency có thể bị thay đổi vô tình. - Bạn không thể tạo đối tượng mà không có Spring (làm việc kiểm thử khó hơn).
- Các dependency bị ẩn — bạn không thể biết class cần gì chỉ bằng cách nhìn constructor.
- Class trở nên liên kết chặt với chính framework Spring.
Nên dùng cái nào?
Luôn sử dụng Constructor Injection. Đây là khuyến nghị chính thức từ đội ngũ Spring. Đây là bảng so sánh:
| Tiêu chí | Constructor | Setter | Field |
|---|---|---|---|
Tính bất biến (trường final) |
Có | Không | Không |
| Luôn ở trạng thái hợp lệ | Có | Không | Không |
| Dễ kiểm thử không cần Spring | Có | Có | Không |
| Dependency hiển thị từ bên ngoài | Có | Phần nào | Không |
| Được đội ngũ Spring khuyến nghị | Có | Chỉ cho dep tùy chọn | Không |
Ngoại lệ duy nhất: sử dụng setter injection cho dependency tùy chọn có giá trị mặc định hợp lý. Với mọi thứ khác, dùng constructor injection.
Spring Application Context & Vòng đời Bean
Bean là gì?
Trong Spring, bean đơn giản là một đối tượng Java được quản lý bởi Spring IoC container. Khi bạn đánh dấu class với @Component, @Service, @Controller, hoặc @Repository, Spring tạo một instance của class đó và quản lý nó. Instance đó là một bean.
@Service // Điều này cho Spring biết: "Tạo một instance của class này và quản lý nó"
public class UserService {
// Class này sẽ trở thành một bean trong Application Context
}
Application Context là gì?
Application Context là container chứa tất cả bean. Hãy nghĩ nó như một HashMap lớn trong đó key là tên bean và value là instance bean.
Khi ứng dụng khởi động, Spring:
- Quét tìm các class được đánh dấu với stereotype (
@Component,@Service, v.v.) - Tạo instance (bean) của các class đó
- Giải quyết dependency giữa các bean (Dependency Injection)
- Lưu tất cả bean vào Application Context
- Làm cho chúng sẵn sàng cho bất kỳ class nào cần
Vòng đời Bean
Mỗi bean trải qua một vòng đời từ lúc tạo đến lúc hủy. Đây là phiên bản đơn giản:
1. Tìm thấy định nghĩa Bean (quét @Component)
↓
2. Bean được khởi tạo (constructor được gọi)
↓
3. Dependency được inject (qua constructor, setter, hoặc field)
↓
4. Phương thức @PostConstruct được gọi (nếu có)
↓
5. Bean sẵn sàng sử dụng
↓
... ứng dụng chạy ...
↓
6. Phương thức @PreDestroy được gọi (nếu có — khi ứng dụng tắt)
↓
7. Bean bị hủy
Bạn có thể can thiệp vào vòng đời này với @PostConstruct và @PreDestroy:
@Service
public class CacheService {
private Map<String, Object> cache;
// Được gọi sau khi bean được tạo và dependency được inject.
// Sử dụng cho logic khởi tạo cần dependency.
@PostConstruct
public void init() {
System.out.println("CacheService đã khởi tạo!");
cache = new HashMap<>();
// Tải dữ liệu ban đầu, khởi động cache, v.v.
}
// Được gọi khi ứng dụng đang tắt.
// Sử dụng cho dọn dẹp: đóng kết nối, flush buffer, v.v.
@PreDestroy
public void cleanup() {
System.out.println("CacheService đang tắt!");
cache.clear();
}
}
Lưu ý: Đừng đặt logic nặng trong constructor. Constructor chạy trước dependency injection (cho setter/field injection) và trước
@PostConstruct. Sử dụng@PostConstructcho khởi tạo phụ thuộc vào dependency đã inject.
Phạm vi Bean (Bean Scope)
Mặc định, Spring bean là singleton — chỉ một instance được tạo và chia sẻ trong toàn bộ ứng dụng. Điều này quan trọng cần hiểu:
@Service
public class UserService {
// Chỉ có MỘT instance của UserService trong toàn bộ ứng dụng.
// Mọi class phụ thuộc vào UserService đều nhận CÙNG instance.
}
Bạn có thể thay đổi scope, nhưng singleton là lựa chọn đúng cho 99% bean trong ứng dụng web:
// Singleton (mặc định) — một instance cho toàn bộ ứng dụng
@Scope("singleton")
// Prototype — instance mới mỗi khi ai đó yêu cầu bean này
@Scope("prototype")
// Request — một instance cho mỗi HTTP request (chỉ ứng dụng web)
@Scope("request")
// Session — một instance cho mỗi HTTP session (chỉ ứng dụng web)
@Scope("session")
Trong series này, chúng ta sẽ sử dụng scope singleton mặc định cho tất cả bean. Chỉ cần nhớ: vì bean là singleton, chúng nên không có trạng thái (stateless) — không lưu dữ liệu đặc thù cho request trong các trường instance.
Các Annotation quan trọng: @Component, @Service, @Repository, @Controller
Spring cung cấp bốn annotation để đánh dấu class là bean. Chúng được gọi là stereotype annotation, và tất cả đều làm cùng một việc cơ bản — đăng ký class là bean trong Application Context. Sự khác biệt là về ngữ nghĩa, nghĩa là chúng truyền đạt mục đích của class cho các lập trình viên khác.
@Component — Annotation chung
@Component là annotation cơ sở. Nó chỉ đơn giản nói: “Class này là bean được Spring quản lý.”
@Component
public class EmailValidator {
public boolean isValid(String email) {
return email != null && email.contains("@");
}
}
Sử dụng @Component khi class không thuộc gọn vào một trong các danh mục dưới đây.
@Service — Tầng Logic nghiệp vụ
@Service là chuyên biệt hóa của @Component. Nó chỉ ra rằng class chứa logic nghiệp vụ.
@Service
public class UserService {
private final UserRepository userRepository;
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
public User createUser(String name, String email) {
// Kiểm tra đầu vào
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("Tên không được để trống");
}
// Logic nghiệp vụ ở đây
User user = new User(name, email);
return userRepository.save(user);
}
}
@Repository — Tầng Truy cập dữ liệu
@Repository là chuyên biệt hóa của @Component cho class tương tác với cơ sở dữ liệu. Nó có thêm một hành vi: Spring tự động chuyển đổi exception đặc thù database thành hệ thống DataAccessException của Spring, giúp xử lý lỗi nhất quán giữa các database khác nhau.
@Repository
public class UserRepository {
// Trong các bài sau, chúng ta sẽ sử dụng Spring Data JPA và
// không phải viết implementation repository thủ công. Hiện tại,
// chúng ta dùng danh sách trong bộ nhớ để mô phỏng thao tác database.
private final List<User> users = new ArrayList<>();
private long nextId = 1;
public User save(User user) {
user.setId(nextId++);
users.add(user);
return user;
}
public Optional<User> findById(Long id) {
return users.stream()
.filter(u -> u.getId().equals(id))
.findFirst();
}
public List<User> findAll() {
return Collections.unmodifiableList(users);
}
}
@Controller / @RestController — Tầng Web
@Controller và @RestController là chuyên biệt hóa của @Component cho class xử lý HTTP request. Chúng ta đã sử dụng @RestController trong Bài 1.
Sự khác biệt giữa chúng:
@Controller— Trả về tên view (template HTML). Dùng cho trang web render phía server.@RestController— Trả về dữ liệu trực tiếp (JSON). Dùng cho REST API. Nó là@Controller+@ResponseBody.
@RestController
public class UserController {
private final UserService userService;
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/api/users")
public List<User> getAllUsers() {
return userService.findAllUsers();
}
}
Kiến trúc phân tầng
Các annotation này tự nhiên tổ chức code của bạn thành các tầng:
HTTP Request
↓
@RestController (Tầng Web — xử lý HTTP, ủy quyền cho service)
↓
@Service (Tầng Service — logic nghiệp vụ, validation, điều phối)
↓
@Repository (Tầng Truy cập dữ liệu — thao tác database)
↓
Database
Mỗi tầng có trách nhiệm rõ ràng:
- Controller: Nhận HTTP request, trích xuất tham số, gọi service, trả về HTTP response. Không có logic nghiệp vụ ở đây.
- Service: Chứa tất cả logic nghiệp vụ — validation, tính toán, điều phối nhiều repository. Không biết về HTTP.
- Repository: Xử lý truy cập dữ liệu — thao tác CRUD trên database. Không biết về quy tắc nghiệp vụ hay HTTP.
Sự phân tách này rất quan trọng. Nó làm code module hóa, dễ kiểm thử, và dễ bảo trì. Chúng ta sẽ tuân theo pattern này cho toàn bộ series.
Về mặt kỹ thuật, chúng giống nhau
Nếu bạn nhìn vào source code của @Service, bạn sẽ thấy:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // <-- @Service CHÍNH LÀ @Component
public @interface Service {
String value() default "";
}
@Service đúng nghĩa là @Component với tên khác. Tương tự cho @Repository và @Controller. Tất cả đều đăng ký class là bean. Sự khác biệt hoàn toàn để:
- Dễ đọc code — lập trình viên khác biết ngay vai trò của class
- Tính năng framework —
@Repositoryđược dịch exception,@Controllerđược xử lý web - AOP targeting — bạn có thể áp dụng cross-cutting concern cho tất cả class
@Service, ví dụ
@Autowired và tại sao Constructor Injection được ưu tiên
@Autowired hoạt động như thế nào
@Autowired cho Spring biết: “Tìm một bean kiểu này trong Application Context và inject vào đây.”
@Service
public class NotificationService {
@Autowired // Spring tìm bean kiểu EmailService và inject
private EmailService emailService;
}
Nhưng đây là tin tốt: bạn thường không cần @Autowired.
Điều kỳ diệu của Single-Constructor Injection
Bắt đầu từ Spring 4.3, nếu class chỉ có một constructor, Spring tự động sử dụng nó cho dependency injection — không cần @Autowired:
@Service
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;
// Chỉ một constructor → Spring tự động sử dụng cho DI.
// Không cần @Autowired!
public NotificationService(EmailService emailService,
SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
}
Đây là cách tiếp cận sạch nhất và được khuyến nghị nhất. Không annotation trên constructor, trường final, và tối đa sự rõ ràng.
Nếu class có nhiều constructor, bạn phải đánh dấu constructor bạn muốn Spring sử dụng với @Autowired:
@Service
public class NotificationService {
private final EmailService emailService;
private final SmsService smsService;
@Autowired // Bắt buộc vì có nhiều constructor
public NotificationService(EmailService emailService,
SmsService smsService) {
this.emailService = emailService;
this.smsService = smsService;
}
// Constructor thứ hai — Spring SẼ KHÔNG sử dụng cái này
public NotificationService(EmailService emailService) {
this(emailService, null);
}
}
Tại sao Constructor Injection được ưu tiên: Ví dụ cụ thể
Hãy xem tại sao constructor injection quan trọng với ví dụ kiểm thử.
Với field injection (xấu):
@Service
public class OrderService {
@Autowired
private PaymentGateway paymentGateway;
@Autowired
private EmailService emailService;
public void placeOrder(Order order) {
paymentGateway.charge(order.getTotal());
emailService.sendConfirmation(order);
}
}
Để test class này, bạn cần Spring hoặc công cụ dựa trên reflection như @InjectMocks:
// Không có Spring, bạn KHÔNG THỂ tạo đối tượng này với dependency
OrderService service = new OrderService();
// service.paymentGateway là null!
// service.emailService là null!
// service.placeOrder(order) → NullPointerException!
Với constructor injection (tốt):
@Service
public class OrderService {
private final PaymentGateway paymentGateway;
private final EmailService emailService;
public OrderService(PaymentGateway paymentGateway,
EmailService emailService) {
this.paymentGateway = paymentGateway;
this.emailService = emailService;
}
public void placeOrder(Order order) {
paymentGateway.charge(order.getTotal());
emailService.sendConfirmation(order);
}
}
Bây giờ kiểm thử đơn giản — không cần Spring:
// Tạo mock implementation
PaymentGateway mockPayment = new FakePaymentGateway();
EmailService mockEmail = new FakeEmailService();
// Tạo service với dependency — chỉ là Java thuần!
OrderService service = new OrderService(mockPayment, mockEmail);
// Test
service.placeOrder(testOrder);
// Xác minh mockPayment đã tính phí, mockEmail đã gửi, v.v.
Khi Spring không tìm thấy Bean
Nếu Spring không tìm thấy bean của kiểu yêu cầu, ứng dụng sẽ không khởi động. Bạn sẽ thấy lỗi như:
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in com.example.OrderService
required a bean of type 'com.example.PaymentGateway'
that could not be found.
Action:
Consider defining a bean of type 'com.example.PaymentGateway'
in your configuration.
Đây thực ra là tính năng, không phải lỗi. Hành vi fail-fast bắt vấn đề cấu hình khi khởi động, không phải lúc runtime khi người dùng truy cập endpoint bị ảnh hưởng.
Khi có nhiều Bean cùng kiểu
Nếu bạn có hai implementation của cùng interface:
public interface PaymentGateway {
void charge(double amount);
}
@Component
public class StripePaymentGateway implements PaymentGateway {
public void charge(double amount) { /* Logic Stripe */ }
}
@Component
public class PayPalPaymentGateway implements PaymentGateway {
public void charge(double amount) { /* Logic PayPal */ }
}
Và một class phụ thuộc vào PaymentGateway:
@Service
public class OrderService {
// Spring tìm thấy HAI bean kiểu PaymentGateway — inject cái nào?
public OrderService(PaymentGateway paymentGateway) { ... }
}
Spring sẽ thất bại với NoUniqueBeanDefinitionException. Bạn có ba cách giải quyết:
Cách 1: @Primary — Đánh dấu một implementation làm mặc định:
@Component
@Primary // Cái này sẽ được inject khi có nhiều ứng viên
public class StripePaymentGateway implements PaymentGateway { ... }
Cách 2: @Qualifier — Chỉ định bạn muốn cái nào theo tên:
@Service
public class OrderService {
public OrderService(
@Qualifier("payPalPaymentGateway") PaymentGateway paymentGateway) {
// Spring sẽ inject PayPalPaymentGateway cụ thể
}
}
Cách 3: Khớp theo tên tham số — Đặt tên tham số constructor khớp với tên bean:
@Service
public class OrderService {
// Tên tham số "stripePaymentGateway" khớp với tên bean
public OrderService(PaymentGateway stripePaymentGateway) { ... }
}
Trong hầu hết trường hợp, @Primary là cách đơn giản nhất. Sử dụng @Qualifier khi bạn cần kiểm soát rõ ràng.
Auto-Configuration hoạt động như thế nào phía sau
Giải mã phép thuật
Auto-configuration là thứ làm Spring Boot cảm giác như phép thuật. Khi bạn thêm spring-boot-starter-web vào pom.xml, Spring Boot tự động:
- Cấu hình server Tomcat nhúng
- Thiết lập Spring MVC với cài đặt mặc định
- Đăng ký Jackson cho JSON serialization
- Cấu hình xử lý lỗi mặc định
Nhưng làm sao nó biết phải làm điều này?
Cơ chế
Annotation @SpringBootApplication bao gồm @EnableAutoConfiguration, kích hoạt quá trình auto-configuration. Đây là những gì xảy ra:
- Spring Boot nhìn vào classpath — các file JAR trong dependency của dự án.
- Nó kiểm tra danh sách auto-configuration class (có hàng trăm) và đánh giá điều kiện cho từng cái.
- Nếu các điều kiện được đáp ứng (class nhất định có mặt, bean nhất định chưa tồn tại), nó tạo và cấu hình bean tự động.
Ví dụ, class DataSourceAutoConfiguration có logic như thế này (đơn giản hóa):
NẾU class "DataSource" có trên classpath
VÀ property "spring.datasource.url" được cấu hình
VÀ chưa có bean kiểu DataSource
THÌ tạo bean DataSource tự động
Conditional Annotation
Các class auto-configuration sử dụng conditional annotation để quyết định có kích hoạt hay không:
// Chỉ kích hoạt nếu class HikariDataSource có trên classpath
@ConditionalOnClass(HikariDataSource.class)
// Chỉ kích hoạt nếu chưa có bean kiểu DataSource
@ConditionalOnMissingBean(DataSource.class)
// Chỉ kích hoạt nếu property được thiết lập
@ConditionalOnProperty(name = "spring.datasource.url")
Điều này có nghĩa:
- Nếu bạn không thêm database driver vào
pom.xml, không có DataSource nào được cấu hình. - Nếu bạn tạo bean DataSource riêng, auto-configuration của Spring Boot lùi lại và sử dụng cái của bạn.
- Nếu bạn không thiết lập
spring.datasource.url, auto-configuration bỏ qua việc thiết lập database.
Xem Auto-Configuration hoạt động
Bạn có thể xem chính xác những gì đã được auto-configure bằng cách thêm dòng này vào application.properties:
# Hiển thị auto-configuration nào được áp dụng và bỏ qua
debug=true
Khi bạn khởi động lại ứng dụng, bạn sẽ thấy báo cáo chi tiết trong console:
============================
CONDITIONS EVALUATION REPORT
============================
Positive matches: (cấu hình ĐÃ được áp dụng)
-----------------
JacksonAutoConfiguration matched:
- @ConditionalOnClass found required class 'com.fasterxml.jackson.databind.ObjectMapper'
WebMvcAutoConfiguration matched:
- @ConditionalOnClass found required class 'jakarta.servlet.Servlet'
Negative matches: (cấu hình ĐÃ BỊ BỎ QUA)
-----------------
DataSourceAutoConfiguration:
- @ConditionalOnClass did not find required class 'javax.sql.DataSource'
Báo cáo này cực kỳ hữu ích cho debug. Nếu gì đó không hoạt động như mong đợi, hãy kiểm tra đây trước.
Điểm mấu chốt
Auto-configuration không phải phép thuật — nó chỉ là tạo bean có điều kiện. Spring Boot kiểm tra bạn có thư viện gì, kiểm tra bạn đã định nghĩa bean gì, và lấp đầy khoảng trống bằng giá trị mặc định hợp lý. Bạn luôn có thể ghi đè bất kỳ bean auto-configure nào bằng cách định nghĩa bean của riêng mình.
application.properties vs application.yml
Spring Boot hỗ trợ hai định dạng cho file cấu hình. Chúng hoàn toàn tương đương về chức năng — sự khác biệt chỉ là về cú pháp.
application.properties
Đây là định dạng properties Java truyền thống — các cặp key-value:
# Cấu hình server
server.port=8080
server.servlet.context-path=/api
# Cấu hình database (chúng ta sẽ dùng trong Bài 5)
spring.datasource.url=jdbc:mariadb://localhost:3306/blogdb
spring.datasource.username=root
spring.datasource.password=secret
# Cấu hình JPA
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.format_sql=true
# Logging
logging.level.root=INFO
logging.level.com.example.blogapi=DEBUG
Ưu điểm:
- Đơn giản và phẳng
- Quen thuộc với lập trình viên Java
- Dễ thiết lập từng property từ dòng lệnh
application.yml
Đây là định dạng YAML — sử dụng thụt dòng để biểu diễn cấu trúc phân cấp:
# Cấu hình server
server:
port: 8080
servlet:
context-path: /api
# Cấu hình database
spring:
datasource:
url: jdbc:mariadb://localhost:3306/blogdb
username: root
password: secret
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
# Logging
logging:
level:
root: INFO
com.example.blogapi: DEBUG
Ưu điểm:
- Cấu trúc phân cấp trực quan hơn — property lồng nhau dễ đọc hơn
- Ít lặp lại — không cần lặp tiền tố cho mọi property
- Hỗ trợ cấu trúc dữ liệu phức tạp (list, map) tự nhiên hơn
Nên chọn cái nào?
Cả hai hoạt động giống hệt nhau. Cộng đồng Spring hơi nghiêng về YAML cho dự án mới vì dễ đọc hơn, đặc biệt khi cấu hình phát triển. Trong series này, chúng ta sẽ sử dụng application.properties vì tính đơn giản, nhưng hãy thoải mái dùng YAML nếu bạn thích.
Quan trọng: Không sử dụng cả hai file trong cùng một dự án. Nếu cả hai tồn tại,
application.propertiescó ưu tiên cao hơn, có thể gây nhầm lẫn.
Property tùy chỉnh
Bạn có thể định nghĩa property tùy chỉnh và đọc chúng trong code:
# Property tùy chỉnh
blog.app.name=My Blog API
blog.app.version=1.0.0
blog.app.max-posts-per-page=20
Đọc chúng trong code với @Value:
@Service
public class AppInfoService {
// @Value inject giá trị property từ application.properties.
// Cú pháp ${property.name} tham chiếu một key property.
// Phần sau ":" là giá trị mặc định nếu property không tìm thấy.
@Value("${blog.app.name:Default Blog}")
private String appName;
@Value("${blog.app.version:0.0.1}")
private String appVersion;
@Value("${blog.app.max-posts-per-page:10}")
private int maxPostsPerPage;
public String getAppInfo() {
return appName + " v" + appVersion;
}
}
Cho nhóm property liên quan, sử dụng @ConfigurationProperties (cách tiếp cận an toàn kiểu hơn):
@Component
@ConfigurationProperties(prefix = "blog.app")
public class BlogProperties {
// Spring ánh xạ blog.app.name → name, blog.app.version → version, v.v.
// Tên trường phải khớp với tên property (sau tiền tố).
private String name;
private String version;
private int maxPostsPerPage;
// Getter và setter bắt buộc cho @ConfigurationProperties
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public int getMaxPostsPerPage() { return maxPostsPerPage; }
public void setMaxPostsPerPage(int maxPostsPerPage) {
this.maxPostsPerPage = maxPostsPerPage;
}
}
Sau đó inject BlogProperties ở bất kỳ đâu bạn cần:
@Service
public class PostService {
private final BlogProperties blogProperties;
public PostService(BlogProperties blogProperties) {
this.blogProperties = blogProperties;
}
public int getPageSize() {
return blogProperties.getMaxPostsPerPage();
}
}
Profile — Quản lý các môi trường khác nhau
Vấn đề
Ứng dụng của bạn hoạt động khác nhau ở các môi trường khác nhau:
- Development: Bạn muốn logging chi tiết, database trong bộ nhớ, và bảo mật lỏng lẻo.
- Testing: Bạn muốn database test và mock các service bên ngoài.
- Production: Bạn muốn hiệu suất tối ưu, database thật, bảo mật nghiêm ngặt, và logging tối thiểu.
Hardcode các thiết lập này nghĩa là phải thay đổi code mỗi khi triển khai sang môi trường khác. Điều đó dễ lỗi và phiền phức.
Spring Profile giải cứu
Một profile là tập hợp cấu hình được đặt tên, có thể kích hoạt hoặc hủy kích hoạt. Bạn tạo file cấu hình đặc thù cho profile bằng cách thêm tên profile làm hậu tố:
src/main/resources/
├── application.properties ← Luôn được tải (thiết lập chung)
├── application-dev.properties ← Tải khi profile "dev" được kích hoạt
├── application-test.properties ← Tải khi profile "test" được kích hoạt
└── application-prod.properties ← Tải khi profile "prod" được kích hoạt
application.properties (thiết lập chung — luôn được tải):
# Thiết lập chia sẻ giữa tất cả môi trường
blog.app.name=My Blog API
server.servlet.context-path=/api
application-dev.properties (thiết lập development):
# Thiết lập đặc thù cho development
server.port=8080
logging.level.root=DEBUG
logging.level.com.example.blogapi=TRACE
spring.datasource.url=jdbc:mariadb://localhost:3306/blog_dev
spring.datasource.username=root
spring.datasource.password=devpassword
spring.jpa.show-sql=true
application-prod.properties (thiết lập production):
# Thiết lập đặc thù cho production
server.port=80
logging.level.root=WARN
logging.level.com.example.blogapi=INFO
spring.datasource.url=jdbc:mariadb://prod-db-server:3306/blog_prod
spring.datasource.username=blog_app
spring.datasource.password=${DB_PASSWORD}
spring.jpa.show-sql=false
Lưu ý ${DB_PASSWORD} trong cấu hình production — nó đọc giá trị từ biến môi trường. Bạn không bao giờ nên hardcode mật khẩu production trong file cấu hình.
Kích hoạt Profile
Có nhiều cách để kích hoạt profile:
Cách 1: Trong application.properties
# Thiết lập profile active mặc định
spring.profiles.active=dev
Cách 2: Tham số dòng lệnh
java -jar blog-api.jar --spring.profiles.active=prod
Cách 3: Biến môi trường
export SPRING_PROFILES_ACTIVE=prod
java -jar blog-api.jar
Cách 4: Trong IDE
Trong IntelliJ IDEA: Run Configuration → Environment Variables → Thêm SPRING_PROFILES_ACTIVE=dev
Bean đặc thù cho Profile
Bạn cũng có thể tạo bean chỉ tồn tại trong profile cụ thể:
@Service
@Profile("dev")
public class FakeEmailService implements EmailService {
// Trong development, chỉ in ra console thay vì gửi email thật
@Override
public void sendEmail(String to, String subject, String body) {
System.out.println("=== EMAIL GIẢ ===");
System.out.println("Đến: " + to);
System.out.println("Tiêu đề: " + subject);
System.out.println("Nội dung: " + body);
System.out.println("=================");
}
}
@Service
@Profile("prod")
public class SmtpEmailService implements EmailService {
// Trong production, gửi email thật qua SMTP
@Override
public void sendEmail(String to, String subject, String body) {
// Logic gửi email SMTP thật
}
}
Khi profile dev được kích hoạt, Spring tạo FakeEmailService. Khi prod được kích hoạt, nó tạo SmtpEmailService. Phần còn lại của code chỉ phụ thuộc vào interface EmailService và không quan tâm implementation nào đang được sử dụng — đó là sức mạnh của Dependency Injection kết hợp với profile.
Thực hành tốt cho Profile
- Sử dụng
devlàm profile mặc định cho phát triển local. - Không bao giờ commit mật khẩu production vào repository. Sử dụng biến môi trường.
- Giữ
application.propertieschung tối giản — chỉ thiết lập thực sự chia sẻ. - Bạn có thể kích hoạt nhiều profile:
spring.profiles.active=prod,email,monitoring
Thực hành: Xây dựng Service Layer đơn giản với DI
Hãy gộp mọi thứ lại bằng cách xây dựng một ứng dụng có cấu trúc, nhiều tầng. Chúng ta sẽ tạo hệ thống quản lý người dùng đơn giản tuân theo pattern Controller → Service → Repository.
Bước 1: Tạo Model User
File: src/main/java/com/example/blogapi/model/User.java
package com.example.blogapi.model;
// Đây là class Java đơn giản (POJO — Plain Old Java Object).
// Nó đại diện cho một người dùng trong ứng dụng.
// Trong các bài sau, chúng ta sẽ thêm annotation JPA để ánh xạ nó đến bảng database.
public class User {
private Long id;
private String name;
private String email;
// Constructor mặc định — bắt buộc cho Jackson deserialization.
// Khi client gửi dữ liệu JSON, Jackson cần constructor này để tạo đối tượng User.
public User() {
}
// Constructor có tham số — tiện lợi cho việc tạo đối tượng User trong code.
public User(String name, String email) {
this.name = name;
this.email = email;
}
// Getter và setter — bắt buộc để Jackson chuyển đổi giữa
// đối tượng Java và JSON. Jackson đọc getter cho serialization
// (Java → JSON) và dùng setter cho deserialization (JSON → Java).
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
// toString() hữu ích cho logging và debug
@Override
public String toString() {
return "User{id=" + id + ", name='" + name + "', email='" + email + "'}";
}
}
Bước 2: Tạo tầng Repository
File: src/main/java/com/example/blogapi/repository/UserRepository.java
package com.example.blogapi.repository;
import com.example.blogapi.model.User;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
// @Repository đánh dấu đây là component truy cập dữ liệu.
// Spring sẽ tạo một singleton instance và quản lý nó.
// Hiện tại, chúng ta dùng List trong bộ nhớ để mô phỏng database.
// Trong Bài 5, chúng ta sẽ thay thế bằng Spring Data JPA + MariaDB.
@Repository
public class UserRepository {
// Database mô phỏng — danh sách đơn giản lưu trong bộ nhớ.
// CẢNH BÁO: Dữ liệu sẽ mất khi ứng dụng khởi động lại!
// Database thật giải quyết vấn đề này. Chúng ta sẽ thêm MariaDB ở Bài 5.
private final List<User> users = new ArrayList<>();
// Bộ đếm ID tự tăng, mô phỏng sequence của database
private long nextId = 1;
// Lưu người dùng — tương tự SQL INSERT
public User save(User user) {
user.setId(nextId++);
users.add(user);
return user;
}
// Tìm người dùng theo ID — tương tự SQL SELECT WHERE id = ?
// Trả về Optional vì người dùng có thể không tồn tại.
// Optional buộc người gọi phải xử lý trường hợp "không tìm thấy" một cách rõ ràng.
public Optional<User> findById(Long id) {
return users.stream()
.filter(user -> user.getId().equals(id))
.findFirst();
}
// Tìm tất cả người dùng — tương tự SQL SELECT *
// Trả về danh sách không thể sửa đổi để ngăn code bên ngoài
// vô tình sửa "database" của chúng ta
public List<User> findAll() {
return Collections.unmodifiableList(users);
}
// Xóa người dùng theo ID — tương tự SQL DELETE WHERE id = ?
// Trả về true nếu tìm và xóa được, false nếu không
public boolean deleteById(Long id) {
return users.removeIf(user -> user.getId().equals(id));
}
// Đếm tổng người dùng — tương tự SQL SELECT COUNT(*)
public long count() {
return users.size();
}
}
Bước 3: Tạo tầng Service
File: src/main/java/com/example/blogapi/service/UserService.java
package com.example.blogapi.service;
import com.example.blogapi.model.User;
import com.example.blogapi.repository.UserRepository;
import org.springframework.stereotype.Service;
import java.util.List;
// @Service đánh dấu class này là component logic nghiệp vụ.
// Nó nằm giữa Controller (tầng HTTP) và Repository (tầng dữ liệu).
// Tất cả quy tắc nghiệp vụ, validation, và logic điều phối nằm ở đây.
@Service
public class UserService {
// Dependency khai báo là trường final — không thể thay đổi sau khi khởi tạo
private final UserRepository userRepository;
// Constructor injection — Spring thấy constructor này, tìm bean kiểu
// UserRepository trong Application Context, và truyền vào.
// Vì đây là constructor DUY NHẤT, không cần @Autowired.
public UserService(UserRepository userRepository) {
this.userRepository = userRepository;
}
// Phương thức nghiệp vụ: Tạo người dùng mới với validation
public User createUser(String name, String email) {
// Quy tắc nghiệp vụ: tên không được rỗng
if (name == null || name.trim().isEmpty()) {
throw new IllegalArgumentException("Tên người dùng không được để trống");
}
// Quy tắc nghiệp vụ: email phải chứa @
if (email == null || !email.contains("@")) {
throw new IllegalArgumentException("Địa chỉ email không hợp lệ: " + email);
}
// Quy tắc nghiệp vụ: cắt khoảng trắng từ tên
name = name.trim();
// Tạo người dùng và ủy quyền cho repository để lưu trữ
User user = new User(name, email.trim().toLowerCase());
return userRepository.save(user);
}
// Phương thức nghiệp vụ: Lấy người dùng theo ID
// Ném exception nếu không tìm thấy — controller sẽ xử lý
public User getUserById(Long id) {
return userRepository.findById(id)
.orElseThrow(() -> new RuntimeException(
"Không tìm thấy người dùng với id: " + id));
}
// Phương thức nghiệp vụ: Lấy tất cả người dùng
public List<User> getAllUsers() {
return userRepository.findAll();
}
// Phương thức nghiệp vụ: Xóa người dùng
public void deleteUser(Long id) {
boolean deleted = userRepository.deleteById(id);
if (!deleted) {
throw new RuntimeException("Không tìm thấy người dùng với id: " + id);
}
}
// Phương thức nghiệp vụ: Đếm số người dùng
public long getUserCount() {
return userRepository.count();
}
}
Bước 4: Tạo tầng Controller
File: src/main/java/com/example/blogapi/controller/UserController.java
package com.example.blogapi.controller;
import com.example.blogapi.model.User;
import com.example.blogapi.service.UserService;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
// @RestController = @Controller + @ResponseBody
// Mọi phương thức trả về dữ liệu (JSON), không phải view (template HTML)
@RestController
@RequestMapping("/api/users") // Đường dẫn gốc — tất cả endpoint trong controller này bắt đầu với /api/users
public class UserController {
private final UserService userService;
// Constructor injection — Spring inject bean UserService
public UserController(UserService userService) {
this.userService = userService;
}
// GET /api/users — Lấy tất cả người dùng
@GetMapping
public List<User> getAllUsers() {
// Controller chỉ đơn giản ủy quyền cho service.
// Không có logic nghiệp vụ ở đây — đó thuộc về tầng service.
return userService.getAllUsers();
}
// GET /api/users/{id} — Lấy một người dùng cụ thể
// @PathVariable trích xuất {id} từ đường dẫn URL.
// Ví dụ: GET /api/users/42 → id = 42
@GetMapping("/{id}")
public User getUserById(@PathVariable Long id) {
return userService.getUserById(id);
}
// POST /api/users — Tạo người dùng mới
// @RequestBody yêu cầu Spring phân tích phần thân HTTP request (JSON)
// và chuyển đổi thành đối tượng Map.
//
// ResponseEntity cho chúng ta quyền kiểm soát mã trạng thái HTTP.
// Chúng ta trả về 201 (Created) thay vì 200 (OK) vì tài nguyên mới đã được tạo.
@PostMapping
public ResponseEntity<User> createUser(@RequestBody Map<String, String> request) {
String name = request.get("name");
String email = request.get("email");
User createdUser = userService.createUser(name, email);
// Trả về người dùng đã tạo với trạng thái HTTP 201
return new ResponseEntity<>(createdUser, HttpStatus.CREATED);
}
// DELETE /api/users/{id} — Xóa người dùng
// Trả về thông báo xác nhận thay vì người dùng đã xóa
@DeleteMapping("/{id}")
public Map<String, String> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
Map<String, String> response = new HashMap<>();
response.put("message", "Xóa người dùng thành công");
response.put("deletedId", id.toString());
return response;
}
// GET /api/users/count — Đếm tổng số người dùng
@GetMapping("/count")
public Map<String, Long> getUserCount() {
Map<String, Long> response = new HashMap<>();
response.put("count", userService.getUserCount());
return response;
}
}
Bước 5: Hiểu chuỗi Dependency
Hãy hình dung cách Spring kết nối mọi thứ khi ứng dụng khởi động:
1. Spring quét package và tìm thấy:
- UserRepository (@Repository)
- UserService (@Service) — cần UserRepository
- UserController (@RestController) — cần UserService
2. Spring tạo bean theo thứ tự dependency:
a) UserRepository được tạo trước (không có dependency)
b) UserService được tạo, nhận UserRepository qua constructor
c) UserController được tạo, nhận UserService qua constructor
3. Luồng HTTP request:
Client gửi: POST /api/users {"name":"Alice","email":"alice@example.com"}
↓
UserController.createUser() — trích xuất name và email từ JSON
↓
UserService.createUser() — validate đầu vào, tạo đối tượng User
↓
UserRepository.save() — lưu người dùng, gán ID
↓
Response đi ngược lên: User{id=1, name="Alice", email="alice@example.com"}
↓
Client nhận: HTTP 201 {"id":1,"name":"Alice","email":"alice@example.com"}
Bước 6: Kiểm thử với curl hoặc Postman
Khởi động ứng dụng và kiểm thử từng endpoint:
# Tạo người dùng
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Alice", "email": "alice@example.com"}'
# Kết quả mong đợi (HTTP 201):
# {"id":1,"name":"Alice","email":"alice@example.com"}
# Tạo người dùng khác
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "Bob", "email": "bob@example.com"}'
# Kết quả mong đợi (HTTP 201):
# {"id":2,"name":"Bob","email":"bob@example.com"}
# Lấy tất cả người dùng
curl http://localhost:8080/api/users
# Kết quả mong đợi:
# [{"id":1,"name":"Alice","email":"alice@example.com"},
# {"id":2,"name":"Bob","email":"bob@example.com"}]
# Lấy người dùng cụ thể
curl http://localhost:8080/api/users/1
# Kết quả mong đợi:
# {"id":1,"name":"Alice","email":"alice@example.com"}
# Đếm số người dùng
curl http://localhost:8080/api/users/count
# Kết quả mong đợi:
# {"count":2}
# Xóa người dùng
curl -X DELETE http://localhost:8080/api/users/1
# Kết quả mong đợi:
# {"message":"Xóa người dùng thành công","deletedId":"1"}
# Thử tạo người dùng với dữ liệu không hợp lệ
curl -X POST http://localhost:8080/api/users \
-H "Content-Type: application/json" \
-d '{"name": "", "email": "invalid"}'
# Kết quả mong đợi (HTTP 500 — chúng ta sẽ cải thiện xử lý lỗi ở Bài 8):
# {"timestamp":"...","status":500,"error":"Internal Server Error",...}
Bước 7: Bài tập
Hãy thử các bài tập sau để củng cố khái niệm từ bài giảng này:
- Thêm phương thức update: Thêm endpoint
PUT /api/users/{id}để cập nhật tên và email người dùng. Bạn sẽ cần thêm phương thứcupdatevào tầng repository và service. - Thêm phương thức tìm kiếm: Thêm endpoint
GET /api/users/search?name=Alitrả về tất cả người dùng có tên chứa chuỗi tìm kiếm (không phân biệt hoa thường). Thêm phương thứcfindByNameContainingvào repository. - Thêm logging: Sử dụng
System.out.println(chúng ta sẽ dùng logging đúng cách ở Bài 9) để in thông báo ở mỗi tầng khi request được xử lý. Quan sát luồng: Controller → Service → Repository → Service → Controller. - Tạo bean đặc thù profile: Tạo
WelcomeServicevới hai implementation — một cho profiledevtrả về “Chào mừng đến Môi trường Dev!” và một cho profileprodtrả về “Chào mừng đến Production!”. Tạo endpoint/welcomeđể kiểm thử.
Tổng kết
Bài giảng này đã cover các khái niệm nền tảng vận hành mọi thứ trong Spring Boot:
- Inversion of Control (IoC): Thay vì class tự tạo dependency, Spring container tạo và cung cấp chúng. Điều này dẫn đến code liên kết lỏng, dễ kiểm thử, và dễ bảo trì.
- Dependency Injection (DI): Cơ chế chuyển giao dependency cho class. Luôn sử dụng constructor injection — nó đảm bảo tính bất biến, trạng thái hợp lệ, và dễ kiểm thử.
- Application Context: Container chứa tất cả bean (đối tượng được quản lý). Spring tạo bean, giải quyết dependency, và quản lý vòng đời tự động.
- Stereotype Annotation: Sử dụng
@Controller/@RestControllercho tầng web,@Servicecho logic nghiệp vụ,@Repositorycho truy cập dữ liệu, và@Componentcho mọi thứ khác. - Auto-Configuration: Spring Boot kiểm tra classpath và tự động cấu hình bean dựa trên thư viện có sẵn. Bạn có thể ghi đè bất kỳ bean auto-configure nào.
- Cấu hình: Sử dụng
application.propertieshoặcapplication.ymlđể cấu hình ứng dụng. Đọc giá trị với@Valuehoặc@ConfigurationProperties. - Profile: Tạo cấu hình đặc thù môi trường (
application-dev.properties,application-prod.properties) và kích hoạt chúng khi chạy. - Kiến trúc phân tầng: Tổ chức code thành các tầng Controller → Service → Repository, mỗi tầng có trách nhiệm rõ ràng.
Bài tiếp theo
Trong Bài 3, chúng ta sẽ đi sâu hơn vào Xây dựng REST API với Spring MVC. Bạn sẽ học về tất cả phương thức HTTP, path variable, request body, response entity, mã trạng thái, và cách thiết kế REST API sạch. Chúng ta sẽ mở rộng hệ thống quản lý người dùng thành CRUD API hoàn chỉnh với các best practice.
Tham chiếu nhanh
| Khái niệm | Mô tả |
|---|---|
| IoC (Inversion of Control) | Nguyên tắc thiết kế nơi framework kiểm soát việc tạo và vòng đời đối tượng |
| DI (Dependency Injection) | Framework cung cấp dependency cho class qua constructor, setter, hoặc field |
| Bean | Đối tượng Java được quản lý bởi Spring IoC container |
| Application Context | Container chứa và quản lý tất cả bean |
@Component |
Stereotype chung — đăng ký class là bean |
@Service |
Stereotype cho class logic nghiệp vụ |
@Repository |
Stereotype cho class truy cập dữ liệu (thêm dịch exception) |
@RestController |
Stereotype cho controller REST API |
@Autowired |
Yêu cầu Spring inject dependency (ngầm định với constructor đơn) |
@Primary |
Đánh dấu bean làm mặc định khi có nhiều ứng viên |
@Qualifier |
Chỉ định bean nào để inject theo tên |
@Value |
Inject giá trị property từ cấu hình |
@ConfigurationProperties |
Gắn nhóm property vào class Java |
@Profile |
Giới hạn bean cho profile cụ thể |
@PostConstruct |
Phương thức được gọi sau khi tạo bean và DI |
@PreDestroy |
Phương thức được gọi trước khi bean bị hủy |
| Auto-Configuration | Spring Boot tự động cấu hình bean dựa trên classpath |
spring.profiles.active |
Property để kích hoạt một hoặc nhiều profile |
