Spring Boot/DispatcherServlet — front controller pattern và vòng đời 1 HTTP request
~26 phútREST API với Spring MVCMiễn phí

DispatcherServlet — front controller pattern và vòng đời 1 HTTP request

DispatcherServlet là entry point của mọi request Spring MVC. Bài này bóc front controller pattern, 9 bước HandlerMapping → HandlerAdapter → Controller → ViewResolver, các bean infra (HandlerMapping, HandlerAdapter, MessageConverter, HandlerExceptionResolver), và cách Boot autoconfig setup tất cả.

Module 02 đã chỉ ra Boot tự register DispatcherServlet qua WebMvcAutoConfiguration. Module 03 này bóc tầng tiếp theo: DispatcherServlet làm gì cụ thể khi nhận request? Vì sao có pattern "front controller"? 9 component infrastructure (HandlerMapping, HandlerAdapter, MessageConverter, ViewResolver, LocaleResolver, ...) phối hợp ra sao để biến HTTP request thành JSON response?

Bài này không dạy @RestController (bài 02 sẽ làm). Bài này trả lời câu hỏi sâu hơn: đằng sau @RestController, infrastructure nào quản request? Hiểu rồi, mọi annotation MVC ở các bài sau hiện ra logic.

1. Front Controller pattern — design pattern nền tảng

Trước khi có Spring MVC, web app Java EE truyền thống dùng page-controller pattern:

GET /orders → OrderListServlet.doGet()
GET /orders/42 → OrderDetailServlet.doGet()
POST /orders → OrderCreateServlet.doPost()

Mỗi URL = 1 servlet. web.xml mapping URL → servlet class. Pain:

  • Cross-cutting concern lặp lại: mỗi servlet phải tự handle auth check, logging, error handling, JSON parse.
  • Boilerplate: khai báo 100 servlet trong web.xml.
  • Khó test: mỗi servlet bind vào servlet container.

Front Controller pattern (Martin Fowler, "Patterns of Enterprise Application Architecture", 2002):

flowchart LR
    R[HTTP Request]
    FC["Front Controller<br/>(1 servlet duy nhat)"]
    H1[Handler 1]
    H2[Handler 2]
    H3[Handler 3]
    Res[HTTP Response]

    R --> FC
    FC --> H1
    FC --> H2
    FC --> H3
    H1 --> Res
    H2 --> Res
    H3 --> Res

    style FC fill:#fef3c7

1 servlet (Front Controller) nhận mọi request, dispatch đến handler phù hợp. Handler là method trong controller class — không phải servlet.

Lợi ích:

  • Cross-cutting tập trung 1 chỗ (Front Controller chạy auth, log, error).
  • Handler đơn giản — chỉ business logic, không servlet API.
  • 1 file XML config thay 100 servlet declaration.

Spring MVC implement pattern này — DispatcherServlet là Front Controller.

2. DispatcherServlet — chính xác làm gì

