Java Spring Roadmap: Các Thành Phần Thiết Yếu Của Một Ứng Dụng Web Spring MVC

Chào mừng các bạn quay trở lại với series “Java Spring Roadmap”! Trên hành trình khám phá sức mạnh của Spring Framework, chúng ta đã cùng nhau đi qua những khái niệm nền tảng như IoC Container, Dependency Injection (DI), quản lý Beans, làm chủ Annotations, và hiểu cách Spring hoạt động ở cấp độ cơ bản. Chúng ta cũng đã có bài giới thiệu sơ lược về Spring MVC và tìm hiểu nền tảng Servlet mà Spring MVC dựa trên. Hôm nay, chúng ta sẽ đào sâu hơn vào “trái tim” của việc xây dựng ứng dụng web truyền thống với Spring: Các thành phần cốt lõi tạo nên một ứng dụng Spring MVC hoàn chỉnh.

Spring MVC là một module mạnh mẽ trong Spring Framework, cung cấp kiến trúc Model-View-Controller (MVC) để phát triển các ứng dụng web linh hoạt và có khả năng mở rộng. Việc hiểu rõ từng thành phần trong mô hình này là chìa khóa để bạn làm chủ luồng xử lý request (yêu cầu) từ người dùng, xử lý logic nghiệp vụ và trả về response (phản hồi) phù hợp.

Hãy cùng nhau khám phá những “viên gạch” xây dựng nên ứng dụng Spring MVC của bạn!

1. DispatcherServlet: Người Gác Cổng Tận Tâm

Nếu coi ứng dụng web của bạn là một tòa nhà, thì DispatcherServlet chính là người gác cổng chính, là điểm vào duy nhất cho mọi request. Đây là thành phần trung tâm trong kiến trúc Spring MVC. Khi một request web được gửi đến ứng dụng Spring MVC, nó sẽ đi qua DispatcherServlet đầu tiên. Công việc chính của DispatcherServlet là tiếp nhận request, xác định controller phù hợp để xử lý request đó, điều phối luồng xử lý và cuối cùng là phân phối kết quả (view) trả về cho client.

Tại sao lại cần DispatcherServlet?

  • Tập trung hóa xử lý: Mọi request đều đi qua một điểm duy nhất, giúp quản lý và áp dụng các xử lý chung (logging, security, transaction, v.v.) dễ dàng hơn.
  • Tách biệt trách nhiệm: Nó đóng vai trò là bộ điều phối, ủy quyền các công việc cụ thể (tìm controller, render view) cho các thành phần chuyên biệt khác.
  • Linh hoạt cấu hình: Bạn có thể cấu hình DispatcherServlet để tùy chỉnh cách các request được xử lý.

Chúng ta đã tìm hiểu kỹ về vai trò và cách hoạt động của DispatcherServlet cùng những thành phần phụ trợ của nó trong bài viết trước. Hãy luôn nhớ rằng đây là “trái tim” của luồng xử lý request trong Spring MVC.

2. Controllers: Nơi Xử Lý Logic Request

Sau khi DispatcherServlet tiếp nhận một request, nó cần biết ai là người có khả năng xử lý request đó. Đó chính là vai trò của các Controller. Controller là các lớp Java được đánh dấu bằng Annotation @Controller.

@Controller
public class HomeController {

    // Các phương thức xử lý request sẽ nằm ở đây
}

Trong Controller, bạn định nghĩa các phương thức (handler methods) để xử lý các request cụ thể dựa trên URL, phương thức HTTP (GET, POST, PUT, DELETE), và các tiêu chí khác.

2.1. Ánh Xạ Request (Request Mapping)

Để Spring MVC biết phương thức nào trong Controller sẽ xử lý URL nào, chúng ta sử dụng Annotation @RequestMapping (hoặc các Annotation chuyên biệt hơn như @GetMapping, @PostMapping, @PutMapping, @DeleteMapping). Annotation này có thể được áp dụng ở cấp độ lớp (class) hoặc cấp độ phương thức (method).

@Controller
@RequestMapping("/users") // Áp dụng prefix "/users" cho tất cả các handler methods trong controller này
public class UserController {

    @GetMapping("/{id}") // Xử lý GET request tới "/users/{id}"
    public String getUserById(@PathVariable("id") int userId, Model model) {
        // Logic lấy thông tin user từ database
        User user = userService.findUserById(userId);
        // Thêm user vào model để hiển thị trên view
        model.addAttribute("user", user);
        return "user-detail"; // Trả về tên view logic
    }

    @PostMapping // Xử lý POST request tới "/users"
    public String createUser(@ModelAttribute("user") User user, BindingResult result) {
        if (result.hasErrors()) {
            // Xử lý lỗi validation
            return "user-form"; // Quay lại form nếu có lỗi
        }
        // Logic lưu user vào database
        userService.saveUser(user);
        return "redirect:/users/" + user.getId(); // Redirect sau khi lưu thành công
    }
}

