Spring REST API & Data JPA
3/46
Bài 3 / 46~12 phútSpring MVC CoreLộ trình · chặng 15/21Miễn phí… lượt xem

URL Routing Pipeline: HandlerMapping Và PathPattern

Khám phá luồng định tuyến URL trong DispatcherServlet: cơ chế HandlerMapping, phân giải HandlerAdapter và công cụ PathPatternParser trong Spring 6.

TL;DR: Khi request đến, DispatcherServlet tra cứu HandlerMapping để ánh xạ đường dẫn URL và phương thức HTTP thành HandlerExecutionChain. Tiếp đó, HandlerAdapter tiếp nhận chuỗi thực thi để phân giải tham số và gọi controller method tương ứng. Việc phân tách hai tầng này tuân thủ nguyên lý Open-Closed, cho phép mở rộng thêm các dạng handler mới mà không cần can thiệp vào mã nguồn servlet cốt lõi. Từ Spring 6, công cụ PathPatternParser được kích hoạt mặc định, giúp tăng tốc độ định tuyến gấp 6 đến 8 lần so với AntPathMatcher.

4 bước cốt lõi trong pipeline định tuyến URL gồm những gì?

Khi yêu cầu HTTP đi vào phương thức doDispatch() của DispatcherServlet, quá trình định tuyến diễn ra tuần tự qua bốn chặng:

  1. DispatcherServlet gửi HttpServletRequest tới danh sách HandlerMapping qua lệnh getHandler(request).
  2. HandlerMapping so khớp thông tin URL, HTTP method, header và trả về HandlerExecutionChain chứa method đích cùng các interceptor.
  3. DispatcherServlet duyệt qua danh sách HandlerAdapter để chọn adapter phù hợp thông qua hàm supports(handler).
  4. HandlerAdapter phân giải các tham số đầu vào từ request, thực thi phương thức Java của controller và chuyển giao kết quả trả về.

Nhờ sự phân định ranh giới rõ ràng, tầng định tuyến URL hoàn toàn độc lập với cách thức gọi thực thi mã nguồn Java bên dưới.

Vì sao Spring tách biệt HandlerMapping và HandlerAdapter theo Open-Closed?

Trong hệ sinh thái Spring Framework, một handler có thể tồn tại dưới nhiều hình thái khác nhau:

  • Lớp cài đặt interface cổ điển Controller hoặc HttpRequestHandler.
  • Phương thức chú thích @RequestMapping hoặc @GetMapping.
  • Hàm tạm dừng (suspend function) trong Kotlin Coroutines.

Nếu DispatcherServlet trực tiếp thực thi handler, nó sẽ phải chứa hàng loạt câu lệnh if/else instanceof để kiểm tra từng loại đối tượng. Bất cứ khi nào ngôn ngữ hay framework bổ sung kiểu thực thi mới, mã nguồn của servlet trung tâm sẽ buộc phải viết lại, vi phạm nguyên lý Open-Closed Principle.

DispatcherServlet cầm HandlerExecutionChain rồi hỏi lần lượt adapter nào supports handler; ba nhánh đi tới RequestMappingHandlerAdapter cho method có @RequestMapping, HttpRequestHandlerAdapter cho resource tĩnh, và một adapter thêm về sau cho gRPC hay coroutine

Thay vào đó, Spring áp dụng mẫu thiết kế Adapter Pattern: DispatcherServlet chỉ cần ủy thác cho HandlerAdapter. Muốn hỗ trợ kiểu handler mới, lập trình viên chỉ cần đăng ký thêm một bean adapter tương ứng vào Spring context mà không làm xáo trộn servlet cốt lõi.

Quá trình tra cứu bản đồ HandlerMapping diễn ra như thế nào?

Lớp RequestMappingHandlerMapping quét toàn bộ các bean trong container khi ứng dụng khởi động để xây dựng bảng định tuyến cố định:

Java
@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public OrderDto getOrder(@PathVariable Long id) {
        return orderService.findById(id);
    }
}

Bảng định tuyến ánh xạ từng tiêu chí:

  • Đường dẫn URL mẫu (URL Pattern).
  • Phương thức HTTP (GET, POST, PUT, DELETE).
  • Kiểu dữ liệu chấp nhận (consumes) và kiểu trả về (produces).

Khi nhận request, mapper tiến hành đối soát theo nguyên tắc ưu tiên cấu hình cụ thể nhất (most specific match). Nếu xuất hiện hai handler có cùng mức độ khớp trên cùng một URL, Spring Boot sẽ ném ngoại lệ IllegalStateException ngay lúc khởi động (fail-fast) để ngăn chặn hành vi định tuyến sai lệch ngầm.

PathPatternParser vượt trội hơn AntPathMatcher ở những điểm nào?

Từ phiên bản Spring 6 (Spring Boot 3), công cụ PathPatternParser thay thế hoàn toàn AntPathMatcher làm bộ phân tích URL mặc định.

Tiêu chí so sánhAntPathMatcher (Legacy)PathPatternParser (Hiện đại)
Thời điểm phân tích cú phápPhân tích lại chuỗi trên từng requestBiên dịch thành cây PathPattern một lần lúc startup
Độ phức tạp thuật toánQuét chuỗi $O(N)$ lặp đi lặp lạiTra cứu theo độ sâu đường dẫn $O(d)$
Hiệu năng xử lýMức cơ sởNhanh hơn gấp 6 đến 8 lần
Quy tắc dấu gạch chéo cuốiCoi /orders/ tương đương /ordersPhân biệt nghiêm ngặt hai URL khác nhau