DispatcherServlet extends HttpServlet. Nó được register làm servlet trong app server (Tomcat/Jetty), bind URL pattern /* (catch-all). Mọi request đến app đều qua nó:

sequenceDiagram
    participant Client
    participant Tomcat
    participant DS as DispatcherServlet
    participant HM as HandlerMapping
    participant HA as HandlerAdapter
    participant Ctrl as @RestController
    participant MC as MessageConverter

    Client->>Tomcat: GET /api/orders/42
    Tomcat->>DS: HttpServletRequest
    DS->>HM: getHandler(request)
    HM-->>DS: HandlerExecutionChain<br/>(OrderController.getOrder method + interceptors)
    DS->>HA: getHandlerAdapter(handler)
    HA-->>DS: RequestMappingHandlerAdapter
    DS->>HA: handle(request, handler)
    HA->>MC: read body / params
    HA->>Ctrl: getOrder(42L)
    Ctrl-->>HA: ResponseEntity<OrderDto>
    HA->>MC: write JSON
    HA-->>DS: ModelAndView (null cho REST)
    DS->>Client: HTTP response with JSON

9 bước cụ thể:

  1. Tomcat receive request, dispatch đến DispatcherServlet.doDispatch(req, res).
  2. DispatcherServlet invoke HandlerMapping.getHandler(request) — tìm method match URL.
  3. Trả về HandlerExecutionChain — gồm handler method + list interceptor.
  4. getHandlerAdapter(handler) — chọn adapter tương thích handler.
  5. Run pre-handle interceptors (authentication, log).
  6. Adapter invoke handler — gọi method controller, bind parameter từ request.
  7. Controller return Object / ResponseEntity.
  8. Adapter convert return value thành response (qua MessageConverter cho JSON).
  9. Run post-handle interceptors + write response.

Xảy ra 200ms tổng cho 1 request — invisible với dev. Hiểu rõ giúp debug khi 1 request "không đến controller" — bug ở bước 2-5, không phải logic.

3. 9 bean infrastructure của Spring MVC

DispatcherServlet không tự làm hết — nó delegate cho 9 loại bean:

BeanVai tròDefault impl (Boot)
HandlerMappingURL → handler methodRequestMappingHandlerMapping
HandlerAdapterInvoke handler đúng cáchRequestMappingHandlerAdapter
HandlerInterceptorCross-cutting trước/sau handler(custom)
HttpMessageConverterConvert body ↔ objectMappingJackson2HttpMessageConverter
LocaleResolverDetect locale từ requestAcceptHeaderLocaleResolver
ThemeResolverTheme cho UI (legacy)FixedThemeResolver
ViewResolverRender view (Thymeleaf, JSP)BeanNameViewResolver
MultipartResolverParse multipart/form-dataStandardServletMultipartResolver
HandlerExceptionResolverHandle exception từ controllerResponseStatusExceptionResolver + DefaultHandlerExceptionResolver

Boot WebMvcAutoConfiguration tự register tất cả default. Bạn override bằng cách register bean cùng type (Module 02 bài 03 — @ConditionalOnMissingBean pattern).

3.1 HandlerMapping — URL routing

RequestMappingHandlerMapping đọc @RequestMapping/@GetMapping/@PostMapping trên controller class + method, build map:

GET /api/orders/{id}    → OrderController.getOrder(Long)
GET /api/orders          → OrderController.listOrders()
POST /api/orders         → OrderController.createOrder(OrderRequest)

Khi request đến, mapper match URL pattern + HTTP method → return handler.

Nếu không match → throw NoHandlerFoundException (Boot 3+ default trả 404). Set spring.mvc.throw-exception-if-no-handler-found=true để custom error response.

3.2 HandlerAdapter — invoke handler đúng cách

Spring có nhiều loại handler (controller method, Servlet, HttpRequestHandler, ...). Adapter pattern cho phép DispatcherServlet không cần biết loại handler — nó hỏi adapter "supports?", adapter biết invoke ra sao.

RequestMappingHandlerAdapter xử lý method @RequestMapping. Logic:

  1. Resolve method argument từ request (@PathVariable, @RequestParam, @RequestBody).
  2. Invoke method.
  3. Convert return value (@ResponseBody, ResponseEntity).
  4. Trả ra response.

3.3 HttpMessageConverter — body ↔ object

Convert giữa HTTP body và Java object:

  • Read: @RequestBody UserDto user → MessageConverter parse JSON body → UserDto instance.
  • Write: return userDto → MessageConverter serialize → JSON body.

Boot register defaults theo classpath:

ConverterFormatPull theo starter
MappingJackson2HttpMessageConverterJSONstarter-web (mặc định)
MappingJackson2XmlHttpMessageConverterXMLstarter-jackson-dataformat-xml
StringHttpMessageConvertertext/plaincore
ByteArrayHttpMessageConverterbyte[]core
FormHttpMessageConverterapplication/x-www-form-urlencodedcore
ResourceHttpMessageConverterfile downloadcore

Content negotiation: client gửi Accept: application/json → Spring chọn JSON converter. Accept: application/xml → XML converter (nếu có jackson-xml).

3.4 HandlerInterceptor — cross-cutting

Chạy trước/sau handler:

@Component
public class TimingInterceptor implements HandlerInterceptor {

    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
        req.setAttribute("startTime", System.currentTimeMillis());
        return true;
    }

    public void afterCompletion(HttpServletRequest req, HttpServletResponse res, Object handler, Exception ex) {
        long elapsed = System.currentTimeMillis() - (long) req.getAttribute("startTime");
        log.info("{} {} took {}ms", req.getMethod(), req.getRequestURI(), elapsed);
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired private TimingInterceptor timingInterceptor;

    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(timingInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/health");
    }
}

HandlerInterceptor khác Filter:

  • Filter: servlet-level, chạy trước DispatcherServlet. Không biết handler nào sẽ chạy.
  • Interceptor: Spring MVC level, chạy sau routing — biết handler method, có thể access bean.

Use case:

  • Auth check phải Filter (chạy sớm, có thể short-circuit).
  • Timing/audit có thể Interceptor (cần biết handler).

3.5 HandlerExceptionResolver — error handling

Khi controller throw exception:

  1. DispatcherServlet catch.
  2. Iterate HandlerExceptionResolver list theo @Order.
  3. Resolver trả ModelAndView (response) hoặc null (skip).
  4. Nếu không resolver nào handle → 500 default error page.

Boot register 3 resolver default:

ResolverHandle
ExceptionHandlerExceptionResolverMethod @ExceptionHandler trong @Controller / @ControllerAdvice
ResponseStatusExceptionResolverException annotated @ResponseStatus
DefaultHandlerExceptionResolverSpring MVC built-in exception (HttpMessageNotReadableException, MethodArgumentNotValidException, ...)

Bài 05 sẽ đào sâu @ExceptionHandler + Problem Details.

4. Cách Boot setup DispatcherServlet tự động

DispatcherServletAutoConfiguration register:

@AutoConfiguration(after = ServletWebServerFactoryAutoConfiguration.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
        DispatcherServlet ds = new DispatcherServlet();
        ds.setDispatchOptionsRequest(webMvcProperties.isDispatchOptionsRequest());
        ds.setDispatchTraceRequest(webMvcProperties.isDispatchTraceRequest());
        // ... 8 setting khac
        return ds;
    }

    @Bean
    @ConditionalOnMissingBean
    public DispatcherServletRegistrationBean dispatcherServletRegistration(...) {
        // Register DispatcherServlet voi servlet container (Tomcat)
        // URL pattern: "/" (root, catch-all)
    }
}

WebMvcAutoConfiguration register 9 bean infrastructure:

@AutoConfiguration(after = {DispatcherServletAutoConfiguration.class, ...})
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
public class WebMvcAutoConfiguration {

    @Bean public RequestMappingHandlerMapping requestMappingHandlerMapping(...) { ... }
    @Bean public RequestMappingHandlerAdapter requestMappingHandlerAdapter(...) { ... }
    @Bean public HandlerExceptionResolver handlerExceptionResolver(...) { ... }
    // ... 6 bean khac
}

User customize qua WebMvcConfigurer:

@Configuration
public class WebConfig implements WebMvcConfigurer {

    public void addInterceptors(InterceptorRegistry registry) { ... }
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) { ... }
    public void addCorsMappings(CorsRegistry registry) { ... }
    public void addFormatters(FormatterRegistry registry) { ... }
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) { ... }
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) { ... }
    // ... 13 method khac
}

WebMvcConfigurer cho phép contribute config không replace default. Boot pick up bean implement interface, gọi mỗi method để collect customization.

5. URL routing — pattern matching

RequestMappingHandlerMapping match URL theo path pattern. Spring MVC support AntPathMatcher (legacy) và PathPatternParser (Spring 5.3+, default từ Spring 6).

5.1 PathPattern syntax

@GetMapping("/orders")                    // exact
@GetMapping("/orders/{id}")               // path variable
@GetMapping("/orders/{id:\\d+}")          // path variable + regex constraint (chi number)
@GetMapping("/orders/{*path}")            // capture remaining path
@GetMapping("/api/v?")                    // single char wildcard (v1, v2, ...)
@GetMapping("/api/*/orders")              // single segment wildcard
@GetMapping("/files/**")                  // multi-segment wildcard

