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:#fef3c71 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 JSON9 bước cụ thể:
- Tomcat receive request, dispatch đến
DispatcherServlet.doDispatch(req, res). DispatcherServletinvokeHandlerMapping.getHandler(request)— tìm method match URL.- Trả về
HandlerExecutionChain— gồm handler method + list interceptor. getHandlerAdapter(handler)— chọn adapter tương thích handler.- Run pre-handle interceptors (authentication, log).
- Adapter invoke handler — gọi method controller, bind parameter từ request.
- Controller return
Object/ResponseEntity. - Adapter convert return value thành response (qua
MessageConvertercho JSON). - 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:
| Bean | Vai trò | Default impl (Boot) |
|---|---|---|
HandlerMapping | URL → handler method | RequestMappingHandlerMapping |
HandlerAdapter | Invoke handler đúng cách | RequestMappingHandlerAdapter |
HandlerInterceptor | Cross-cutting trước/sau handler | (custom) |
HttpMessageConverter | Convert body ↔ object | MappingJackson2HttpMessageConverter |
LocaleResolver | Detect locale từ request | AcceptHeaderLocaleResolver |
ThemeResolver | Theme cho UI (legacy) | FixedThemeResolver |
ViewResolver | Render view (Thymeleaf, JSP) | BeanNameViewResolver |
MultipartResolver | Parse multipart/form-data | StandardServletMultipartResolver |
HandlerExceptionResolver | Handle exception từ controller | ResponseStatusExceptionResolver + 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:
- Resolve method argument từ request (
@PathVariable,@RequestParam,@RequestBody). - Invoke method.
- Convert return value (
@ResponseBody,ResponseEntity). - Trả ra response.
3.3 HttpMessageConverter — body ↔ object
Convert giữa HTTP body và Java object:
- Read:
@RequestBody UserDto user→ MessageConverter parse JSON body →UserDtoinstance. - Write:
return userDto→ MessageConverter serialize → JSON body.
Boot register defaults theo classpath:
| Converter | Format | Pull theo starter |
|---|---|---|
MappingJackson2HttpMessageConverter | JSON | starter-web (mặc định) |
MappingJackson2XmlHttpMessageConverter | XML | starter-jackson-dataformat-xml |
StringHttpMessageConverter | text/plain | core |
ByteArrayHttpMessageConverter | byte[] | core |
FormHttpMessageConverter | application/x-www-form-urlencoded | core |
ResourceHttpMessageConverter | file download | core |
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:
DispatcherServletcatch.- Iterate
HandlerExceptionResolverlist theo@Order. - Resolver trả
ModelAndView(response) hoặcnull(skip). - Nếu không resolver nào handle → 500 default error page.
Boot register 3 resolver default:
| Resolver | Handle |
|---|---|
ExceptionHandlerExceptionResolver | Method @ExceptionHandler trong @Controller / @ControllerAdvice |
ResponseStatusExceptionResolver | Exception annotated @ResponseStatus |
DefaultHandlerExceptionResolver | Spring 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 --> NettyAPI 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:
max-connections— OS accept tối đa bao nhiêu socket. Vượt → kernel reject (TCP RST).max-threads— bao nhiêu thread xử lý đồng thời.accept-count— queue khimax-threadsbusy. 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:
synchronizedblock "pin" virtual thread → block platform carrier. Replace vớiReentrantLock.- 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ự:
/actuator/metrics/http.server.requests— break down byuri,status,outcome. Identify endpoint/status combination chậm.- 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.
- 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.
- Block
- Heap dump (
/actuator/heapdump) — memory leak suspect.
Action tree:
| Cause | Action |
|---|---|
| DB slow query | Check pg_stat_statements, add index, refactor query |
| Connection pool exhausted | Tăng pool, refactor long tx, add PgBouncer |
| External API slow | Enable circuit breaker (Resilience4j), add timeout |
| GC pause | Tune heap size, switch ZGC/Generational |
| Lock contention | Refactor synchronized → ReentrantLock / lock-free |
| Virtual thread pinning | Replace 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
| Probe | Khi fail | K8s action |
|---|---|---|
| Liveness | App stuck (deadlock, OOM-near) | Restart pod |
| Readiness | Tạ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:
- SIGTERM nhận → readiness probe fail → K8s stop route traffic mới (15-30s grace).
- In-flight request complete trong grace timeout (30s).
- 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
Spring Framework Reference:
- Spring MVC Reference — DispatcherServlet — overview chính thức.
- Spring MVC Reference — Special Bean Types — 9 bean infrastructure.
- Spring MVC Reference — Mapping Requests —
@RequestMapping+ path pattern + match by header/param. - Spring MVC Reference — HandlerExceptionResolver — exception handling.
Spring Boot:
- Spring Boot Reference — Spring MVC Auto-configuration — autoconfig của MVC.
- Spring Boot Reference — Customize Spring MVC —
WebMvcConfigurer.
Pattern reference:
- Martin Fowler — Front Controller — pattern gốc 2002.
- PEAA Book — Patterns of Enterprise Application Architecture — Front Controller chương 14.
Source:
DispatcherServlet— đọc methoddoDispatch(~80 dòng) hiểu sâu 9 bước.RequestMappingHandlerMapping— URL routing engine.
Tool:
/actuator/mappings— runtime list mọi route mapped.- IntelliJ "Endpoints" tool window — visualize tất cả
@RequestMapping. --debugflag — 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@ConditionalOnMissingBeanpattern hoặcWebMvcConfigurerinterface. MessageConvertermap giữa HTTP body và Java object. Default Jackson cho JSON. Content negotiation quaAcceptheader.- Filter ≠ Interceptor: Filter chạy trước DispatcherServlet (servlet-level), Interceptor chạy sau routing (MVC-level, biết handler).
- 3
HandlerExceptionResolverdefault:ExceptionHandlerExceptionResolver(method@ExceptionHandler),ResponseStatusExceptionResolver(exception@ResponseStatus),DefaultHandlerExceptionResolver(built-in MVC exception). - URL routing qua
PathPatternParser(Spring 6+) hoặcAntPathMatcher(legacy). Support exact, wildcard*,**, regex\d+, capture remaining{*path}. @EnableWebMvcdisable Boot autoconfig. Customize quaWebMvcConfigurerinterface — 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
Q1Vì 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.xmlkhai 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:
DispatcherServlethandle 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ễ:
MockMvcmockDispatcherServlet, 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.
Q2Filter và Interceptor đều intercept request — khi nào nên dùng cái nào? Cho 2 ví dụ cụ thể.▸
| Aspect | Filter | HandlerInterceptor |
|---|---|---|
| Mức | Servlet API (chuẩn Java EE) | Spring MVC (Spring-specific) |
| Chạy khi | Trước DispatcherServlet | Sau routing, trước handler |
| Biết handler | Không | Có (handler method, controller class) |
| Modify request/response | Có (wrap) | Có nhưng ít chuẩn |
| Short-circuit | Dễ — không call chain.doFilter() | Trả false từ preHandle |
| Spring beans | Phải register FilterRegistrationBean | Là @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());
}
}
▸
@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:
MappingJackson2HttpMessageConverterkhông tự register — JSON serialize fail. - Mất static resource handler: file trong
/static/không serve. - Mất content negotiation default:
Acceptheader 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.
Q4Request đế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?▸
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:
RequestMappingHandlerMappingmatch URL → tìm handlerOrderController.create()với@PostMapping("/orders").- Method có
@RequestBody OrderRequest req— Spring cầnHttpMessageConverterđọc body. - Spring iterate converter list, hỏi "
canRead(OrderRequest.class, application/xml)?".MappingJackson2HttpMessageConvertersupport JSON only → false.- Không converter nào support XML → fail.
- Spring throw
HttpMediaTypeNotSupportedException. DefaultHandlerExceptionResolvermap exception → 415 status code.
Fix tùy yêu cầu:
- Add XML support:Boot autoconfig add
<dependency> <groupId>com.fasterxml.jackson.dataformat</groupId> <artifactId>jackson-dataformat-xml</artifactId> </dependency>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) { ... }consumesfilter 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.
Q5Có 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?▸
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") // 3Accept: 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:
- Match path pattern — cả 3 đều match
/api/orders/42(pattern 3 thêm regex constraint nhưng42match\d+). - Match HTTP method — cả 3 đều GET → tie.
- Match consume + produces — handler 3 require
produces=application/json, requestAccept: application/json→ match. Handler 1, 2 không khai produces → match any. - Match params + headers — handler 2 require
X-Version=2, request có → match. Handler 1, 3 không khai → match any.
Score from "most specific":
| Handler | Path constraint | Headers | Produces | Total specificity |
|---|---|---|---|---|
| 1 | basic | - | - | 0 |
| 2 | basic | X-Version=2 | - | 1 |
| 3 | regex | - | application/json | 2 (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.
Q6Bạ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?▸
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:
- Bước 2 (HandlerMapping): URL routing là O(N) qua N route. App 1000 endpoint → vài ms. Hiếm chậm 5s.
- 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.
- Bước 6 (Handler invocation): business logic trong controller/service. Slow query, slow external API call → 5s realistic.
- Bước 7 (MessageConverter write): serialize huge response (10MB JSON) → vài hundred ms. Không 5s thường.
- Bước 8 (postHandle Interceptors): tương tự bước 5.
Quy trình diagnose:
- Enable timing log:
logging.level.org.springframework.web=DEBUG— show URL match, handler invoke, time. - Add Filter timing đo phase:So với log Spring DEBUG → tìm phase chiếm phần lớn time.
long start = System.currentTimeMillis(); chain.doFilter(req, res); long total = System.currentTimeMillis() - start; log.info("{} {} took {}ms", req.getMethod(), req.getRequestURI(), total); - Inspect interceptor: bật
/actuator/mappingsxem interceptor nào active. Mỗi interceptor add logpreHandle/afterCompletionvới timestamp. - Profile JVM: dùng JFR (Java Flight Recorder) hoặc async-profiler để thấy stack trace nóng.
- Check downstream: nếu controller call DB → bật
org.hibernate.SQL=DEBUG, count query + duration. N+1 hay slow query phổ biến. - 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...