Kiến Trúc Spring MVC: DispatcherServlet và Những Người Bạn

Chào mừng các bạn quay trở lại với series “Java Spring Roadmap”! Sau khi chúng ta đã cùng nhau khám phá những điều cơ bản nhất về Spring MVC và xây dựng ứng dụng web đầu tiên của mình (nếu bạn chưa đọc, hãy bắt đầu từ bài Spring MVC Giải Mã: Xây Dựng Ứng Dụng Web Đầu Tiên Của Bạn), có lẽ bạn sẽ thắc mắc: Điều gì thực sự xảy ra đằng sau hậu trường khi một yêu cầu (request) từ trình duyệt đến ứng dụng Spring MVC của chúng ta?

Trong bài viết này, chúng ta sẽ đi sâu vào kiến trúc của Spring MVC, đặc biệt là làm quen với “trái tim” của nó: DispatcherServlet và những thành phần “bạn bè” quan trọng giúp nó xử lý mọi yêu cầu một cách hiệu quả. Hiểu rõ kiến trúc này là chìa khóa để làm chủ Spring MVC, gỡ lỗi dễ dàng hơn và viết code web tốt hơn.

Spring MVC Kiến Trúc Tổng Quan: Một Mô Hình MVC Của Riêng Spring

Trước tiên, hãy nhắc lại về mô hình MVC (Model-View-Controller). Đây là một mẫu thiết kế phổ biến trong phát triển ứng dụng web, giúp tách biệt logic ứng dụng thành ba phần chính:

  • Model: Biểu diễn dữ liệu và logic nghiệp vụ.
  • View: Xử lý việc hiển thị dữ liệu (thường là giao diện người dùng).
  • Controller: Xử lý các yêu cầu từ người dùng, tương tác với Model để chuẩn bị dữ liệu và chọn View phù hợp để hiển thị.

Spring MVC là một framework dựa trên mô hình MVC này, được xây dựng trên nền tảng của Servlet API. Nó cung cấp một cách tiếp cận có cấu trúc và linh hoạt để xây dựng các ứng dụng web. Nếu bạn muốn ôn lại về Servlet, hãy xem bài viết Servlets là Gì? Nền Tảng Vô Hình Giúp Spring MVC Vận Hành.

Điểm khác biệt và cũng là sức mạnh của Spring MVC nằm ở kiến trúc hướng “Front Controller”, với DispatcherServlet đóng vai trò trung tâm.

Trái Tim Của Spring MVC: DispatcherServlet

Hãy hình dung DispatcherServlet như một “người gác cổng” hay một “điều phối viên” tài ba cho toàn bộ ứng dụng web của bạn. Thay vì mỗi loại yêu cầu (ví dụ: xử lý form, hiển thị trang chủ) lại cần một Servlet riêng biệt như cách truyền thống, Spring MVC sử dụng một Servlet duy nhất là DispatcherServlet để tiếp nhận *tất cả* các yêu cầu web gửi đến ứng dụng.

Vai trò của DispatcherServlet là gì?

  1. Tiếp nhận yêu cầu: Nó là điểm vào duy nhất cho mọi yêu cầu HTTP.
  2. Điều phối: Dựa vào cấu hình, nó sẽ “điều phối” yêu cầu đến các thành phần xử lý phù hợp khác trong framework.
  3. Quản lý quy trình: Nó quản lý toàn bộ vòng đời xử lý một yêu cầu, từ khi nó đến cho đến khi phản hồi được gửi đi.
  4. Tích hợp: Nó tích hợp chặt chẽ với Spring IoC Container (mà chúng ta đã tìm hiểu trong các bài trước như Hiểu Về Dependency InjectionSpring IoC trong Thực Tế) để tận dụng sức mạnh của Dependency Injection và quản lý các “Bean” (các đối tượng được quản lý bởi Spring Container).

Nhờ có DispatcherServlet, chúng ta không cần phải viết code lặp đi lặp lại để xử lý các tác vụ chung như ánh xạ URL, xử lý đa ngôn ngữ (localization), quản lý exception, v.v. DispatcherServlet sẽ ủy quyền các tác vụ này cho những “người bạn” chuyên biệt của nó.

Vòng Đời Xử Lý Yêu Cầu: DispatcherServlet và Những Người Bạn Hoạt Động Cùng Nhau