Multiple pattern:

@GetMapping({"/orders", "/api/orders"})     // 2 URL cung handler

5.2 Method matching

@GetMapping              // GET only
@PostMapping             // POST only
@RequestMapping(method = {RequestMethod.GET, RequestMethod.HEAD})    // multiple

5.3 Producing/Consuming

@PostMapping(value = "/orders",
             consumes = MediaType.APPLICATION_JSON_VALUE,
             produces = MediaType.APPLICATION_JSON_VALUE)
public OrderDto create(@RequestBody OrderRequest req) { ... }

consumes: chỉ accept request có Content-Type: application/json. Khác → 415 Unsupported Media Type. produces: chỉ trả application/json. Client Accept: application/xml → 406 Not Acceptable.

5.4 Headers / Params matching

@GetMapping(value = "/orders", headers = "X-API-Version=v2")
public List<OrderDto> listV2() { ... }

@GetMapping(value = "/orders", params = "format=summary")
public List<OrderSummary> listSummary() { ... }

Match request có header/param cụ thể. Hữu ích cho API versioning hoặc filter mode.

6. Pattern infrastructure thực tế

6.1 Servlet 3.1 async + Virtual Threads

Từ Boot 3.2 + Java 21:

spring:
  threads:
    virtual:
      enabled: true

DispatcherServlet chạy mỗi request trên virtual thread thay platform thread. App I/O-bound (gọi DB, REST client) scale từ ~200 concurrent → ~10,000+ concurrent với cùng heap.