Ở đây, @RequestMapping("/users") ở cấp độ lớp giúp nhóm các request liên quan đến người dùng dưới cùng một đường dẫn. @GetMapping("/{id}") ánh xạ request GET tới `/users/{id}`, và @PostMapping ánh xạ request POST tới `/users`.

2.2. Trích Xuất Dữ Liệu Từ Request

Controller cần truy cập dữ liệu từ request (tham số URL, dữ liệu form, body của request). Spring MVC cung cấp các Annotation tiện lợi để làm điều này:

  • @RequestParam: Trích xuất giá trị của một tham số từ query string (ví dụ: `/users?id=123`) hoặc dữ liệu form (ví dụ: khi submit form POST).
  • @PathVariable: Trích xuất giá trị từ một phần của URL đường dẫn (ví dụ: `/users/123` -> trích xuất `123`).
  • @RequestBody: Trích xuất toàn bộ body của request và tự động chuyển đổi (deserialize) nó thành một đối tượng Java (thường dùng cho các request POST/PUT chứa dữ liệu JSON/XML).
  • @ModelAttribute: Liên kết dữ liệu từ request parameters/form với một đối tượng Java. Nó cũng có thể thêm đối tượng này vào Model.

Ví dụ:

@GetMapping("/search")
public String searchUsers(@RequestParam(name = "keyword", required = false) String keyword, Model model) {
    List<User> users = userService.searchUsers(keyword);
    model.addAttribute("users", users);
    return "user-list";
}

@PostMapping("/create")
public ResponseEntity<User> createUserFromJson(@RequestBody User user) {
    User createdUser = userService.saveUser(user);
    return ResponseEntity.status(HttpStatus.CREATED).body(createdUser);
}

Các Annotation này giúp developer tập trung vào logic nghiệp vụ mà không phải bận tâm nhiều đến việc xử lý trực tiếp các đối tượng Servlet như `HttpServletRequest`.

3. Model: Mang Dữ Liệu Đến View

Sau khi Controller xử lý logic nghiệp vụ (ví dụ: lấy dữ liệu từ database), nó cần truyền dữ liệu đó tới View để hiển thị cho người dùng. Thành phần Model (hoặc ModelMap) chính là nơi chứa dữ liệu này.

Model là một interface trong Spring MVC, cung cấp một cách để “đính kèm” các đối tượng Java vào request scope. Khi Controller trả về tên của View, Spring MVC sẽ tự động truyền các dữ liệu trong Model đến View đó.

@GetMapping("/greet/{name}")
public String greeting(@PathVariable String name, Model model) {
    // Thêm dữ liệu vào Model
    model.addAttribute("message", "Hello, " + name + "!");
    model.addAttribute("timestamp", new Date());

    // Trả về tên view logic
    return "greeting";
}

Trong ví dụ trên, controller thêm hai thuộc tính “message” và “timestamp” vào Model. View “greeting” (có thể là file JSP, Thymeleaf, v.v.) sau đó có thể truy cập các thuộc tính này để hiển thị nội dung động.

4. Views và View Resolvers: Hiển Thị Kết Quả

View là thành phần chịu trách nhiệm tạo ra giao diện cuối cùng (HTML, JSON, XML, v.v.) để trả về cho client. Controller chỉ trả về một tên logic của View (ví dụ: “greeting”, “user-detail”). Nhiệm vụ của ViewResolver là dựa vào tên logic đó để tìm ra View vật lý tương ứng.

Các View có thể được triển khai bằng nhiều công nghệ khác nhau như:

  • JSP (JavaServer Pages): Công nghệ truyền thống của Java. Chúng ta đã có bài về Spring MVC và JSP.
  • Thymeleaf: Một template engine hiện đại và phổ biến, dễ dàng tích hợp với Spring.
  • FreeMarker: Một template engine khác.
  • Hoặc đơn giản là trả về dữ liệu dạng JSON/XML (thường dùng cho RESTful APIs), trong trường hợp này ViewResolver sẽ sử dụng các Message Converter phù hợp.

Cấu hình ViewResolver thường được thực hiện thông qua cấu hình Spring. Ví dụ, để cấu hình InternalResourceViewResolver cho JSP:

@Configuration
@EnableWebMvc // Kích hoạt cấu hình MVC
public class WebConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver viewResolver() {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/"); // Thư mục chứa file JSP
        resolver.setSuffix(".jsp");          // Hậu tố của file JSP
        return resolver;
    }

    // ... các cấu hình MVC khác ...
}

Với cấu hình trên, khi Controller trả về tên view là “greeting”, InternalResourceViewResolver sẽ tìm đến file `/WEB-INF/views/greeting.jsp` và render nội dung của nó.