Đây là phần cốt lõi để hiểu kiến trúc Spring MVC. Khi một yêu cầu HTTP đến DispatcherServlet, một chuỗi các sự kiện và tương tác giữa DispatcherServlet và các thành phần “bạn bè” của nó sẽ diễn ra. Hãy cùng phân tích từng bước:

  1. Yêu cầu đến DispatcherServlet: Trình duyệt gửi một yêu cầu HTTP (ví dụ: GET /users/123) đến server web (như Tomcat nhúng mà Spring Boot hay dùng, xem bài Máy Chủ Tomcat Nhúng Hoạt Động Như Thế Nào Trong Spring Boot?). Server web chuyển yêu cầu này đến DispatcherServlet (vì nó được cấu hình để xử lý các URL pattern tương ứng, thường là / hoặc /*).
  2. Tìm kiếm Handler (Controller Method): DispatcherServlet cần biết “ai” sẽ xử lý yêu cầu này. Nó ủy quyền việc này cho một hoặc nhiều HandlerMapping đã được cấu hình. HandlerMapping sẽ dựa vào thông tin trong yêu cầu (URL, phương thức HTTP như GET/POST, headers, parameters, v.v.) để tìm ra một handler phù hợp. Trong hầu hết các ứng dụng Spring MVC hiện đại, handler này thường là một phương thức trong lớp Controller (được đánh dấu bằng các annotation như @Controller, @RestController@RequestMapping, @GetMapping, v.v. – bạn có thể tìm hiểu thêm về sức mạnh của annotation trong bài Tất Tần Tật Về Spring Annotations). HandlerMapping trả về handler đã tìm thấy (hoặc null nếu không tìm thấy).
  3. Tìm kiếm HandlerAdapter: Khi DispatcherServlet đã có handler, nó cần một cách để thực thi handler đó. Đây là lúc HandlerAdapter vào cuộc. DispatcherServlet sẽ duyệt qua danh sách các HandlerAdapter đã được cấu hình để tìm một adapter có khả năng thực thi kiểu handler đã tìm thấy ở bước 2. Ví dụ, nếu handler là một phương thức trong một lớp Controller được đánh dấu bằng annotation, DispatcherServlet sẽ tìm một HandlerAdapter phù hợp với kiểu handler này (ví dụ: RequestMappingHandlerAdapter).
  4. Thực thi Handler (Controller Method): DispatcherServlet yêu cầu HandlerAdapter đã tìm thấy thực thi handler. HandlerAdapter chịu trách nhiệm chuẩn bị dữ liệu cần thiết để gọi phương thức handler (ví dụ: lấy giá trị từ request parameters, path variables, request body và truyền vào các tham số của phương thức Controller – đây là một phần của Data Binding) và gọi phương thức Controller đó.
  5. Handler trả về ModelAndView: Phương thức Controller sau khi xử lý yêu cầu sẽ trả về một kết quả. Kết quả này thường là một đối tượng ModelAndView (hoặc chỉ là tên View String, hoặc dữ liệu trực tiếp nếu dùng @ResponseBody). ModelAndView chứa tên của View cần hiển thị và Model (dữ liệu cần gửi đến View).
  6. Phân giải View (View Resolution): Nếu kết quả từ handler là một ModelAndView (hoặc tên View), DispatcherServlet cần biến tên View logic (ví dụ: “home“, “userDetails“) thành tài nguyên View vật lý thực tế (ví dụ: file /WEB-INF/views/home.jsp, file templates/userDetails.html). Nó ủy quyền việc này cho một hoặc nhiều ViewResolver đã được cấu hình. ViewResolver sẽ cố gắng tìm View vật lý tương ứng với tên View logic. Ví dụ: InternalResourceViewResolver có thể thêm prefix và suffix vào tên View để tạo đường dẫn đến file JSP. Nếu bạn dùng Thymeleaf, sẽ có ThymeleafViewResolver.
  7. Render View: Sau khi ViewResolver trả về đối tượng View (biểu diễn tài nguyên View vật lý), DispatcherServlet sẽ yêu cầu đối tượng View này tự render. Quá trình render bao gồm việc sử dụng Model (dữ liệu từ Controller) để tạo ra nội dung phản hồi cuối cùng (thường là HTML) và ghi vào đối tượng HttpServletResponse. Nếu controller trả về dữ liệu trực tiếp (ví dụ: JSON với @ResponseBody), bước View Resolution và Render này có thể bị bỏ qua, thay vào đó, một HttpMessageConverter sẽ được sử dụng để chuyển đổi đối tượng Java thành định dạng phù hợp và ghi vào response.
  8. Phản hồi được gửi đi: Nội dung phản hồi cuối cùng được gửi trở lại trình duyệt của client.

Quá trình này cho thấy sự phân tách rõ ràng các nhiệm vụ: DispatcherServlet điều phối, HandlerMapping tìm handler, HandlerAdapter thực thi handler, Controller chứa logic nghiệp vụ, ViewResolver tìm View, và View render kết quả.

Những Người Bạn Thân Của DispatcherServlet: Chi Tiết Hơn

Để làm rõ hơn vai trò của từng thành phần, hãy đi sâu vào một số “người bạn” chính của DispatcherServlet:

HandlerMapping

HandlerMapping là giao diện chịu trách nhiệm ánh xạ một yêu cầu web đến một handler. Spring cung cấp nhiều triển khai của giao diện này:

  • BeanNameUrlHandlerMapping: Ánh xạ URL đến bean dựa trên tên bean (ít dùng trong code hiện đại).
  • SimpleUrlHandlerMapping: Cho phép cấu hình ánh xạ URL-to-handler một cách tường minh.
  • RequestMappingHandlerMapping: Đây là triển khai phổ biến nhất trong các ứng dụng dựa trên annotation. Nó kiểm tra các lớp và phương thức được đánh dấu bằng @Controller, @RestController, @RequestMapping, @GetMapping, v.v., và tạo ra ánh xạ dựa trên thông tin trong các annotation đó.

Trong hầu hết các trường hợp khi sử dụng annotation, bạn sẽ làm việc chủ yếu với RequestMappingHandlerMapping, mặc dù bạn không cần cấu hình nó một cách rõ ràng trong Spring Boot (vì Auto-configuration đã làm điều đó cho bạn).


@Controller
@RequestMapping("/users") // Class-level mapping
public class UserController {

    @GetMapping("/{id}") // Method-level mapping, combined with class-level -> /users/{id}
    public String getUser(@PathVariable Long id, Model model) {
        // logic to get user by id
        User user = userService.getUserById(id);
        model.addAttribute("user", user);
        return "userDetails"; // logical view name
    }

    @PostMapping // combined with class-level -> /users
    public String createUser(@ModelAttribute User user) {
        // logic to save user
        userService.saveUser(user);
        return "redirect:/users"; // redirect to /users after creating
    }
}

Trong ví dụ trên, RequestMappingHandlerMapping sẽ tìm thấy phương thức getUser được ánh xạ tới GET /users/{id} và phương thức createUser tới POST /users.

HandlerAdapter

HandlerAdapter là giao diện giúp DispatcherServlet thực thi một handler đã được HandlerMapping tìm thấy, bất kể kiểu của handler đó là gì. Điều này mang lại sự linh hoạt cho Spring MVC, cho phép hỗ trợ nhiều kiểu handler khác nhau.

  • SimpleControllerHandlerAdapter: Dành cho các handler implement giao diện Controller (từ thời Spring 2.x trở về trước, ít dùng hiện nay).
  • HttpRequestHandlerAdapter: Dành cho các handler implement giao diện HttpRequestHandler.
  • RequestMappingHandlerAdapter: Đây là adapter được sử dụng phổ biến nhất hiện nay, chuyên xử lý các handler được đánh dấu bằng annotation (@RequestMapping). Nó chịu trách nhiệm gọi đúng phương thức Controller, xử lý tham số đầu vào (như @PathVariable, @RequestParam, @RequestBody thông qua HandlerMethodArgumentResolver) và xử lý giá trị trả về (như String cho tên View, ModelAndView, đối tượng POJO cho JSON/XML thông qua HandlerMethodReturnValueHandler).

RequestMappingHandlerAdapter là nơi diễn ra rất nhiều “magic” liên quan đến việc tự động binding dữ liệu từ request vào các tham số phương thức Controller của bạn.

Controller

Như đã nói, Controller là nơi chứa logic để xử lý một yêu cầu cụ thể. Trong Spring MVC hiện đại, các lớp Controller thường được đánh dấu bằng @Controller hoặc @RestController. Các phương thức trong Controller được đánh dấu bằng @RequestMapping hoặc các annotation dẫn xuất như @GetMapping, @PostMapping, v.v., để ánh xạ với URL.

Các phương thức Controller có thể có nhiều kiểu tham số đầu vào khác nhau (được xử lý bởi HandlerMethodArgumentResolver) và nhiều kiểu giá trị trả về khác nhau (được xử lý bởi HandlerMethodReturnValueHandler), mang lại sự linh hoạt đáng kinh ngạc.


@Controller
public class ProductController {

    private final ProductService productService; // Injected via Dependency Injection

    public ProductController(ProductService productService) { // Constructor Injection
        this.productService = productService;
    }

    @GetMapping("/products")
    public String listProducts(Model model) {
        List<Product> products = productService.getAllProducts();
        model.addAttribute("products", products); // Add data to the Model
        return "productList"; // Return logical view name
    }

    @GetMapping("/products/{id}")
    public String getProductDetail(@PathVariable Long id, Model model) {
        Product product = productService.getProductById(id);
        model.addAttribute("product", product);
        return "productDetail";
    }
}

Bạn có thể thấy Controller tương tác với Service layer (logic nghiệp vụ) và thêm dữ liệu vào Model để gửi đến View. Việc inject ProductService là ví dụ điển hình về Dependency Injection trong Spring.

ModelAndView

ModelAndView là một lớp đơn giản dùng để đóng gói cả Model (dữ liệu) và View (tên View hoặc đối tượng View). Nó là một cách truyền thống để các phương thức Controller trả về kết quả. Tuy nhiên, các phương thức Controller hiện đại thường trả về trực tiếp tên View String hoặc sử dụng đối tượng Model (trong tham số phương thức) để thêm dữ liệu và chỉ trả về tên View String.


// Using ModelAndView (classic approach)
@GetMapping("/old-way")
public ModelAndView showOldPage() {
    ModelAndView mav = new ModelAndView("oldPage"); // Set view name
    mav.addObject("message", "This is the old way!"); // Add data to model
    return mav;
}

// Using String return value and Model argument (modern approach)
@GetMapping("/new-way")
public String showNewPage(Model model) {
    model.addAttribute("message", "This is the new way!"); // Add data to model
    return "newPage"; // Return view name
}

Cả hai cách đều hoạt động, nhưng cách dùng Model argument và trả về String thường gọn gàng hơn.

ViewResolver

Sau khi DispatcherServlet nhận được tên View logic từ Controller, nó cần ViewResolver để tìm ra tài nguyên View vật lý. Spring MVC hỗ trợ nhiều loại View khác nhau (JSP, Thymeleaf, FreeMarker, v.v.) và mỗi loại thường có một ViewResolver tương ứng.

  • InternalResourceViewResolver: Phổ biến cho JSP. Bạn cấu hình tiền tố (prefix) và hậu tố (suffix) để nó xây dựng đường dẫn đến file JSP. Ví dụ: prefix=”/WEB-INF/views/”, suffix=”.jsp”. Tên View “home” sẽ được giải thành /WEB-INF/views/home.jsp. (Xem thêm về kết hợp Spring MVC và JSP)
  • ThymeleafViewResolver: Dành cho Thymeleaf templates.
  • FreeMarkerViewResolver: Dành cho FreeMarker templates.

Bạn có thể cấu hình nhiều ViewResolver và đặt thứ tự ưu tiên cho chúng.


// Example of ViewResolver configuration (Java Config)
@Configuration
public class WebConfig implements WebMvcConfigurer {

    // This is often auto-configured by Spring Boot when you include a template engine starter
    // but shown here for understanding

    @Bean
    public ViewResolver internalResourceViewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/"); // Path to your JSP files
        resolver.setSuffix(".jsp"); // File extension
        return resolver;
    }

    // Example for Thymeleaf (Spring Boot automatically configures this if thymeleaf-starter is present)
    // @Bean
    // public SpringTemplateEngine templateEngine() { ... }
    // @Bean
    // public ThymeleafViewResolver thymeleafViewResolver() {
    //     ThymeleafViewResolver resolver = new ThymeleafViewResolver();
    //     resolver.setTemplateEngine(templateEngine());
    //     resolver.setPrefix("classpath:/templates/"); // Path to your Thymeleaf files
    //     resolver.setSuffix(".html");
    //     return resolver;
    // }
}

View

Đối tượng View là kết quả của quá trình phân giải View. Nó là một giao diện chịu trách nhiệm render nội dung phản hồi cuối cùng dựa trên Model. Các triển khai cụ thể của giao diện View bao gồm JstlView (cho JSP), ThymeleafView, FreeMarkerView, MappingJackson2JsonView (để render JSON khi dùng @ResponseBody), v.v.

Quá trình render View là bước cuối cùng trước khi phản hồi được gửi đi.

Những Người Bạn Khác

Kiến trúc Spring MVC còn có nhiều thành phần tùy chọn khác mà DispatcherServlet có thể tương tác, tùy thuộc vào nhu cầu của ứng dụng:

  • HandlerExceptionResolver: Xử lý các exception nảy sinh trong quá trình xử lý request. Cho phép bạn định nghĩa các trang lỗi tùy chỉnh hoặc logic xử lý lỗi đặc biệt.
  • LocaleResolver: Xác định ngôn ngữ (locale) của người dùng dựa trên thông tin trong request (ví dụ: từ header Accept-Language, từ cookie, hoặc từ parameter). Dùng cho việc quốc tế hóa (internationalization – i18n).
  • ThemeResolver: Xác định theme cho ứng dụng (tập hợp các file CSS, hình ảnh, v.v.) dựa trên thông tin trong request.
  • MultipartResolver: Xử lý các yêu cầu multipart, chủ yếu dùng cho việc upload file.

Sự tồn tại của các thành phần “bạn bè” này và khả năng cắm (plug) các triển khai khác nhau vào DispatcherServlet chính là điểm mạnh của Spring MVC, mang lại sự linh hoạt và khả năng mở rộng cao.

Cấu Hình DispatcherServlet và Bạn Bè

Việc cấu hình DispatcherServlet và các thành phần khác đã trở nên rất đơn giản, đặc biệt là với Spring Boot. Khi bạn thêm dependency spring-boot-starter-web (mà chúng ta đã nói đến trong bài Spring Boot Starters), Spring Boot sẽ tự động:

  • Tự động cấu hình và đăng ký một DispatcherServlet.
  • Tự động cấu hình các HandlerMapping (đặc biệt là RequestMappingHandlerMapping).
  • Tự động cấu hình các HandlerAdapter (đặc biệt là RequestMappingHandlerAdapter).
  • Tự động cấu hình ViewResolver phù hợp nếu bạn có template engine (Thymeleaf, FreeMarker) hoặc cấu hình InternalResourceViewResolver nếu bạn sử dụng JSP (cần cấu hình thêm prefix/suffix).
  • Cấu hình nhiều thành phần khác như HttpMessageConverter, HandlerExceptionResolver mặc định, v.v.

Tất cả những điều này diễn ra nhờ cơ chế Auto-configuration của Spring Boot, giảm thiểu đáng kể lượng cấu hình thủ công mà bạn phải viết.

Tóm tắt: DispatcherServlet và Các Thành Phần Chính

Để dễ hình dung lại, đây là bảng tóm tắt vai trò của các thành phần chính trong kiến trúc Spring MVC:

Thành phần Vai trò chính
DispatcherServlet Front Controller: Tiếp nhận tất cả yêu cầu, điều phối đến các thành phần khác, quản lý vòng đời xử lý yêu cầu.
HandlerMapping Ánh xạ yêu cầu (URL, phương thức HTTP, v.v.) đến Handler (thường là phương thức Controller).
HandlerAdapter Thực thi Handler đã tìm thấy, xử lý tham số đầu vào và giá trị trả về của phương thức Controller.
Controller Chứa logic xử lý nghiệp vụ cho một yêu cầu cụ thể. Tương tác với Model và chọn View.
ModelAndView Đóng gói dữ liệu (Model) và tên View logic/đối tượng View.
ViewResolver Phân giải tên View logic thành tài nguyên View vật lý (file JSP, HTML template, v.v.).
View Render nội dung phản hồi cuối cùng dựa trên dữ liệu trong Model.
HandlerExceptionResolver Xử lý các ngoại lệ xảy ra trong quá trình xử lý yêu cầu.

Kết luận

DispatcherServlet không chỉ là một Servlet đơn thuần; nó là trái tim và bộ não của Spring MVC, là điểm trung tâm điều phối toàn bộ quy trình xử lý yêu cầu web. Bằng cách ủy quyền các nhiệm vụ cụ thể cho các thành phần “bạn bè” chuyên biệt như HandlerMapping, HandlerAdapter, ViewResolver, v.v., Spring MVC đạt được sự phân tách rõ ràng, linh hoạt và khả năng mở rộng cao.

Hiểu rõ kiến trúc này sẽ giúp bạn dễ dàng hơn trong việc cấu hình, tùy chỉnh và gỡ lỗi các ứng dụng web Spring MVC. Nó cũng cho thấy cách Spring tận dụng sức mạnh của IoC Container và Dependency Injection để quản lý và kết nối các thành phần này lại với nhau một cách liền mạch.

Chặng đường khám phá Java Spring Roadmap vẫn còn nhiều điều thú vị. Ở các bài viết tiếp theo, chúng ta sẽ tiếp tục đi sâu vào các khía cạnh khác của Spring Framework, ví dụ như cách xử lý exception, các cơ chế binding dữ liệu phức tạp hơn, hay làm thế nào để tạo ra các RESTful API mạnh mẽ với Spring MVC (hoặc cụ thể hơn là Spring WebFlux cho các ứng dụng reactive). Đừng quên theo dõi và tiếp tục học hỏi nhé!

Nếu có bất kỳ câu hỏi nào, đừng ngần ngại để lại bình luận bên dưới!

Hẹn gặp lại trong bài viết tiếp theo của series Java Spring Roadmap!

Chỉ mục