6.2 Reactive vs Servlet

DispatcherServlet thuộc servlet stack (blocking). Spring có DispatcherHandler cho reactive stack (WebFlux):

flowchart TB
    Servlet["spring-webmvc<br/>(servlet stack)"]
    DS["DispatcherServlet<br/>(extends HttpServlet)"]
    Tomcat["Tomcat / Jetty / Undertow"]

    Reactive["spring-webflux<br/>(reactive stack)"]
    DH["DispatcherHandler<br/>(implements WebHandler)"]
    Netty["Netty / Servlet 3.1+ async"]

    Servlet --> DS --> Tomcat
    Reactive --> DH --> Netty

API tương đương — @RestController, @RequestMapping work trong cả 2 — nhưng implementation hoàn toàn khác. WebFlux là Module 10. Bài này chỉ MVC.

6.3 Multiple DispatcherServlet

Hiếm — nhưng có thể register nhiều DispatcherServlet cho path khác nhau:

@Bean
public ServletRegistrationBean<DispatcherServlet> apiV1Servlet() {
    DispatcherServlet ds = new DispatcherServlet();
    ds.setContextConfigLocation("classpath:api-v1-context.xml");
    return new ServletRegistrationBean<>(ds, "/api/v1/*");
}

Use case: API versioning với context riêng. Phức tạp — đa phần dùng path pattern thay.

7. Vận hành production — thread sizing, slow request runbook, request limits

DispatcherServlet là entry point — mọi request đều qua. Sizing thread pool sai → throughput suy giảm. Slow request không alert → user impact âm thầm. Section này cover sizing, limits, runbook diagnose slow request.

7.1 Tomcat thread pool sizing — 3 limit chồng lớp

server:
  tomcat:
    threads:
      max: 200                    # max worker thread
      min-spare: 10               # min idle thread
    accept-count: 100             # queue size when threads busy
    max-connections: 8192         # OS-level connection
    connection-timeout: 20000     # 20s idle close

3 limit:

  1. max-connections — OS accept tối đa bao nhiêu socket. Vượt → kernel reject (TCP RST).
  2. max-threads — bao nhiêu thread xử lý đồng thời.
  3. accept-count — queue khi max-threads busy. Vượt → 503.

Math:

  • App I/O bound (gọi DB, REST client): thread vượt CPU core nhiều lần OK (vì block I/O thread idle).
  • App CPU bound (compute): thread = CPU core nhân 1-2.
  • Default 200 thread phù hợp 90% case.

Sizing thực tế:

max_threads = (target_concurrent_requests) / (1 - blocking_io_ratio)

Vd: 100 concurrent request, 80% time block I/O → max_threads = 100 / 0.2 = 500. Tăng lên 500 nếu cần concurrency cao.

Hoặc dễ hơn: switch sang virtual threads (section 7.2).

7.2 Virtual threads (Java 21+) — game changer

Boot 3.2+ + Java 21 enable:

spring:
  threads:
    virtual:
      enabled: true

Behavior:

  • Mỗi request mount virtual thread thay platform thread.
  • Virtual thread cheap (khoảng 100 byte stack vs 1MB platform thread).
  • I/O block → virtual thread unmount, platform thread reuse.

Scale:

  • Platform thread: khoảng 200 concurrent (bounded by thread pool).
  • Virtual thread: 10,000+ concurrent (bounded by RAM).

Caveat — pinning:

  • synchronized block "pin" virtual thread → block platform carrier. Replace với ReentrantLock.
  • ThreadLocal vẫn work nhưng tốn memory nếu nhiều virtual thread cùng dùng.
  • Native call (JNI) pin.

Test pinning trong dev:

java -Djdk.tracePinnedThreads=full -jar app.jar

Output show stack trace mỗi pin event. Audit pre-prod xác định không có hot pin path.

7.3 Request limits — DoS protection

Tomcat-level limits (đặt baseline):

server:
  max-http-request-header-size: 8KB        # default
  tomcat:
    max-swallow-size: 2MB                  # max body Spring read fail-fast
    max-http-form-post-size: 10MB          # form upload
    max-parameter-count: 1000              # avoid hash flood DoS

Thêm rate limiter (Bucket4j) cho per-client throttle:

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RateLimitFilter extends OncePerRequestFilter {

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();

    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        String clientId = req.getRemoteAddr();      // hoac JWT user id
        Bucket bucket = buckets.computeIfAbsent(clientId, k ->
            Bucket.builder()
                .addLimit(Bandwidth.simple(100, Duration.ofMinutes(1)))
                .build());

        if (!bucket.tryConsume(1)) {
            res.setStatus(429);
            res.setHeader("Retry-After", "60");
            return;
        }
        chain.doFilter(req, res);
    }
}

