Chào mừng quay trở lại với loạt bài “Lộ trình Java Spring”! Nếu bạn đã theo dõi, bạn đã khám phá lý do Spring là lựa chọn phổ biến và chúng ta thậm chí đã cố gắng giải thích các khái niệm cốt lõi như IoC và Dependency Injection (DI) một cách đơn giản.
Giờ đây, đã đến lúc vén màn và xem Spring thực sự hoạt động *như thế nào*. Hiểu được kiến trúc cơ bản của nó không chỉ mang tính học thuật; nó giúp bạn sử dụng framework hiệu quả hơn, khắc phục sự cố và xây dựng các ứng dụng mạnh mẽ. Hãy coi nó như việc hiểu nền móng trước khi bạn bắt đầu xây nhà.
Trong bài viết này, chúng ta sẽ khám phá các trụ cột kiến trúc chính của Spring Framework. Đừng lo, chúng tôi sẽ giữ nó thân thiện với người mới bắt đầu, tập trung vào các ý tưởng cốt lõi làm nên Spring.
Mục lục
Nền tảng: Đảo Ngược Điều Khiển (IoC) và Tiêm Phụ Thuộc (DI)
Chúng ta đã nói về điều này trước đây, nhưng nó rất cơ bản nên chúng ta phải bắt đầu từ đây. Về cốt lõi, kiến trúc của Spring được xây dựng dựa trên nguyên tắc **Đảo Ngược Điều Khiển (IoC)**.
Theo truyền thống, trong lập trình hướng đối tượng, các đối tượng của bạn chịu trách nhiệm tạo và quản lý các phụ thuộc của chúng (các đối tượng khác mà chúng cần để hoạt động).
// Cách tiếp cận truyền thống: Đối tượng tự tạo phụ thuộc
public class MyService {
private MyRepository repository = new MyRepository(); // MyService tự tạo MyRepository
public void doSomething() {
repository.fetchData();
}
}
Với IoC, *điều khiển* việc tạo và quản lý phụ thuộc bị *đảo ngược*. Thay vì đối tượng của bạn tạo các phụ thuộc, một container (trong trường hợp này là Spring Container) tạo các phụ thuộc và tiêm chúng vào đối tượng của bạn. Đây là mẫu **Tiêm Phụ Thuộc (DI)** trong hành động.
// Cách tiếp cận Spring: Phụ thuộc được tiêm
public class MyService {
private MyRepository repository; // Phụ thuộc được khai báo nhưng không tạo ở đây
// Tiêm qua Constructor (phổ biến và được khuyến nghị)
public MyService(MyRepository repository) {
this.repository = repository; // Spring tiêm instance MyRepository
}
public void doSomething() {
repository.fetchData();
}
}
**Spring đạt được điều này như thế nào?**
Spring sử dụng siêu dữ liệu cấu hình (có thể là XML, chú thích Java hoặc mã Java) để hiểu các đối tượng nào (gọi là “beans”) cần được tạo và các phụ thuộc của chúng là gì. Khi Spring Container khởi động, nó đọc cấu hình này, tạo các bean và kết nối chúng bằng cách tiêm các phụ thuộc khi cần thiết.
**Tại sao lựa chọn kiến trúc này quan trọng?**
* **Tách rời:** Các đối tượng ít phụ thuộc vào chi tiết triển khai cụ thể của các phụ thuộc của chúng. Bạn có thể dễ dàng thay thế `MyRepository` bằng `AnotherRepository` mà không cần thay đổi `MyService`.
* **Khả năng kiểm thử:** Dễ dàng kiểm thử `MyService` bằng cách cung cấp (mock) các phụ thuộc của nó trong quá trình kiểm thử.
* **Khả năng quản lý:** Container quản lý vòng đời của các đối tượng một cách tập trung, xử lý việc tạo, cấu hình và hủy.
Nguyên tắc cốt lõi này của IoC/DI là mô liên kết giữ toàn bộ kiến trúc Spring. Mọi mô-đun đều xây dựng dựa trên nền tảng này.
Thêm Các Mối Quan Tâm Chéo: Lập Trình Hướng Khía Cạnh (AOP)
Một trụ cột kiến trúc quan trọng khác trong Spring là hỗ trợ **Lập Trình Hướng Khía Cạnh (AOP)**. Trong khi IoC/DI giúp tách rời các đối tượng khỏi các phụ thuộc của chúng, AOP giúp tách rời **các mối quan tâm chéo** khỏi logic nghiệp vụ chính của bạn.
Các mối quan tâm chéo là gì? Đây là các chức năng thường nằm rải rác ở nhiều phần của ứng dụng nhưng không phải là phần logic nghiệp vụ chính của các phần đó. Các ví dụ phổ biến bao gồm:
* Ghi log
* Quản lý giao dịch
* Kiểm tra bảo mật
* Bộ nhớ đệm
Hãy tưởng tượng bạn cần ghi log mỗi khi một phương thức trong lớp dịch vụ của bạn được gọi. Không có AOP, bạn sẽ rải mã ghi log khắp hàng chục hoặc hàng trăm phương thức. Điều này lặp đi lặp lại, dễ gây lỗi và làm cho mã khó bảo trì hơn.
AOP cho phép bạn định nghĩa các mối quan tâm này dưới dạng **Aspect**. Một aspect đóng gói logic cho một mối quan tâm chéo và chỉ định *nơi* và *khi nào* logic này nên được áp dụng.
**Các Khái Niệm AOP Chính trong Spring:**
* **Aspect:** Một mô-đun đóng gói logic chéo. (ví dụ: LoggingAspect).
* **Join Point:** Một điểm cụ thể trong quá trình thực thi chương trình, như thực thi phương thức, thiết lập trường, v.v. (ví dụ: thực thi bất kỳ phương thức nào trong lớp dịch vụ của bạn).
* **Advice:** Mã được thực thi tại một join point cụ thể. (ví dụ: mã ghi log). Các loại advice khác nhau bao gồm `Before` (thực thi trước join point), `After` (thực thi sau), `Around` (bao bọc join point), `AfterReturning` (thực thi sau khi trả về thành công), `AfterThrowing` (thực thi sau khi ném ngoại lệ).
* **Pointcut:** Biểu thức khớp với một hoặc nhiều join point. (ví dụ: tất cả các phương thức public trong gói `com.example.service`).
* **Weaving:** Quá trình áp dụng các aspect vào các đối tượng đích tại các join point cụ thể. Spring thường sử dụng proxy động để dệt AOP tại thời gian chạy.
**Spring đạt được AOP như thế nào?**
Spring AOP chủ yếu hoạt động bằng cách tạo các đối tượng proxy. Khi bạn có một bean khớp với pointcut được định nghĩa bởi một aspect, Spring tạo một proxy cho bean đó. Mọi lời gọi đến các phương thức của đối tượng đích đều đi qua proxy này, nó chặn lời gọi, áp dụng advice liên quan và sau đó ủy quyền cho đối tượng gốc.
// Ví dụ khái niệm sử dụng chú thích
@Aspect
@Component
public class LoggingAspect {
// Định nghĩa pointcut cho tất cả các phương thức trong gói service
@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}
// Áp dụng Before advice cho pointcut serviceMethods
@Before("serviceMethods()")
public void logMethodEntry(JoinPoint joinPoint) {
System.out.println("Entering method: " + joinPoint.getSignature().getName());
}
// Áp dụng AfterReturning advice
@AfterReturning(pointcut = "serviceMethods()", returning = "result")
public void logMethodExit(JoinPoint joinPoint, Object result) {
System.out.println("Exiting method: " + joinPoint.getSignature().getName() + " with result: " + result);
}
}
Tính năng kiến trúc này giữ cho logic nghiệp vụ chính của bạn sạch sẽ và tập trung, làm cho ứng dụng dễ hiểu và bảo trì hơn.
Trái Tim: Spring Container
**Spring Container** là động cơ cốt lõi của Spring Framework. Nó chịu trách nhiệm:
1. **Đọc Cấu Hình:** Hiểu các bean nào cần được quản lý và cách chúng được cấu hình (phụ thuộc, vòng đời, phạm vi).
2. **Tạo Bean:** Khởi tạo các lớp được cấu hình.
3. **Cấu Hình Bean:** Tiêm phụ thuộc (DI), áp dụng các aspect AOP và các cấu hình khác.
4. **Quản Lý Vòng Đời Bean:** Xử lý khởi tạo (ví dụ: gọi các phương thức `@PostConstruct`) và hủy (ví dụ: gọi các phương thức `@PreDestroy`).
Có hai loại Spring Container chính:
* **`BeanFactory`:** Container đơn giản nhất. Nó cung cấp khả năng DI cơ bản. Các bean thường được tạo lười (chỉ khi được yêu cầu).
* **`ApplicationContext`:** Đây là một giao diện con của `BeanFactory` và là container bạn hầu như luôn sử dụng trong các ứng dụng doanh nghiệp. Nó mở rộng `BeanFactory` với nhiều tính năng phù hợp với ứng dụng doanh nghiệp, như:
* Tích hợp dễ dàng hơn với các tính năng AOP của Spring.
* Xử lý nguồn thông báo (cho quốc tế hóa – i18n).
* Lan truyền sự kiện.
* Tải tài nguyên (ví dụ: lấy tệp).
* Tích hợp ứng dụng web dễ dàng hơn.
* Các bean thường được tạo sớm (khi khởi động), giúp phát hiện sớm các vấn đề cấu hình.
`ApplicationContext` đọc siêu dữ liệu cấu hình của bạn (tệp XML, lớp cấu hình Java hoặc chú thích quét thành phần) và điền vào chính nó các instance đã được cấu hình đầy đủ của các đối tượng ứng dụng của bạn – các “bean”.
// Ví dụ tạo ApplicationContext
public class MainApp {
public static void main(String[] args) {
// Sử dụng AnnotationConfigApplicationContext cho Cấu Hình Java/Chú Thích
ApplicationContext context =
new AnnotationConfigApplicationContext(AppConfig.class); // Truyền lớp cấu hình của bạn
// Lấy một bean từ container
MyService service = context.getBean(MyService.class);
service.doSomething();
// Đóng context (quan trọng trong ứng dụng độc lập)
((AnnotationConfigApplicationContext)context).close();
}
}
Container hoạt động như một nhà máy và người điều phối tinh vi, tạo và quản lý các thành phần của ứng dụng dựa trên hướng dẫn của bạn.
Hệ Sinh Thái Spring: Kiến Trúc Mô-đun
Một trong những điểm mạnh lớn nhất của Spring là **kiến trúc mô-đun**. Framework này không phải là một thư viện đơn lẻ, nguyên khối. Thay vào đó, nó bao gồm nhiều mô-đun, mỗi mô-đun cung cấp chức năng cụ thể. Điều này cho phép các nhà phát triển chỉ sử dụng các phần họ cần, giữ cho các phụ thuộc ứng dụng gọn nhẹ.
Dưới đây là một số mô-đun chính xây dựng dựa trên Core và AOP containers:
Tên Mô-đun | Mục Đích Chính | Mô Tả Ngắn |
---|---|---|
Spring Core Container | IoC, DI, Beans | Cung cấp các phần cơ bản của framework, bao gồm BeanFactory và ApplicationContext. |
Spring AOP | Lập Trình Hướng Khía Cạnh | Cho phép triển khai các mối quan tâm chéo như ghi log, giao dịch và bảo mật. |
Spring Data Access / Integration (JDBC, ORM, OXM, JMS, Transactions) | Tương Tác Cơ Sở Dữ Liệu, Tin Nhắn, Giao Dịch | Cung cấp các lớp trừu tượng và hỗ trợ làm việc với cơ sở dữ liệu (JDBC, các framework ORM như JPA/Hibernate), tích hợp qua JMS và quản lý giao dịch khai báo. |
Spring Web (MVC, WebFlux) | Phát Triển Ứng Dụng Web | Cung cấp hỗ trợ xây dựng ứng dụng web, bao gồm kiến trúc Model-View-Controller (Spring MVC) và phát triển web phản ứng (Spring WebFlux). |
Spring Security | Xác Thực và Ủy Quyền | Một framework mạnh mẽ để thêm các tính năng bảo mật vào ứng dụng của bạn. |
Spring Boot | Đơn Giản Hóa Phát Triển Spring | Xây dựng trên Spring framework cốt lõi để cung cấp quy ước, tự động cấu hình và máy chủ nhúng giúp giảm đáng kể thời gian thiết lập. (Chúng tôi đã đề cập trong bài Lộ trình Spring Boot!) |
Spring Batch | Xử Lý Hàng Loạt | Framework để xây dựng các ứng dụng hàng loạt mạnh mẽ. |
Spring Cloud | Hệ Thống Phân Tán / Microservices | Một bộ sưu tập các dự án cung cấp công cụ xây dựng các mẫu phổ biến trong hệ thống phân tán (ví dụ: phát hiện dịch vụ, bộ ngắt mạch, quản lý cấu hình). |
Tính mô-đun này có nghĩa là bạn có thể bắt đầu với Core, thêm Web nếu bạn đang xây dựng ứng dụng web, Data nếu bạn cần truy cập cơ sở dữ liệu, Security nếu bạn cần xác thực, v.v. Bạn không phải mang theo gánh nặng của các mô-đun bạn không sử dụng.
Xử Lý Yêu Cầu Web: Kiến Trúc Spring MVC
Đối với nhiều nhà phát triển, lần đầu tiên họ tương tác với Spring thường là thông qua việc xây dựng ứng dụng web bằng **Spring MVC**. Mô-đun này minh họa cách các nguyên tắc kiến trúc cốt lõi được áp dụng trong một ngữ cảnh cụ thể.
Kiến trúc Spring MVC tuân theo mẫu thiết kế **Model-View-Controller** và tập trung xung quanh một servlet đặc biệt gọi là `DispatcherServlet`.
Dưới đây là luồng đơn giản của cách một yêu cầu web thường được xử lý trong ứng dụng Spring MVC:
- Nhận Yêu Cầu: Một yêu cầu (ví dụ: người dùng nhấp vào liên kết) đến máy chủ web.
- DispatcherServlet: Máy chủ web chuyển tiếp yêu cầu đến `DispatcherServlet`. `DispatcherServlet` hoạt động như bộ điều khiển phía trước – tất cả yêu cầu cho ứng dụng của bạn đều đi qua nó.
- Handler Mapping: `DispatcherServlet` tham khảo một hoặc nhiều bean `HandlerMapping` để xác định phương thức điều khiển (handler) nào nên xử lý yêu cầu dựa trên URL yêu cầu, phương thức HTTP, tham số yêu cầu, v.v.
- Thực Thi Controller: Khi phương thức điều khiển được xác định, `DispatcherServlet` chuyển yêu cầu đến nó. Phương thức điều khiển chứa logic nghiệp vụ của bạn. Nó xử lý yêu cầu, tương tác với các dịch vụ/kho lưu trữ (được tiêm qua DI!) và chuẩn bị một Model (dữ liệu sẽ hiển thị).
- Giải Quyết View: Controller thường trả về tên view logic (ví dụ: “userList”) và Model. `DispatcherServlet` sau đó tham khảo bean `ViewResolver` để ánh xạ tên view logic đến triển khai View cụ thể (ví dụ: tệp JSP, mẫu Thymeleaf, phản hồi REST).
- Kết Xuất View: View được chọn sau đó được kết xuất bằng cách truyền dữ liệu Model vào nó.
- Gửi Phản Hồi: View đã kết xuất (ví dụ: HTML) được gửi lại qua `DispatcherServlet` đến trình duyệt của khách hàng.
// Ví dụ về một Spring MVC Controller đơn giản
@Controller // Báo cho Spring biết lớp này là một bean Controller
public class UserController {
private final UserService userService; // Khai báo phụ thuộc
// Tiêm qua Constructor bởi Spring
public UserController(UserService userService) {
this.userService = userService;
}
@GetMapping("/users") // Ánh xạ yêu cầu GET đến /users vào phương thức này
public String listUsers(Model model) {
List<User> users = userService.findAllUsers(); // Sử dụng dịch vụ được tiêm
model.addAttribute("users", users); // Thêm dữ liệu vào Model
return "userList"; // Trả về tên view logic
}
// @PostMapping, @PutMapping, v.v. xử lý các phương thức HTTP khác
}
Kiến trúc này tách biệt rõ ràng các mối quan tâm: `DispatcherServlet` xử lý định tuyến yêu cầu, controllers xử lý logic nghiệp vụ và chuẩn bị dữ liệu, và views xử lý trình bày. IoC/DI được sử dụng xuyên suốt để tiêm các phụ thuộc như dịch vụ vào controllers.
Tổng Hợp Tất Cả Lại
Vậy, các phần này kết hợp với nhau như thế nào?
**Spring Container** là người quản lý trung tâm. Nó đọc **Cấu Hình** của bạn (chú thích, Cấu Hình Java, XML) để biết về các **Bean** của bạn. Dựa trên cấu hình này, nó tạo và kết nối các bean của bạn với nhau bằng cách sử dụng **Tiêm Phụ Thuộc**.
Khi bạn sử dụng các mô-đun như Spring MVC, Spring Data hoặc Spring Security, chúng cung cấp các loại bean cụ thể (như `DispatcherServlet`, `Controller`, `Repository`, `UserDetailsService`). Bạn khai báo các bean này (thường chỉ bằng cách thêm các chú thích như `@Controller`, `@Repository`, `@Service` và để quét thành phần tìm thấy chúng), và Spring Container quản lý chúng.
**AOP** tích hợp với container để tạo proxy xung quanh các bean của bạn khi cần, cho phép các mối quan tâm chéo như giao dịch hoặc kiểm tra bảo mật được áp dụng liền mạch mà không làm lộn xộn logic nghiệp vụ chính của bạn.
**Spring Boot** nằm trên cùng, cung cấp các mặc định hợp lý và tự động cấu hình dựa trên các phụ thuộc của bạn, giúp thiết lập ứng dụng Spring nhanh hơn và dễ dàng hơn bằng cách sử dụng các nguyên tắc và mô-đun cốt lõi này. Nó đơn giản hóa khía cạnh *cấu hình*, nhưng IoC Container, DI, AOP và cấu trúc mô-đun cơ bản vẫn hoạt động mạnh mẽ.
Đơn Giản Hóa Phát Triển: Spring Boot
Mặc dù hiểu kiến trúc cốt lõi của Spring là rất quan trọng, Spring Boot làm cho việc *sử dụng* nó dễ tiếp cận hơn nhiều, đặc biệt là cho người mới bắt đầu. Như chúng tôi đã thảo luận trong Lộ trình Spring Boot, Boot tận dụng tính mô-đun và các nguyên tắc cốt lõi của Spring Framework nhưng thêm vào:
* **Tự Động Cấu Hình:** Tự động cấu hình ứng dụng của bạn dựa trên các jar trong classpath. Nếu bạn có `spring-webmvc.jar`, Boot giả định bạn đang xây dựng ứng dụng web và thiết lập `DispatcherServlet` cho bạn với các mặc định hợp lý.
* **Starter Dependencies:** Cung cấp các phụ thuộc được tuyển chọn mang theo tất cả các jar cần thiết cho một tính năng cụ thể (ví dụ: `spring-boot-starter-web` mang theo Spring MVC, Jackson cho JSON, validation, Tomcat nhúng, v.v.).
* **Mặc Định Có Ý Kiến:** Cung cấp quy ước thay vì cấu hình, giảm lượng mã mẫu bạn cần viết.
* **Máy Chủ Nhúng:** Cho phép bạn đóng gói ứng dụng của mình dưới dạng một JAR thực thi duy nhất với máy chủ Tomcat, Jetty hoặc Undertow nhúng.
Spring Boot không thay thế Spring Framework cốt lõi; nó cung cấp cách nhanh hơn và dễ dàng hơn để xây dựng ứng dụng dựa trên Spring bằng cách đơn giản hóa cấu hình và thiết lập dựa trên kiến trúc cơ bản mà chúng ta đã thảo luận.
Kết Luận
Hiểu kiến trúc cốt lõi của Spring Framework – cách IoC/DI quản lý các phụ thuộc, cách AOP xử lý các mối quan tâm chéo, cách Spring Container điều phối mọi thứ và cách các mô-đun cung cấp các chức năng cụ thể – là chìa khóa để trở thành một nhà phát triển Spring thành thạo.
Nó giống như học ngữ pháp và cấu trúc câu của một ngôn ngữ trước khi viết tiểu thuyết. Bạn có thể bắt đầu với những câu đơn giản (có thể sử dụng tự động cấu hình của Spring Boot), nhưng biết ngữ pháp cơ bản (kiến trúc cốt lõi) cho phép bạn viết các ứng dụng phức tạp và tinh tế hơn khi cần.
Chúng tôi đã đề cập rất nhiều: IoC/DI, AOP, Spring Container (`ApplicationContext`), hệ sinh thái mô-đun và cái nhìn thoáng qua về xử lý yêu cầu của Spring MVC. Đây là nền tảng.
Trong các bài viết tiếp theo trong loạt “Lộ trình Java Spring”, chúng tôi sẽ đi sâu hơn vào các khía cạnh cụ thể, như cấu hình Spring bean chi tiết hơn, khám phá các mô-đun khác nhau và xây dựng các ứng dụng hoàn chỉnh.
Hãy tiếp tục thử nghiệm, xây dựng các dự án nhỏ và tham khảo lại các khái niệm cốt lõi này. Bạn càng viết mã với Spring, kiến trúc của nó sẽ càng trở nên rõ ràng. Chúc bạn viết mã vui vẻ!”.