DispatcherServlet & HandlerMapping: ai điều phối request

4 câu hỏi~25 phútTừ khóa: Front Controller, DispatcherServlet, RequestMappingHandlerMapping, HandlerAdapter, HandlerMethodArgumentResolver, HandlerMethodReturnValueHandler, ViewResolver, @ResponseBody

Tiếp đi: DispatcherServlet. Bài trước nói nó "điều phối tất cả", cụ thể nó làm gì?

DispatcherServlet là servlet duy nhất của ứng dụng Spring MVC, map vào /. Nó không tự xử lý nghiệp vụ, chỉ hỏi các "chuyên gia" rồi ghép kết quả lại.

1. Bài toán: shop-app có hàng chục endpoint

GET    /api/products/{id}      ProductController#get
GET    /api/products?page=0    ProductController#list
POST   /api/orders             OrderController#create
GET    /api/orders/{id}        OrderController#get
POST   /api/orders/{id}/cancel OrderController#cancel

2. Vấn đề: thời Servlet thuần, mỗi URL một servlet

@WebServlet("/api/orders/*")
public class OrderServlet extends HttpServlet {
    protected void doPost(HttpServletRequest req, HttpServletResponse res) throws IOException {
        String path = req.getPathInfo();                       // tự cắt URL
        if (path == null) {
            String body = req.getReader().lines().collect(joining());
            CreateOrderRequest dto = new ObjectMapper().readValue(body, CreateOrderRequest.class);
            // tự validate, tự gọi service, tự set Content-Type, tự ghi JSON...
        } else if (path.endsWith("/cancel")) { ... }
    }
}

3. Giải pháp: Front Controller pattern

Một cửa vào duy nhất, chia việc cho các bean chiến lược (strategy). Mỗi loại việc là một interface, Spring Boot đăng ký sẵn các implementation.

InterfaceImplementation chínhTrả lời câu hỏi
HandlerMappingRequestMappingHandlerMappingRequest này do ai xử lý?
HandlerAdapterRequestMappingHandlerAdapterGọi handler đó bằng cách nào?
HandlerMethodArgumentResolverRequestResponseBodyMethodProcessor, PathVariableMethodArgumentResolver...Lấy giá trị cho từng tham số ở đâu?
HandlerMethodReturnValueHandlerRequestResponseBodyMethodProcessor, ViewNameMethodReturnValueHandler...Kết quả trả về xử lý ra sao?
HandlerExceptionResolverExceptionHandlerExceptionResolverCó exception thì làm gì?
ViewResolverThymeleafViewResolverTên view "orders/list" là file nào?

4. Code từng bước

Bước 1: Dependency. spring-boot-starter-web; DispatcherServletAutoConfiguration tự tạo bean dispatcherServlet và đăng ký vào Tomcat.

Bước 2: Config

server:
  servlet:
    context-path: /shop        # mọi URL thành /shop/api/...
spring:
  mvc:
    servlet:
      path: /                  # path của DispatcherServlet (mặc định)
    log-request-details: true

Bước 3: Code Java, xem danh sách mapping và tự viết một ArgumentResolver

// In toàn bộ mapping lúc khởi động
@Component
@RequiredArgsConstructor
public class MappingPrinter implements ApplicationListener<ApplicationReadyEvent> {
    private final RequestMappingHandlerMapping mapping;

    @Override
    public void onApplicationEvent(ApplicationReadyEvent e) {
        mapping.getHandlerMethods().forEach((info, method) ->
            System.out.println(info + " -> " + method));
    }
}

Ví dụ: muốn controller nhận thẳng thông tin khách hàng từ header X-Customer-Id mà không lặp code đọc header.

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentCustomer {}

public record CustomerRef(long id) {}

public class CurrentCustomerResolver implements HandlerMethodArgumentResolver {
    @Override
    public boolean supportsParameter(MethodParameter p) {
        return p.hasParameterAnnotation(CurrentCustomer.class)
            && p.getParameterType() == CustomerRef.class;
    }
    @Override
    public Object resolveArgument(MethodParameter p, ModelAndViewContainer mav,
                                  NativeWebRequest req, WebDataBinderFactory bf) {
        String id = req.getHeader("X-Customer-Id");
        if (id == null) throw new ResponseStatusException(HttpStatus.UNAUTHORIZED);
        return new CustomerRef(Long.parseLong(id));
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentCustomerResolver());
    }
}