Production: dùng centralized rate limiter (Redis-backed, vd bucket4j-redis) cho multi-pod deployment — limit theo client thực sự, không per-pod.

7.4 Slow request runbook — diagnose chronological

Triệu chứng: P99 endpoint tăng đột ngột (alert SLO).

Diagnose thứ tự:

  1. /actuator/metrics/http.server.requests — break down by uri, status, outcome. Identify endpoint/status combination chậm.
  2. APM trace (Tempo/Jaeger) — pick slow trace, breakdown span. Span lâu nhất = bottleneck:
    • DB query span lâu → check Hikari + Postgres slow query log.
    • External HTTP span lâu → check downstream service health.
    • GC pause span lâu → check JVM GC log.
  3. Thread dump (/actuator/threaddump) — nếu trace không đủ:
    • Block synchronized → hot lock contention.
    • Block JDBC getConnection → pool exhausted (Module 04 bài 05).
    • Block external API → downstream slow.
  4. Heap dump (/actuator/heapdump) — memory leak suspect.

Action tree:

CauseAction
DB slow queryCheck pg_stat_statements, add index, refactor query
Connection pool exhaustedTăng pool, refactor long tx, add PgBouncer
External API slowEnable circuit breaker (Resilience4j), add timeout
GC pauseTune heap size, switch ZGC/Generational
Lock contentionRefactor synchronized → ReentrantLock / lock-free
Virtual thread pinningReplace synchronized hot path, audit JNI

7.5 Health check — readiness vs liveness

K8s probe pattern:

livenessProbe:
  httpGet:
    path: /actuator/health/liveness
    port: 9090                     # management port internal
  initialDelaySeconds: 30
  periodSeconds: 10
  failureThreshold: 3              # 30s consecutive fail → restart pod

readinessProbe:
  httpGet:
    path: /actuator/health/readiness
    port: 9090
  initialDelaySeconds: 5
  periodSeconds: 5
  failureThreshold: 3              # 15s fail → remove from service endpoints
ProbeKhi failK8s action
LivenessApp stuck (deadlock, OOM-near)Restart pod
ReadinessTạm không ready (cache warming, DB reconnect)Remove from LB, không restart

Spring Boot 3 mặc định 2 probe split. Custom indicator:

@Component
public class ExternalApiHealthIndicator implements HealthIndicator {
    public Health health() {
        try {
            externalApi.ping();
            return Health.up().build();
        } catch (Exception e) {
            return Health.down().withException(e).build();
        }
    }
}

Cảnh báo: liveness probe không check downstream — cascade fail. Vd nếu liveness check DB và DB tạm down → mọi pod restart loop → app không bao giờ heal. Liveness chỉ check JVM alive. Readiness mới check downstream.

7.6 Graceful shutdown với in-flight request

server:
  shutdown: graceful
spring:
  lifecycle:
    timeout-per-shutdown-phase: 30s

Behavior K8s rolling update:

  1. SIGTERM nhận → readiness probe fail → K8s stop route traffic mới (15-30s grace).
  2. In-flight request complete trong grace timeout (30s).
  3. Pod terminate.

Pre-condition: P99 request duration phải dưới grace timeout. Nếu request lâu (vd long-poll, SSE) → tăng grace hoặc tách endpoint riêng.

7.7 DispatcherServlet hot path — observation

Boot 3 + Micrometer Observation API tự instrument mọi controller method. Custom observation:

@RestController
public class OrderController {

    @Observed(name = "orders.list", contextualName = "list-orders")
    @GetMapping("/orders")
    public List<OrderDto> list() { ... }
}

Generate:

  • Metric orders.list.active — concurrent execution.
  • Span trace với name list-orders.
  • Log với observation context.

Dashboard standard:

  • Request rate per endpoint.
  • P50/P95/P99 latency.
  • Error rate split by status class.
  • Active concurrent request.

8. Pitfall tổng hợp

Nhầm 1: Tin @RestController là magic. ✅ Đó chỉ là @Controller + @ResponseBody. Logic infra: RequestMappingHandlerMapping match URL → RequestMappingHandlerAdapter invoke method → HttpMessageConverter serialize JSON. Không magic.

Nhầm 2: Confuse Filter và Interceptor. ✅ Filter chạy trước DispatcherServlet (servlet-level). Interceptor chạy sau routing (Spring MVC-level, biết handler).

Nhầm 3: Override WebMvcAutoConfiguration bằng @EnableWebMvc.