5. WebMvcConfigurer: Tùy Biến Cấu Hình MVC

WebMvcConfigurer là một interface trong Spring MVC cho phép bạn dễ dàng tùy biến các khía cạnh của cấu hình MVC mà không cần can thiệp sâu vào cấu hình mặc định của Spring. Bằng cách implement interface này trong một lớp cấu hình (được đánh dấu `@Configuration`), bạn có thể:

  • Thêm các Interceptors (để xử lý trước hoặc sau khi handler method được gọi, ví dụ: logging, authentication).
  • Cấu hình static resources (CSS, JavaScript, hình ảnh).
  • Cấu hình View Controllers đơn giản (ánh xạ URL trực tiếp đến view mà không cần controller method).
  • Cấu hình Message Converters (để chuyển đổi dữ liệu HTTP body sang đối tượng Java và ngược lại, ví dụ: JSON <-> Object).
  • Cấu hình Cors mapping.
  • Và nhiều tùy chọn khác.
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        // Ánh xạ URL "/about" trực tiếp tới view "about"
        registry.addViewController("/about").setViewName("about");
    }

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        // Cấu hình phục vụ static resources từ thư mục "/static/"
        registry.addResourceHandler("/static/**")
                .addResourceLocations("classpath:/static/");
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // Thêm một custom interceptor
        registry.addInterceptor(new MyLoggingInterceptor()).addPathPatterns("/**");
    }
}

Sử dụng WebMvcConfigurer là cách tiếp cận được khuyến khích trong Spring Boot để tùy chỉnh MVC, vì nó giữ cho cấu hình của bạn rõ ràng và có tổ chức.

6. Xử Lý Ngoại Lệ (Exception Handling)

Trong quá trình xử lý request, có thể xảy ra ngoại lệ (exception). Spring MVC cung cấp nhiều cách để xử lý các ngoại lệ này một cách graceful và trả về phản hồi lỗi thân thiện cho người dùng thay vì một trang lỗi trắng hoặc stack trace đáng sợ.

  • @ExceptionHandler: Đặt trên một phương thức trong Controller hoặc lớp `@ControllerAdvice` để xử lý các loại ngoại lệ cụ thể.
  • @ControllerAdvice / @RestControllerAdvice: Lớp đánh dấu Annotation này có thể chứa các phương thức `@ExceptionHandler` áp dụng cho nhiều Controller khác nhau.
  • HandlerExceptionResolver: Interface cấp thấp hơn cho phép kiểm soát chi tiết hơn quá trình xử lý ngoại lệ.
@ControllerAdvice // Áp dụng cho tất cả các Controllers
public class GlobalExceptionHandler {

    @ExceptionHandler(ResourceNotFoundException.class)
    @ResponseStatus(HttpStatus.NOT_FOUND) // Trả về status code 404
    public String handleResourceNotFound(ResourceNotFoundException ex, Model model) {
        model.addAttribute("error", "Resource not found: " + ex.getMessage());
        return "error-page"; // Trả về view lỗi tùy chỉnh
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST) // Trả về status code 400
    public ResponseEntity<ErrorResponse> handleValidationErrors(MethodArgumentNotValidException ex) {
        // Xử lý lỗi validation, trả về JSON
        List<String> errors = ex.getBindingResult().getFieldErrors().stream()
                .map(error -> error.getField() + ": " + error.getDefaultMessage())
                .collect(Collectors.toList());
        ErrorResponse errorResponse = new ErrorResponse("Validation Failed", errors);
        return ResponseEntity.badRequest().body(errorResponse);
    }
}

Việc triển khai xử lý ngoại lệ giúp cải thiện trải nghiệm người dùng và cung cấp thông tin lỗi hữu ích hơn cho client (đặc biệt quan trọng với RESTful APIs).

7. Validation (Kiểm Tra Dữ Liệu Đầu Vào)

Trước khi xử lý dữ liệu từ người dùng (ví dụ: dữ liệu nhập từ form hoặc JSON body), việc kiểm tra tính hợp lệ của dữ liệu là rất quan trọng để tránh các lỗi nghiệp vụ hoặc lỗ hổng bảo mật. Spring MVC tích hợp chặt chẽ với Bean Validation (JSR 303, JSR 349, v.v.), thường sử dụng Hibernate Validator là implementation phổ biến nhất.

Bạn chỉ cần thêm các Annotation validation (như @NotNull, @Size, @Min, @Max, @Email, v.v.) vào các trường trong đối tượng Java đại diện cho dữ liệu đầu vào (ví dụ: một lớp POJO). Sau đó, sử dụng Annotation @Valid hoặc @Validated trước tham số của handler method và thêm đối tượng BindingResult để kiểm tra kết quả validation.

public class UserForm {