// OrderController
@GetMapping("/mine")
public List<OrderResponse> myOrders(@CurrentCustomer CustomerRef customer) {
    return orderService.findByCustomer(customer.id());
}

Thực tế người dùng thường lấy từ JWT qua @AuthenticationPrincipal, cũng chính là một argument resolver do Spring Security đăng ký.

Bước 4: Test bằng curl

# log lúc khởi động:
{GET [/api/orders/mine]} -> com.shop.order.OrderController#myOrders(CustomerRef)
{POST [/api/orders]}     -> com.shop.order.OrderController#create(CreateOrderRequest)

curl localhost:8080/shop/api/orders/mine -H "X-Customer-Id: 42"
# [{"id":101,"status":"PAID",...}]

curl -i localhost:8080/shop/api/orders/mine
# HTTP/1.1 401

5. Cách hoạt động: bên trong doDispatch()

DispatcherServlet.doDispatch(req, res)
 │
 ├─ 1. getHandler(req)
 │     for (HandlerMapping hm : handlerMappings)        // theo thứ tự order
 │        RequestMappingHandlerMapping  ← @RequestMapping (ưu tiên trước)
 │        RouterFunctionMapping         ← functional endpoints
 │        SimpleUrlHandlerMapping       ← static resources /**
 │     → HandlerExecutionChain(handler = OrderController#create, interceptors)
 │
 ├─ 2. getHandlerAdapter(handler)  → RequestMappingHandlerAdapter
 │
 ├─ 3. chain.applyPreHandle()
 │
 ├─ 4. adapter.handle()
 │     ├─ mỗi tham số → tìm ArgumentResolver đầu tiên supportsParameter()
 │     │     @RequestBody   → RequestResponseBodyMethodProcessor → Jackson
 │     │     @PathVariable  → PathVariableMethodArgumentResolver
 │     │     @CurrentCustomer → CurrentCustomerResolver (của mình)
 │     ├─ method.invoke(controller, args)   (qua reflection)
 │     └─ giá trị trả về → ReturnValueHandler
 │           có @ResponseBody? → converter ghi JSON, xong (mav = null)
 │           String/ModelAndView? → trả tên view
 │
 ├─ 5. chain.applyPostHandle()
 │
 ├─ 6. processDispatchResult()
 │     exception? → HandlerExceptionResolvers (@ExceptionHandler, @ControllerAdvice)
 │     có view?   → ViewResolver: "orders/list" → templates/orders/list.html → render
 │
 └─ 7. triggerAfterCompletion()

Lúc khởi động, RequestMappingHandlerMapping quét mọi bean có @Controller, đọc @RequestMapping, dựng bảng RequestMappingInfo → HandlerMethod. Khi có request, nó chỉ tra bảng này.

6. So sánh: ViewResolver và @ResponseBody

View (Thymeleaf, JSP)@ResponseBody / REST
Controller trả vềTên view + ModelObject (DTO)
Ai xử lýViewResolver → View.render()HttpMessageConverter
OutputHTML render phía serverJSON/XML
Chọn định dạngTheo templateTheo header Accept (content negotiation)
ClientTrình duyệtSPA, mobile, service khác

7. Khi nào nên đụng vào các thành phần này

Mình có GET /api/products/{id} và GET /api/products/featured. Gọi /featured có bị nhầm thành id = "featured" không?

Không bị nhầm. Khi nhiều mapping cùng khớp, Spring so sánh và chọn cái cụ thể nhất.

Request: GET /api/products/featured
  khớp: /api/products/featured   (0 biến, 0 wildcard)   ← thắng
  khớp: /api/products/{id}       (1 biến)
  khớp: /api/products/**         (wildcard)

Lưu ý: Spring Boot 3 dùng PathPatternParser và không còn khớp dấu / cuối: /api/products/ khác /api/products.

Mình viết @Controller trả về "ok" thì bị lỗi không tìm thấy template ok. Sao trả String mà nó không in chữ ok ra?

Đúng triệu chứng kinh điển. Trong @Controller, kiểu trả về String được hiểu là tên view, không phải nội dung.

@Controller                       return "ok"
   │
   ReturnValueHandler: không có @ResponseBody, kiểu String
   → ViewNameMethodReturnValueHandler → view name = "ok"
   → ViewResolver tìm templates/ok.html → không có
   → lỗi "Error resolving template [ok]" (hoặc 404/500)

@RestController / @ResponseBody   return "ok"
   → RequestResponseBodyMethodProcessor
   → StringHttpMessageConverter → body "ok", text/plain

Thứ tự ReturnValueHandler quyết định: handler cho @ResponseBody được hỏi trước, nếu không có annotation thì String rơi xuống handler tên view. Cách sửa: thêm @ResponseBody hoặc dùng @RestController (bài sau).

Phỏng vấn hỏi DispatcherServlet và HandlerMapping thì trả lời sao?

Q1. DispatcherServlet là gì, vì sao Spring MVC cần nó?

🇻🇳 Tiếng Việt:

DispatcherServlet là front controller của Spring MVC, một servlet duy nhất nhận mọi request. Nó không xử lý nghiệp vụ mà ủy quyền cho các thành phần: HandlerMapping tìm handler, HandlerAdapter gọi handler, argument resolver và message converter chuẩn bị dữ liệu, exception resolver xử lý lỗi, ViewResolver render view. Nhờ vậy logic chung nằm một chỗ và controller chỉ còn nghiệp vụ.

🇬🇧 English:

The DispatcherServlet is Spring MVC's front controller, a single servlet that receives every request. It doesn't run business logic; it delegates to strategies: HandlerMappings find the handler, a HandlerAdapter invokes it, argument resolvers and message converters prepare the data, exception resolvers handle errors, and ViewResolvers render views. Cross-cutting logic lives in one place and controllers stay focused on business code.

Q2. HandlerMapping và HandlerAdapter khác nhau thế nào?

🇻🇳 Tiếng Việt:

HandlerMapping trả lời "ai xử lý request này", ví dụ RequestMappingHandlerMapping dựng bảng từ @RequestMapping lúc khởi động. HandlerAdapter trả lời "gọi handler đó thế nào", vì handler có thể là HandlerMethod, HttpRequestHandler hay HandlerFunction. Tách hai việc giúp DispatcherServlet hỗ trợ nhiều kiểu handler mà không phải sửa code.

🇬🇧 English:

A HandlerMapping answers "who handles this request"; RequestMappingHandlerMapping builds its lookup table from @RequestMapping at startup. A HandlerAdapter answers "how do I invoke that handler", since a handler can be a HandlerMethod, an HttpRequestHandler or a HandlerFunction. Separating them lets the DispatcherServlet support many handler types without changes.

Q3. Controller trả về object thì ra JSON nhờ đâu?

🇻🇳 Tiếng Việt:

Nhờ @ResponseBody. RequestResponseBodyMethodProcessor xử lý giá trị trả về, chọn HttpMessageConverter phù hợp theo header Accept và kiểu dữ liệu, thường là Jackson, rồi ghi thẳng vào response body. Khi đó không có ViewResolver tham gia. Không có @ResponseBody thì String được hiểu là tên view.

🇬🇧 English:

Thanks to @ResponseBody. The RequestResponseBodyMethodProcessor handles the return value, picks an HttpMessageConverter based on the Accept header and the type, usually Jackson, and writes directly to the response body. No ViewResolver is involved. Without @ResponseBody, a String is treated as a view name.

Cách nhớ ngắn gọn


Tự kiểm tra

Bảng mapping URL → method được dựng lúc nào?

Lúc khởi động, khi RequestMappingHandlerMapping quét các bean @Controller.

Hai method cùng GET /api/orders/{id} thì sao?

Ứng dụng không khởi động được: lỗi Ambiguous mapping.

Muốn inject CustomerRef vào tham số controller từ header, implement interface nào?

HandlerMethodArgumentResolver, đăng ký qua WebMvcConfigurer.addArgumentResolvers.

Thêm @EnableWebMvc vào app Spring Boot gây ra gì?

Tắt auto-config Spring MVC của Boot (static resources, converter mặc định...).

REST controller có dùng ViewResolver không?

Không. Giá trị trả về được HttpMessageConverter ghi thẳng vào body.