@Configuration
@EnableWebMvc       // tat HET WebMvcAutoConfiguration
public class MyConfig { ... }

@EnableWebMvc disable Boot autoconfig — bạn phải config từ đầu. Chỉ dùng khi thực sự cần. Customize qua WebMvcConfigurer interface — giữ Boot defaults.

Nhầm 4: Method @RequestMapping không trả response.

@GetMapping("/process")
public void process() {
    // do stuff, but no return -> 200 with empty body
}

✅ Trả void cho operation không có data ra (POST với 201 Created). Dùng ResponseEntity<Void> nếu cần custom status/header.

Nhầm 5: Dùng path variable không escape.

@GetMapping("/files/{path}")
public Resource get(@PathVariable String path) {
    return resourceLoader.getResource("file:" + path);   // security risk
}

✅ Validate path. Use {*path} cho multi-segment. Spring tự decode %2F (/), %5C () — vẫn cần check absolute path traversal.

Nhầm 6: Confuse produces với Accept header parser. ✅ produces filter handler match. Nếu controller trả type khác (vd String), produces không convert — nó chỉ filter.

9. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Spring Framework Reference:

Spring Boot:

Pattern reference:

Source:

Tool:

  • /actuator/mappings — runtime list mọi route mapped.
  • IntelliJ "Endpoints" tool window — visualize tất cả @RequestMapping.
  • --debug flag — log chi tiết MVC autoconfig.

Ghi chú: đọc DispatcherServlet.doDispatch() source code 1 lần — 80 dòng nhưng cô đọng toàn bộ flow. Sau đó mọi annotation MVC sẽ click.

10. Tóm tắt

  • Front Controller pattern: 1 servlet duy nhất nhận mọi request, dispatch handler. Spring MVC implement qua DispatcherServlet.
  • 9 bước flow: Tomcat → DispatcherServlet → HandlerMapping (URL → handler) → HandlerAdapter (invoke) → Controller method → MessageConverter (body ↔ JSON) → response.
  • 9 bean infrastructure: HandlerMapping, HandlerAdapter, HandlerInterceptor, HttpMessageConverter, LocaleResolver, ThemeResolver, ViewResolver, MultipartResolver, HandlerExceptionResolver.
  • Boot autoconfig (DispatcherServletAutoConfiguration + WebMvcAutoConfiguration) register tất cả default. Override qua @ConditionalOnMissingBean pattern hoặc WebMvcConfigurer interface.
  • MessageConverter map giữa HTTP body và Java object. Default Jackson cho JSON. Content negotiation qua Accept header.
  • Filter ≠ Interceptor: Filter chạy trước DispatcherServlet (servlet-level), Interceptor chạy sau routing (MVC-level, biết handler).
  • 3 HandlerExceptionResolver default: ExceptionHandlerExceptionResolver (method @ExceptionHandler), ResponseStatusExceptionResolver (exception @ResponseStatus), DefaultHandlerExceptionResolver (built-in MVC exception).
  • URL routing qua PathPatternParser (Spring 6+) hoặc AntPathMatcher (legacy). Support exact, wildcard *, **, regex \d+, capture remaining {*path}.
  • @EnableWebMvc disable Boot autoconfig. Customize qua WebMvcConfigurer interface — giữ defaults.
  • Boot 3.2+ + Java 21: spring.threads.virtual.enabled=true → mỗi request trên virtual thread, scale 50x.

11. Tự kiểm tra

Tự kiểm tra
Q1
Vì sao Spring MVC dùng Front Controller pattern thay Page Controller (1 servlet / URL)? Lợi ích cụ thể là gì?

Vấn đề Page Controller (Java EE truyền thống):

  • Cross-cutting lặp lại: mỗi servlet phải tự code auth check, log, error handling, JSON parse. 100 servlet = 100 chỗ duplicate.
  • Boilerplate config: web.xml khai báo từng servlet mapping URL — 100 entry cho 100 endpoint.
  • Khó test: servlet bind cứng vào servlet container — phải start Tomcat để test.

Lợi ích Front Controller (Spring MVC):

  • 1 servlet duy nhất: DispatcherServlet handle mọi URL. Auth/log/error tập trung vào filter + interceptor + exception resolver — chạy 1 lần cho mọi request.
  • Handler là method, không phải servlet: OrderController.getOrder() chỉ là method Java — không extends servlet, không bind container.
  • URL mapping qua annotation: @GetMapping("/orders/{id}") — declarative, không XML.
  • Test dễ: MockMvc mock DispatcherServlet, test handler không cần Tomcat.
  • Add endpoint mới: tạo class controller + annotation — không sửa web.xml.