    @NotNull(message = "Username cannot be null")
    @Size(min = 3, max = 20, message = "Username must be between 3 and 20 characters")
    private String username;

    @Email(message = "Invalid email format")
    private String email;

    // getters and setters
}

@PostMapping("/register")
public String registerUser(@Valid @ModelAttribute("userForm") UserForm userForm, BindingResult result, Model model) {
    if (result.hasErrors()) {
        // Nếu có lỗi validation, thêm lại lỗi vào model và trả về form view
        model.addAttribute("errors", result.getAllErrors());
        return "registration-form";
    }
    // Nếu validation thành công, xử lý đăng ký
    userService.register(userForm);
    return "redirect:/registration-success";
}

BindingResult chứa thông tin về bất kỳ lỗi validation nào. Bạn có thể kiểm tra result.hasErrors() và lấy danh sách lỗi để hiển thị cho người dùng.

Tổng Kết Các Thành Phần Cốt Lõi

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

Thành phần Vai trò chính Annotation/Interface liên quan
DispatcherServlet Front Controller, điểm vào duy nhất, điều phối request. Cấu hình trong web.xml hoặc Java Config (AbstractAnnotationConfigDispatcherServletInitializer).
Controllers Xử lý logic nghiệp vụ cho request cụ thể, ánh xạ URL. @Controller, @RequestMapping, @GetMapping, @PostMapping, v.v.
Request Data Extraction Trích xuất dữ liệu từ request (parameters, path variables, body). @RequestParam, @PathVariable, @RequestBody, @ModelAttribute.
Model Chứa dữ liệu được truyền từ Controller sang View. Interface Model, ModelMap.
Views Tạo giao diện phản hồi cuối cùng (HTML, JSON, v.v.). JSP, Thymeleaf, FreeMarker, RESTful output.
ViewResolver Tìm View vật lý dựa trên tên view logic do Controller trả về. InternalResourceViewResolver, ThymeleafViewResolver, ContentNegotiatingViewResolver, v.v.
WebMvcConfigurer Cung cấp API để tùy biến cấu hình Spring MVC. Interface WebMvcConfigurer, Annotation @Configuration, @EnableWebMvc.
Exception Handling Xử lý ngoại lệ xảy ra trong quá trình xử lý request. @ExceptionHandler, @ControllerAdvice, HandlerExceptionResolver.
Validation Kiểm tra tính hợp lệ của dữ liệu đầu vào. @Valid, @Validated, BindingResult, Bean Validation Annotations (@NotNull, @Size, v.v.).

Luồng Xử Lý Request Cơ Bản trong Spring MVC

Khi hiểu rõ các thành phần, việc nắm bắt luồng xử lý request sẽ trở nên dễ dàng hơn:

  1. Request được gửi đến DispatcherServlet.
  2. DispatcherServlet yêu cầu HandlerMapping tìm Controller (và phương thức xử lý) phù hợp với request.
  3. HandlerMapping trả về Controller và Handler Method tương ứng.
  4. DispatcherServlet yêu cầu HandlerAdapter thực thi Handler Method của Controller.
  5. Handler Method trong Controller xử lý logic nghiệp vụ (ví dụ: gọi Service/Repository), có thể tương tác với Model để thêm dữ liệu.
  6. Handler Method trả về một đối tượng ModelAndView (hoặc chỉ tên view logic và dữ liệu trong Model).
  7. DispatcherServlet sử dụng ViewResolver để phân giải tên view logic thành một đối tượng View vật lý.
  8. View sử dụng dữ liệu từ Model để render nội dung phản hồi (ví dụ: tạo trang HTML).
  9. DispatcherServlet trả về phản hồi cuối cùng cho client.

Luồng này có thể phức tạp hơn với sự tham gia của Interceptors, Exception Handlers, v.v., nhưng đây là core flow bạn cần nắm vững.

Lời Kết

Hy vọng bài viết này đã cung cấp cho bạn cái nhìn rõ ràng và chi tiết về các thành phần thiết yếu tạo nên một ứng dụng web sử dụng Spring MVC. Việc hiểu sâu về vai trò và cách tương tác của DispatcherServlet, Controllers, Model, Views, và View Resolvers là cực kỳ quan trọng để bạn có thể xây dựng và debug các ứng dụng web Spring MVC hiệu quả.

Đây là những kiến thức nền tảng vững chắc trên con đường làm chủ Spring Framework theo Lộ trình Java Spring Roadmap của chúng ta. Trong các bài viết tiếp theo, chúng ta sẽ tiếp tục khám phá những khía cạnh khác của Spring, bao gồm cả việc kết hợp MVC với các công nghệ hiện đại hơn và cách Spring Boot đơn giản hóa đáng kể cấu hình MVC.

Hãy tiếp tục theo dõi series để không bỏ lỡ những kiến thức hữu ích nhé!

Chỉ mục