Hai cột đối chiếu: AntPathMatcher giữ pattern dạng chuỗi rồi parse lại từ đầu ở mỗi request, còn PathPatternParser compile mỗi pattern thành cây PathPattern ngay lúc khởi động nên mỗi request chỉ đi cây

Cú pháp PathPattern hỗ trợ các biểu thức linh hoạt:

  • /orders/{id:\\d+}: Bắt biến đường dẫn với điều kiện số nguyên dương. Nếu client truyền chuỗi ký tự, hệ thống trả về 404 (không tìm thấy handler phù hợp).
  • /files/{*path}: Bắt toàn bộ các phân đoạn còn lại của URL vào một biến duy nhất.

HandlerAdapter thực thi giải nén tham số và gọi controller ra sao?

Khi tiếp nhận HandlerMethod, bean RequestMappingHandlerAdapter phối hợp cùng chuỗi các bộ phân giải tham số độc lập:

Bốn việc của RequestMappingHandlerAdapter xếp dọc: resolve tham số từ @PathVariable @RequestBody @RequestParam, gọi method.invoke, xử lý giá trị trả về, ghi body qua HttpMessageConverter; hai bước đầu và bước ba đều có điểm cắm thêm resolver hoặc return value handler

  1. Phân giải danh sách đối số: Adapter duyệt qua danh sách hơn 30 HandlerMethodArgumentResolver có sẵn để trích xuất dữ liệu từ URL, query string, header hoặc HTTP body.
  2. Thực thi phương thức: Gọi phương thức Java qua Reflection với mảng đối số đã được ép kiểu.
  3. Xử lý giá trị trả về: Sử dụng HandlerMethodReturnValueHandler để đóng gói dữ liệu và gọi HttpMessageConverter xuất chuỗi JSON ra response.

Bạn có thể tạo bộ phân giải tùy biến (ví dụ: tự động inject thông tin người dùng từ JWT Token) bằng cách cài đặt HandlerMethodArgumentResolver:

Java
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(CurrentUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory) {
        String token = webRequest.getHeader("Authorization");
        return jwtService.extractUser(token);
    }
}

Mã nguồn doDispatch hé lộ điều gì về cơ chế thực thi?

Dưới đây là cấu trúc giản lược của phương thức doDispatch() trong DispatcherServlet:

Java
// Simplified execution flow inside DispatcherServlet
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HandlerExecutionChain mappedHandler = getHandler(request);
    if (mappedHandler == null) {
        noHandlerFound(request, response);
        return;
    }

    HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

    if (!mappedHandler.applyPreHandle(request, response)) {
        return; // Short-circuited by interceptor
    }

    ModelAndView mv = ha.handle(request, response, mappedHandler.getHandler());
    mappedHandler.applyPostHandle(request, response, mv);
    processDispatchResult(request, response, mappedHandler, mv, null);
}

Mỗi chặng đều được thiết kế để ngắt luồng sớm: nếu getHandler() không tìm ra ánh xạ, mã 404 được trả về ngay; nếu một interceptor trả về false trong preHandle(), controller method sẽ không bao giờ được gọi.

Những lưu ý vận hành và lỗi thường gặp trong production là gì?

Dưới đây là các lưu ý quan trọng khi triển khai routing trên hệ thống thực tế:

  1. Lỗi 404 do trailing slash khi nâng cấp Spring Boot 3: Client gửi request tới /api/orders/ nhưng backend chỉ khai báo /api/orders. Cần cập nhật client gửi đúng URL hoặc định nghĩa mảng mapping { "/orders", "/orders/" }.
  2. Tác vụ nặng chặn luồng trong Interceptor: Thực hiện truy vấn cơ sở dữ liệu đồng bộ trong preHandle() sẽ trở thành điểm nghẽn hiệu năng lớn nhất của toàn bộ hệ thống.
  3. Nhầm lẫn giữa mã lỗi 400 và 404 khi dùng Regex Path: Đặt ràng buộc /orders/{id:\\d+} sẽ khiến request chứa ký tự chữ nhận về mã 404 thay vì 400 Bad Request.

Tóm tắt

  • Luồng định tuyến chia làm hai phần: HandlerMapping tìm kiếm handler, HandlerAdapter gọi thực thi.
  • Thiết kế Adapter Pattern giúp Spring MVC tuân thủ chặt chẽ nguyên lý Open-Closed.
  • PathPatternParser trong Spring 6 biên dịch đường dẫn một lần lúc startup, mang lại hiệu năng vượt trội.
  • Hệ thống hỗ trợ mở rộng tham số linh hoạt thông qua các HandlerMethodArgumentResolver.
  • Bật log mức TRACE cho package org.springframework.web.servlet là phương pháp nhanh nhất để điều tra nguyên nhân request không đến controller.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao DispatcherServlet lại ủy thác việc thực thi controller cho HandlerAdapter thay vì tự gọi phương thức trực tiếp?
  2. Q2
    Một controller khai báo @GetMapping('/api/orders/{id:\d+}'). Nếu client gửi request GET /api/orders/abc, hệ thống trả về mã trạng thái nào?
  3. Q3
    Điểm cải tiến vượt trội nhất của PathPatternParser trong Spring 6 so với AntPathMatcher cũ là gì?
  4. Q4
    Khi nâng cấp từ Spring Boot 2 lên Spring Boot 3, tại sao request có dấu gạch chéo cuối (/orders/) lại có thể nhận mã lỗi 404?
  5. Q5
    Muốn tạo một annotation tùy biến như @CurrentUser để tự động inject thông tin UserPrincipal vào tham số của controller, lập trình viên cần cài đặt interface nào?

Bài tiếp theo: @RestController & Request Mapping

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 15/21 · REST API với Spring MVC — bài 3/21 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

@RestController & Request Mapping: Xây Dựng REST API