Pattern này không phải đặc thù Spring — Struts 2, JAX-RS, Express.js (Node), Flask (Python) đều dùng Front Controller. Đó là "right way" cho web framework hiện đại.

Q2
Filter và Interceptor đều intercept request — khi nào nên dùng cái nào? Cho 2 ví dụ cụ thể.
AspectFilterHandlerInterceptor
MứcServlet API (chuẩn Java EE)Spring MVC (Spring-specific)
Chạy khiTrước DispatcherServletSau routing, trước handler
Biết handlerKhôngCó (handler method, controller class)
Modify request/responseCó (wrap)Có nhưng ít chuẩn
Short-circuitDễ — không call chain.doFilter()Trả false từ preHandle
Spring beansPhải register FilterRegistrationBean@Component luôn

Ví dụ Filter — Authentication:

@Component
public class JwtAuthFilter extends OncePerRequestFilter {
  protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
      String token = req.getHeader("Authorization");
      if (token == null) {
          res.setStatus(401);
          return;          // short-circuit — khong cho qua DispatcherServlet
      }
      Authentication auth = jwtService.validate(token);
      SecurityContextHolder.getContext().setAuthentication(auth);
      chain.doFilter(req, res);
  }
}

Filter phù hợp: chạy sớm, có thể reject request trước khi chạm controller. Spring Security dùng filter chain.

Ví dụ Interceptor — Audit log với handler info:

@Component
public class AuditInterceptor implements HandlerInterceptor {
  public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
      if (handler instanceof HandlerMethod hm) {
          String method = hm.getMethod().getName();
          String controller = hm.getBeanType().getSimpleName();
          log.info("Audit: {} called {}.{}", req.getRemoteUser(), controller, method);
      }
      return true;
  }
}

Interceptor phù hợp: cần biết controller method nào sẽ chạy (audit, metrics theo handler).

Quy tắc: auth/CORS/security → Filter. Audit/metrics/feature flag check theo handler → Interceptor.

Q3
Đoạn sau có gì sai? Boot sẽ làm gì?
@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new TimingInterceptor());
  }
}

Vấn đề: @EnableWebMvc annotation disable Boot's WebMvcAutoConfiguration — bạn mất tất cả default Boot setup.

Cụ thể:

  • Mất Jackson auto-register: MappingJackson2HttpMessageConverter không tự register — JSON serialize fail.
  • Mất static resource handler: file trong /static/ không serve.
  • Mất content negotiation default: Accept header parsing không work standard.
  • Mất Boot tinh chỉnh: error page mapping, multipart, URL pattern parser default.

Lý do nguy hiểm: code compile + start ok. Bug chỉ lộ khi test endpoint — JSON return empty, static file 404. Hard to debug.

Cách đúng — bỏ @EnableWebMvc:

@Configuration
public class WebConfig implements WebMvcConfigurer {           // KHONG @EnableWebMvc
  public void addInterceptors(InterceptorRegistry registry) {
      registry.addInterceptor(new TimingInterceptor());
  }
}

Boot pick up bean implement WebMvcConfigurer tự động qua DelegatingWebMvcConfiguration. Mọi method bạn override merge với default — không replace.

Khi nào DÙNG @EnableWebMvc: chỉ khi bạn thực sự muốn config từ đầu, không dùng Boot default. Hiếm — 99% case dùng WebMvcConfigurer interface đủ.

Bài học: Boot autoconfig opinionated — đừng disable trừ khi có lý do specific. Override qua interface cho contribute customization, không replace.

Q4
Request đến POST /api/orders với Content-Type: application/xml body XML. App có Jackson JSON converter (default Boot) — không có XML converter. Điều gì xảy ra?

Spring trả 415 Unsupported Media Type.

Lý do — flow:

  1. RequestMappingHandlerMapping match URL → tìm handler OrderController.create() với @PostMapping("/orders").
  2. Method có @RequestBody OrderRequest req — Spring cần HttpMessageConverter đọc body.
  3. Spring iterate converter list, hỏi "canRead(OrderRequest.class, application/xml)?".
    • MappingJackson2HttpMessageConverter support JSON only → false.
    • Không converter nào support XML → fail.
  4. Spring throw HttpMediaTypeNotSupportedException.
  5. DefaultHandlerExceptionResolver map exception → 415 status code.

Fix tùy yêu cầu:

  • Add XML support:
    <dependency>
      <groupId>com.fasterxml.jackson.dataformat</groupId>
      <artifactId>jackson-dataformat-xml</artifactId>
    </dependency>
    Boot autoconfig add MappingJackson2XmlHttpMessageConverter. Cùng controller serve cả JSON + XML qua content negotiation.
  • Hoặc reject XML rõ ràng:
    @PostMapping(value = "/orders", consumes = MediaType.APPLICATION_JSON_VALUE)
    public OrderDto create(@RequestBody OrderRequest req) { ... }
    consumes filter handler — request XML → 415 sớm hơn, log clearer.

