Chào mừng các bạn quay trở lại với series “Java Spring Roadmap“! Trong những bài viết trước, chúng ta đã cùng nhau khám phá lý do tại sao Spring lại phổ biến đến vậy (Tại sao chọn Spring?), làm quen với kiến trúc cốt lõi (Cách Spring Hoạt Động), hiểu sâu về trái tim của Spring là Dependency Injection (Hiểu Về Dependency Injection) và IoC Container (Spring IoC trong Thực Tế), cũng như bắt đầu hành trình với Spring MVC (Spring MVC Giải Mã).
Chúng ta đã thấy Spring MVC giúp việc xây dựng ứng dụng web trở nên dễ dàng và có cấu trúc hơn rất nhiều. Tuy nhiên, để thực sự hiểu “phép màu” này hoạt động như thế nào ở lớp dưới, chúng ta cần quay về một khái niệm nền tảng của Java web: Servlets.
Trong bài viết này, chúng ta sẽ cùng nhau “giải mã” Servlets là gì, vai trò của chúng trong hệ sinh thái Java web truyền thống, và quan trọng nhất, làm thế nào chúng trở thành “nền móng vô hình” giúp Spring MVC hoạt động mạnh mẽ và linh hoạt như vậy. Hãy cùng bắt đầu nhé!
Mục lục
Servlets Là Gì? Nền Tảng Của Thế Giới Web Java
Trước khi có các framework “thần kỳ” như Spring MVC, việc xây dựng các ứng dụng web động bằng Java chủ yếu dựa vào Servlet API. Vậy Servlets thực chất là gì?
Servlet là một lớp (class) Java dùng để xử lý các yêu cầu (requests) từ trình duyệt (hoặc các client HTTP khác) và tạo ra phản hồi (responses) động. Chúng chạy trên một máy chủ web hỗ trợ Servlet (gọi là Servlet Container hoặc Web Container), phổ biến nhất là Apache Tomcat, Jetty, hay WildFly.
Hãy hình dung thế này: khi bạn gõ một địa chỉ web vào trình duyệt và nhấn Enter, trình duyệt sẽ gửi một yêu cầu HTTP đến máy chủ. Nếu ứng dụng web đó được xây dựng bằng Java Servlets, yêu cầu này sẽ được máy chủ web tiếp nhận và chuyển đến Servlet Container. Container này có nhiệm vụ tìm đúng Servlet được cấu hình để xử lý loại yêu cầu đó, khởi tạo nó (nếu cần), và gọi một phương thức xử lý yêu cầu trên Servlet đó. Servlet sẽ xử lý logic nghiệp vụ, tương tác với cơ sở dữ liệu (nếu có), và tạo ra phản hồi HTTP (thường là trang HTML) gửi trả lại cho trình duyệt.
Vòng Đời Của Một Servlet
Mỗi Servlet trong container đều trải qua một vòng đời được quản lý bởi chính container đó. Vòng đời này cơ bản bao gồm ba giai đoạn chính, tương ứng với ba phương thức trong interface javax.servlet.Servlet
:
- Khởi tạo (Initialization): `init()`
- Phương thức
init(ServletConfig config)
chỉ được gọi một lần duy nhất khi Servlet được khởi tạo lần đầu tiên bởi Servlet Container. - Đây là nơi lý tưởng để thực hiện các công việc setup ban đầu, ví dụ như đọc cấu hình, thiết lập kết nối cơ sở dữ liệu (trong các ứng dụng cũ), v.v.
- Container sẽ không xử lý yêu cầu nào cho Servlet này cho đến khi phương thức
init()
hoàn thành.
- Phương thức
- Xử lý yêu cầu (Request Handling): `service()`
- Phương thức
service(ServletRequest req, ServletResponse res)
được gọi cho mỗi yêu cầu gửi đến Servlet này. - Đây là “trái tim” của Servlet, nơi chứa logic xử lý chính. Dựa vào loại yêu cầu (GET, POST, PUT, DELETE…), phương thức
service()
sẽ ủy quyền việc xử lý cho các phương thức cụ thể hơn (nhưdoGet()
,doPost()
…) trong lớp conHttpServlet
. - Container cung cấp đối tượng
ServletRequest
chứa thông tin về yêu cầu (tham số, header, body…) và đối tượngServletResponse
để ghi dữ liệu phản hồi (HTML, JSON…). Trong web HTTP, chúng ta thường làm việc với các lớp con của chúng làHttpServletRequest
vàHttpServletResponse
.
- Phương thức
- Kết thúc (Destruction): `destroy()`
- Phương thức
destroy()
chỉ được gọi một lần duy nhất khi Servlet Container quyết định gỡ bỏ Servlet khỏi dịch vụ (ví dụ: khi tắt máy chủ, hoặc khi deploy lại ứng dụng). - Đây là nơi bạn thực hiện các công việc dọn dẹp tài nguyên, đóng kết nối, lưu trạng thái cuối cùng…
- Phương thức
Thông thường, khi làm việc với web HTTP, chúng ta sẽ kế thừa từ lớp tiện ích javax.servlet.http.HttpServlet
, lớp này đã cung cấp sẵn phương thức service()
mặc định để phân phối yêu cầu đến các phương thức doGet()
, doPost()
, doPut()
, doDelete()
… tương ứng với các động từ HTTP.
Một ví dụ đơn giản về cấu trúc một Servlet HTTP:
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class MySimpleServlet extends HttpServlet {
@Override
public void init() throws ServletException {
// Code khởi tạo, chạy 1 lần khi servlet được deploy
System.out.println("MySimpleServlet đang khởi tạo...");
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Code xử lý yêu cầu GET
System.out.println("Nhận yêu cầu GET tại MySimpleServlet");
// Đặt kiểu nội dung phản hồi
response.setContentType("text/html");
response.setCharacterEncoding("UTF-8");
// Ghi nội dung phản hồi
response.getWriter().println("<!DOCTYPE html>");
response.getWriter().println("<html>");
response.getWriter().println("<head><title>Xin chào Servlet</title></head>");
response.getWriter().println("<body>");
response.getWriter().println("<h1>Xin chào từ MySimpleServlet!</h1>");
response.getWriter().println("<p>Đây là nội dung được tạo ra từ Servlet Java.</p>");
response.getWriter().println("</body>");
response.getWriter().println("</html>");
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// Code xử lý yêu cầu POST
System.out.println("Nhận yêu cầu POST tại MySimpleServlet");
// ... xử lý dữ liệu từ request.getParameter() ...
}
@Override
public void destroy() {
// Code dọn dẹp, chạy 1 lần khi servlet bị gỡ bỏ
System.out.println("MySimpleServlet đang bị gỡ bỏ...");
}
}
Để Servlet này hoạt động, bạn cần đóng gói nó cùng các file ứng dụng web khác (như HTML, CSS, JS) vào một file WAR (Web Archive) và triển khai trên một Servlet Container. Cấu hình ánh xạ URL đến Servlet sẽ được định nghĩa trong file web.xml
(theo phong cách cũ) hoặc thông qua các chú thích (annotation) hay cấu hình Java (phong cách hiện đại).
Làm việc trực tiếp với Servlets cung cấp sự kiểm soát chi tiết, nhưng cũng đòi hỏi phải xử lý rất nhiều boilerplate code: phân tích yêu cầu, quản lý các loại phản hồi khác nhau, xử lý lỗi, quản lý session… Điều này trở nên cồng kềnh khi ứng dụng phức tạp hơn. Đây chính là lúc các framework như Spring MVC ra đời để đơn giản hóa công việc.
Tại Sao Spring MVC Lại Cần Đến Servlets?
Bây giờ là phần quan trọng: Servlets liên kết với Spring MVC như thế nào? Mặc dù Spring MVC cung cấp một lớp trừu tượng cao hơn, nhưng ở tầng nền, nó vẫn hoạt động dựa trên nền tảng Servlet API.
Lý do rất đơn giản: Các Servlet Container như Tomcat chỉ hiểu và tương tác với các lớp Java tuân thủ Servlet API. Để một framework web Java có thể nhận và xử lý các yêu cầu HTTP đến từ trình duyệt thông qua container, nó buộc phải “giao tiếp” được với container, và cách chính là thông qua Servlet API.
Spring MVC giải quyết vấn đề này bằng cách sử dụng một Servlet đặc biệt làm điểm truy cập trung tâm cho tất cả các yêu cầu web: đó là DispatcherServlet
.
DispatcherServlet: Trái Tim Servlet Của Spring MVC
DispatcherServlet
là một lớp con của HttpServlet
được cung cấp bởi framework Spring. Nó đóng vai trò là Bộ điều phối phía trước (Front Controller) trong kiến trúc MVC của Spring. Mọi yêu cầu HTTP gửi đến ứng dụng Spring MVC đều được Servlet Container chuyển hướng đến DispatcherServlet
này.
Hãy xem cách nó tận dụng vòng đời Servlet:
- Khởi tạo (`init()`): Khi Servlet Container khởi tạo
DispatcherServlet
, phương thứcinit()
của nó được gọi. Trong quá trình này,DispatcherServlet
sẽ tạo và cấu hình ApplicationContext riêng cho tầng web (còn gọi là WebApplicationContext). Đây là nơi Spring container sẽ quét và quản lý các Bean liên quan đến tầng web, như Controllers, ViewResolvers, HandlerMappings, v.v. Việc này rất quan trọng vì nó kết nối thế giới Servlet truyền thống với sức mạnh của Spring IoC (Spring IoC trong Thực Tế) và Dependency Injection (Hiểu Về Dependency Injection). - Xử lý yêu cầu (`service()`): Khi có một yêu cầu HTTP đến, Servlet Container gọi phương thức
service()
củaDispatcherServlet
. Thay vì tự mình xử lý toàn bộ logic như một Servlet thông thường,DispatcherServlet
sẽ đảm nhận nhiệm vụ điều phối:- Nó xác định Handler (thường là một Controller method) nào phù hợp để xử lý yêu cầu dựa trên các cấu hình (ví dụ: `@RequestMapping` annotations – xem thêm về Spring Annotations). Nó sử dụng các
HandlerMapping
Bean được cấu hình trong ApplicationContext cho mục đích này. - Nó gọi Handler đó, truyền vào các tham số cần thiết (được lấy từ
HttpServletRequest
) và nhận về kết quả (thường là một đối tượngModelAndView
hoặc dữ liệu trực tiếp). - Nếu kết quả là
ModelAndView
, nó sẽ sử dụng cácViewResolver
Bean để xác định view nào sẽ được hiển thị (ví dụ: một file JSP, Thymeleaf, FreeMarker…). - Nó render view đó, sử dụng dữ liệu từ Model.
- Cuối cùng, nó ghi kết quả (thường là HTML) vào
HttpServletResponse
và hoàn thành yêu cầu.
- Nó xác định Handler (thường là một Controller method) nào phù hợp để xử lý yêu cầu dựa trên các cấu hình (ví dụ: `@RequestMapping` annotations – xem thêm về Spring Annotations). Nó sử dụng các
- Kết thúc (`destroy()`): Khi ứng dụng dừng lại,
DispatcherServlet
sẽ đóng WebApplicationContext và giải phóng các tài nguyên.
Sơ đồ luồng xử lý yêu cầu trong Spring MVC, với vai trò trung tâm của DispatcherServlet:
Client Request -> Servlet Container -> DispatcherServlet -> HandlerMapping (Tìm Controller) -> Handler (Controller) -> HandlerAdapter (Gọi Controller Method) -> Service Layer/DAO -> (Kết quả: Model & View) -> ViewResolver (Tìm View) -> View (Render View) -> (Phản hồi HTML) -> DispatcherServlet -> Servlet Container -> Client Response
Như bạn thấy, DispatcherServlet
không tự làm mọi thứ. Nó chỉ là người điều phối thông minh, tận dụng sức mạnh của Servlet API để nhận yêu cầu và sau đó ủy thác công việc cho các thành phần khác của Spring framework (được quản lý bởi IoC Container) để thực hiện logic cụ thể. Điều này tuân thủ chặt chẽ nguyên tắc Tách biệt các mối quan tâm (Separation of Concerns) và giúp mã nguồn trở nên modular, dễ quản lý và kiểm thử hơn.
Từ Raw Servlets Đến Sức Mạnh Của Spring MVC
Việc hiểu rằng Spring MVC được xây dựng trên nền tảng Servlets giúp chúng ta đánh giá cao những gì framework này mang lại. Hãy so sánh kinh nghiệm phát triển web với Servlets “trần” và với Spring MVC:
Tính năng | Phát Triển Với Raw Servlets | Phát Triển Với Spring MVC |
---|---|---|
Xử lý Yêu Cầu HTTP | Phải tự phân tích HttpServletRequest , đọc tham số thủ công, xử lý khác biệt giữa GET/POST trong cùng service() hoặc các do... methods. |
Sử dụng Annotations (`@RequestMapping`, `@GetMapping`, `@PostMapping`, v.v.) để ánh xạ URL đến Controller method cụ thể. Spring tự động bind tham số yêu cầu vào các tham số phương thức. |
Quản lý Phản Hồi/View | Phải tự ghi nội dung HTML/JSON vào HttpServletResponse.getWriter() hoặc dùng Request Dispatcher để forward đến JSP. Phức tạp khi cần truyền dữ liệu từ logic đến view. |
Sử dụng Model để truyền dữ liệu. Sử dụng tên view (string) và ViewResolver để chọn công nghệ view (JSP, Thymeleaf…). Spring xử lý việc rendering view. Dễ dàng trả về dữ liệu (JSON/XML) trực tiếp với `@ResponseBody` hoặc `@RestController` (Spring MVC Giải Mã). |
Cấu Hình | Cấu hình ánh xạ Servlet trong web.xml . Quản lý vòng đời của các tài nguyên dùng chung (như kết nối DB) phức tạp hơn, thường dùng ServletContextListener . |
Cấu hình tập trung thông qua DispatcherServlet và Spring ApplicationContext. Dễ dàng cấu hình các Bean (Controller, Service, Repository) bằng Java Config hoặc Component Scan (Làm Chủ Cấu Hình). Đặc biệt đơn giản với Spring Boot (Spring Boot Autoconfiguration). |
Quản lý Dependencies | Phải tự quản lý các đối tượng phụ thuộc (ví dụ: tạo instance của service/DAO trong Servlet). Khó khăn khi unit test. | Tận dụng sức mạnh của IoC Container và Dependency Injection (Hiểu Về Dependency Injection) để quản lý các Bean. Dễ dàng inject các service, repository vào Controller. |
Kiểm Thử (Testability) | Khó unit test logic nghiệp vụ vì nó thường gắn chặt với HttpServletRequest /HttpServletResponse . Cần mock các đối tượng Servlet API. |
Controller method có thể được unit test dễ dàng hơn vì logic xử lý request/response đã được Spring MVC trừu tượng hóa. Spring cung cấp các công cụ hỗ trợ test (như Spring Test MVC). |
Tính Mở Rộng (Extensibility) | Muốn thêm tính năng mới (như logging, security, transaction) phải can thiệp trực tiếp vào logic của Servlet hoặc dùng Filter (cũng là một loại cấu hình Servlet API phức tạp). | Tích hợp dễ dàng với các module khác của Spring (Spring Security (Spring Security 101), Spring Data (Spring Data JPA), Spring AOP (Spring AOP)…). Sử dụng Interceptors để can thiệp vào luồng xử lý request một cách linh hoạt. |
Qua bảng so sánh trên, bạn có thể thấy Spring MVC không thay thế Servlets, mà là xây dựng một lớp trừu tượng và một hệ sinh thái phong phú trên nền tảng Servlet API. DispatcherServlet
là cầu nối quan trọng giữa Servlet Container và framework Spring, cho phép Spring MVC tận dụng cơ chế xử lý yêu cầu chuẩn của Java web trong khi cung cấp cho nhà phát triển một mô hình lập trình hiệu quả, có cấu trúc và dễ bảo trì hơn nhiều.
Cấu Hình DispatcherServlet: Từ web.xml Đến Spring Boot
Việc cấu hình để Servlet Container biết “đường” chuyển yêu cầu đến DispatcherServlet
cũng đã thay đổi theo thời gian.
- Phong cách truyền thống (`web.xml`): Trong các ứng dụng Java web cũ, bạn sẽ khai báo
DispatcherServlet
và ánh xạ URL mẫu đến nó trong fileWEB-INF/web.xml
.
<!-- Ví dụ cấu hình web.xml -->
<web-app ...>
<servlet>
<servlet-name>dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring-mvc-servlet.xml</param-value> <!-- File cấu hình Spring -->
</init-param>
<load-on-startup>1</load-on-startup> <!-- Khởi tạo ngay khi deploy -->
</servlet>
<servlet-mapping>
<servlet-name>dispatcher</servlet-name>
<url-pattern>/</url-pattern> <!-- Ánh xạ tất cả các yêu cầu đến DispatcherServlet -->
</servlet-mapping>
</web-app>
- Phong cách hiện đại (Java Config): Với sự ra đời của Servlet 3.0+, chúng ta có thể cấu hình Servlet bằng code Java thay vì XML. Spring cung cấp interface
WebApplicationInitializer
. Lớp implement interface này sẽ được Servlet Container tìm thấy và gọi khi ứng dụng khởi động, cho phép bạn đăng kýDispatcherServlet
và cấu hình Spring ApplicationContext bằng code Java.
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletRegistration;
public class MyWebAppInitializer implements WebApplicationInitializer {
@Override
public void onStartup(ServletContext servletContext) {
// Tạo Spring application context
AnnotationConfigWebApplicationContext appContext = new AnnotationConfigWebApplicationContext();
appContext.register(AppConfig.class); // Lớp cấu hình Spring chính của bạn
// Tạo và đăng ký DispatcherServlet
DispatcherServlet dispatcherServlet = new DispatcherServlet(appContext);
ServletRegistration.Dynamic registration = servletContext.addServlet("dispatcher", dispatcherServlet);
// Ánh xạ DispatcherServlet tới URL pattern "/"
registration.addMapping("/");
// Đặt load-on-startup
registration.setLoadOnStartup(1);
}
}
- Phong cách Spring Boot: Đây là cách phổ biến nhất hiện nay. Spring Boot tự động cấu hình
DispatcherServlet
cho bạn khi phát hiện các Spring MVC dependencies. Mọi yêu cầu mặc định sẽ đượcDispatcherServlet
xử lý. Bạn không cần khai báo tường minh trongweb.xml
hay tạo lớpWebApplicationInitializer
nữa. Cơ chế này dựa trên Spring Boot Autoconfiguration (Spring Boot Autoconfiguration – Có Gì Diễn Ra Bên Dưới?) và việc sử dụng các máy chủ nhúng (embedded servers) như Tomcat (Máy Chủ Tomcat Nhúng Hoạt Động Như Thế Nào?). Điều này giúp việc setup một ứng dụng web Spring trở nên cực kỳ nhanh chóng với chỉ một vài dòng code và annotations.
Dù bạn cấu hình theo cách nào, nguyên lý hoạt động cơ bản vẫn không thay đổi: DispatcherServlet
vẫn là Servlet được đăng ký trong Container để nhận và điều phối các yêu cầu HTTP.
Kết Luận
Servlets là một khái niệm nền tảng không thể thiếu trong thế giới Java web. Chúng cung cấp API chuẩn để xử lý các yêu cầu HTTP trên máy chủ.
Spring MVC, một trong những framework phổ biến nhất để xây dựng ứng dụng web bằng Java, không “phớt lờ” Servlets. Ngược lại, nó tận dụng triệt để Servlet API bằng cách sử dụng DispatcherServlet
làm điểm vào duy nhất cho mọi yêu cầu. DispatcherServlet
đóng vai trò là bộ điều phối trung tâm, nhận yêu cầu từ Servlet Container và ủy quyền việc xử lý chi tiết cho các thành phần khác trong hệ sinh thái Spring (Controller, Service, Repository, ViewResolver…).
Việc hiểu mối quan hệ giữa Servlets và Spring MVC, đặc biệt là vai trò của DispatcherServlet
, sẽ giúp bạn có cái nhìn sâu sắc hơn về cách ứng dụng Spring MVC của mình hoạt động “dưới mui xe”. Nó giải thích tại sao bạn cần một Servlet Container để chạy ứng dụng web Spring truyền thống (trừ khi dùng Spring Boot với máy chủ nhúng), và làm rõ cách Spring có thể cung cấp một mô hình lập trình dựa trên Controller/Method thay vì phải tương tác trực tiếp với HttpServletRequest
và HttpServletResponse
.
Trên hành trình Java Spring Roadmap của chúng ta, việc nắm vững những kiến thức nền tảng này sẽ giúp bạn không chỉ sử dụng framework hiệu quả hơn mà còn gỡ lỗi (debug) dễ dàng hơn khi gặp vấn đề. Tiếp theo, chúng ta sẽ đi sâu hơn vào các khía cạnh khác của Spring MVC và hệ sinh thái Spring. Hẹn gặp lại trong các bài viết tiếp theo!