Servlets là Gì? Nền Tảng Vô Hình Giúp Spring MVC Vận Hành | Java Spring Roadmap

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é!

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:

  1. 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.
  2. 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 con HttpServlet.
    • 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ượng ServletResponse để 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à HttpServletRequestHttpServletResponse.
  3. 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…

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:

  1. Khởi tạo (`init()`): Khi Servlet Container khởi tạo DispatcherServlet, phương thức init() 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).
  2. 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ủa DispatcherServlet. 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ượng ModelAndView hoặc dữ liệu trực tiếp).
    • Nếu kết quả là ModelAndView, nó sẽ sử dụng các ViewResolver 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.
  3. 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 file WEB-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ẽ được DispatcherServlet xử lý. Bạn không cần khai báo tường minh trong web.xml hay tạo lớp WebApplicationInitializer 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 HttpServletRequestHttpServletResponse.

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!

Chỉ mục