Insight: 415 không phải bug — Spring đúng. Spring chỉ convert format có converter. 99% Spring app modern dùng JSON only → không pull XML lib → behavior đúng.

Q5
Có 3 controller method match URL GET /api/orders/42:
@GetMapping("/api/orders/{id}")                                  // 1
@GetMapping(value = "/api/orders/{id}", headers = "X-Version=2") // 2
@GetMapping(value = "/api/orders/{id:\\d+}", produces = "application/json") // 3
Request có Accept: application/json + X-Version: 2. Spring chọn handler nào?

Spring chọn handler bằng most specific match. Quy tắc tie-breaking từ docs:

  1. Match path pattern — cả 3 đều match /api/orders/42 (pattern 3 thêm regex constraint nhưng 42 match \d+).
  2. Match HTTP method — cả 3 đều GET → tie.
  3. Match consume + produces — handler 3 require produces=application/json, request Accept: application/json → match. Handler 1, 2 không khai produces → match any.
  4. Match params + headers — handler 2 require X-Version=2, request có → match. Handler 1, 3 không khai → match any.

Score from "most specific":

HandlerPath constraintHeadersProducesTotal specificity
1basic--0
2basicX-Version=2-1
3regex-application/json2 (regex + produces)

Spring chọn handler 3 — most specific (path regex + produces explicit).

Caveat: nếu specificity tie, Spring throw IllegalStateException("Ambiguous handler methods") tại startup — buộc dev clean up.

Best practice: đừng define multiple handler cùng URL trừ khi thực sự cần version A/B test. Dễ nhầm lẫn debug. Versioning tốt hơn:

  • Path versioning: /api/v1/orders/{id} vs /api/v2/orders/{id}.
  • Subdomain: v1.api.olhub.org.
  • Accept header version: Accept: application/vnd.olhub.v2+json + matrix dispatch trong controller.
Q6
Bạn nhận log "Tomcat initialized with port 8080" rất nhanh nhưng request đến controller mất 5 giây. Suspect bước nào trong 9-step flow của DispatcherServlet? Cách diagnose?

Suspect bước 5-8: Interceptor + Handler invocation + MessageConverter.

Phân tích logic — request đến port 8080 nhanh = Tomcat OK. Chậm khi vào DispatcherServlet flow:

  1. Bước 2 (HandlerMapping): URL routing là O(N) qua N route. App 1000 endpoint → vài ms. Hiếm chậm 5s.
  2. Bước 5 (preHandle Interceptors): nếu interceptor gọi DB/external service → bottleneck. Common: auth interceptor query user table mỗi request, không cache.
  3. Bước 6 (Handler invocation): business logic trong controller/service. Slow query, slow external API call → 5s realistic.
  4. Bước 7 (MessageConverter write): serialize huge response (10MB JSON) → vài hundred ms. Không 5s thường.
  5. Bước 8 (postHandle Interceptors): tương tự bước 5.

Quy trình diagnose:

  1. Enable timing log: logging.level.org.springframework.web=DEBUG — show URL match, handler invoke, time.
  2. Add Filter timing đo phase:
    long start = System.currentTimeMillis();
    chain.doFilter(req, res);
    long total = System.currentTimeMillis() - start;
    log.info("{} {} took {}ms", req.getMethod(), req.getRequestURI(), total);
    So với log Spring DEBUG → tìm phase chiếm phần lớn time.
  3. Inspect interceptor: bật /actuator/mappings xem interceptor nào active. Mỗi interceptor add log preHandle/afterCompletion với timestamp.
  4. Profile JVM: dùng JFR (Java Flight Recorder) hoặc async-profiler để thấy stack trace nóng.
  5. Check downstream: nếu controller call DB → bật org.hibernate.SQL=DEBUG, count query + duration. N+1 hay slow query phổ biến.
  6. Check Virtual Threads: nếu Boot 3.2+ + Java 21 + virtual thread enabled, blocking sync chain (synchronized block) có thể "pin" virtual thread.

Tool 2026: Spring Boot Actuator /actuator/httptrace (deprecate) → dùng /actuator/metrics + Micrometer Tracing với OTLP → Jaeger UI thấy span breakdown từng bước.

Bài tiếp theo: @RestController, @RequestMapping, HTTP method annotations

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

Bình luận (0)

Đang tải...