DispatcherServlet & Front Controller — một servlet nhận mọi request
DispatcherServlet là Front Controller duy nhất của Spring MVC. Bài này bóc cơ chế thật: tại sao Front Controller tồn tại (cross-cutting tập trung), DispatcherServlet làm gì bên dưới, 9 bean infrastructure phối hợp ra sao, và Boot tự setup thế nào — để mọi annotation MVC ở các bài sau hiện ra logic.
TL;DR: DispatcherServlet là Front Controller duy nhất của Spring MVC — nhận mọi HTTP request rồi dispatch đến handler phù hợp. Lý do tồn tại: gom cross-cutting (auth, log, i18n, error) vào một chỗ thay vì lặp trong từng servlet. Bên dưới, nó delegate cho 9 bean infrastructure: HandlerMapping tìm handler, HandlerAdapter invoke, HttpMessageConverter serialize JSON, HandlerExceptionResolver xử lý lỗi,... Boot WebMvcAutoConfiguration + DispatcherServletAutoConfiguration setup tất cả theo pattern @ConditionalOnMissingBean. Pitfall cốt lõi: dùng @EnableWebMvc vô tình disable toàn bộ Boot autoconfig — luôn customize qua WebMvcConfigurer.
DispatcherServlet là bean nằm trong Spring ApplicationContext — context được khởi động qua refresh() 12 bước trước khi servlet sẵn sàng nhận request. Bài này bóc phần servlet biết request đã vào — bài URL routing & request pipeline tiếp tục với HandlerMapping deep dive.
1. Vì sao cần Front Controller — vấn đề của page-controller cũ
Trước Spring MVC, Java EE web app dùng page-controller pattern: mỗi URL map một servlet riêng.
GET /orders → OrderListServlet.doGet()
GET /orders/42 → OrderDetailServlet.doGet()
POST /orders → OrderCreateServlet.doPost()
DELETE /orders/42 → OrderDeleteServlet.doDelete()
Mỗi servlet extends HttpServlet, khai báo trong web.xml. Đây là cách Java EE chuẩn — nhưng đau theo 3 chiều:
Cross-cutting concern lặp lại — auth check, logging, i18n locale, error handling phải viết trong mỗi servlet. App 50 endpoint = 50 chỗ duplicate. Thêm một bước audit log mới? Sửa 50 file.
Boilerplate config — web.xml khai báo từng servlet + URL mapping. 100 endpoint = 200 entry XML. Tất cả phải restart để nhận config mới.
Khó test — servlet bind cứng vào servlet container. Test OrderListServlet phải start Tomcat, không test unit thuần.
Front Controller pattern (Martin Fowler, "Patterns of Enterprise Application Architecture", 2002) giải quyết bằng một điểm vào duy nhất:
flowchart LR
R["HTTP Request<br/>bat ky URL nao"]
FC["DispatcherServlet<br/>(Front Controller<br/>1 servlet duy nhat)"]
H1["OrderController<br/>.getOrder()"]
H2["UserController<br/>.listUsers()"]
H3["ProductController<br/>.search()"]
R --> FC
FC -->|"route"| H1
FC -->|"route"| H2
FC -->|"route"| H31 servlet nhận mọi request. Cross-cutting (auth filter, log interceptor, error resolver) chạy trong/quanh servlet đó — viết 1 lần, áp dụng cho mọi request. Handler là method Java thường — không extends servlet, không bind container, test dễ.
Vì sao thiết kế này tồn tại, không phải alternative? Alternative là middleware chain (Express.js style) — cũng valid, nhưng Java Servlet spec đã có sẵn single-entry-point qua URL pattern /*. Front Controller tận dụng pattern đó, không phá vỡ spec. Struts 2, JAX-RS Jersey, Spring MVC đều chọn Front Controller vì lý do này.
2. DispatcherServlet là gì bên dưới
DispatcherServlet extends FrameworkServlet extends HttpServletBean extends HttpServlet. Nó là một servlet Java EE thật — Tomcat không biết gì đặc biệt về nó.
flowchart TB
DS["DispatcherServlet"]
FS["FrameworkServlet"]
HSB["HttpServletBean"]
HS["javax.servlet.http.HttpServlet"]
DS -->|extends| FS
FS -->|extends| HSB
HSB -->|extends| HSKhi request vào, Tomcat gọi service(req, res) — đây là servlet API chuẩn. FrameworkServlet.service() route tới doGet()/doPost()/... rồi tất cả hội tụ về DispatcherServlet.doDispatch(request, response) — đây là method cốt lõi (~80 dòng) thực hiện toàn bộ flow dispatch.
Cơ chế bên dưới — luồng doDispatch
sequenceDiagram
participant Tomcat
participant DS as DispatcherServlet
participant HM as HandlerMapping
participant HA as HandlerAdapter
participant IC as Interceptors
participant Ctrl as Controller method
participant MC as MessageConverter
Tomcat->>DS: service(request, response)
DS->>HM: getHandler(request)
HM-->>DS: HandlerExecutionChain<br/>(method + interceptor list)
DS->>HA: getHandlerAdapter(handler)
HA-->>DS: RequestMappingHandlerAdapter
DS->>IC: preHandle() x N
DS->>HA: handle(request, response, handler)
HA->>Ctrl: getOrder(42L)
Ctrl-->>HA: ResponseEntity
HA->>MC: write JSON to response
DS->>IC: postHandle() + afterCompletion()9 bước theo thứ tự:
- Tomcat nhận request, dispatch đến
DispatcherServlet.doDispatch(). getHandler(request)hỏi danh sáchHandlerMapping— tìm handler match URL + HTTP method.- Trả về
HandlerExecutionChain— wrapper chứa handler method + list interceptor áp dụng. getHandlerAdapter(handler)chọn adapter tương thích loại handler.- Chạy
preHandle()trên mỗi interceptor theo thứ tự. Interceptor nào trảfalsesẽ short-circuit flow — request dừng tại đó, không bao giờ đến controller. - Adapter invoke handler — resolve argument từ request, gọi method controller.
- Controller return
ObjecthoặcResponseEntity. - Adapter dùng
HttpMessageConverterserialize return value thành HTTP response body. - Chạy
postHandle()+afterCompletion()trên interceptor theo thứ tự ngược.
Vì sao biết flow này quan trọng? Khi request "không đến controller" — bug ở bước 2 (không match URL), 3 (interceptor reject), hoặc 5 (adapter không tìm được). Không biết flow, debug mù.
3. 9 bean infrastructure
DispatcherServlet không tự làm hết — nó delegate cho 9 loại bean. Đây là Dependency Inversion: servlet chỉ gọi interface, implementation thay được mà không sửa servlet.
| Bean interface | Vai trò | Default impl (Boot) |
|---|---|---|
HandlerMapping | URL + HTTP method → handler | RequestMappingHandlerMapping |
HandlerAdapter | Invoke handler đúng cách | RequestMappingHandlerAdapter |
HandlerInterceptor | Cross-cutting trước/sau handler | (custom, đăng ký qua WebMvcConfigurer) |
HttpMessageConverter | HTTP body ↔ Java object | MappingJackson2HttpMessageConverter |
HandlerExceptionResolver | Convert exception → HTTP response | ExceptionHandlerExceptionResolver |
ViewResolver | Tên view → View object (cho MVC truyền thống) | ContentNegotiatingViewResolver |
LocaleResolver | Detect locale từ request | AcceptHeaderLocaleResolver |
MultipartResolver | Parse multipart/form-data | StandardServletMultipartResolver |
ThemeResolver | Theme UI (legacy, Spring 6 deprecated) | FixedThemeResolver |
Với REST API (trọng tâm module này), 4 bean quan trọng nhất là HandlerMapping, HandlerAdapter, HttpMessageConverter, và HandlerExceptionResolver. ViewResolver ít dùng vì REST trả JSON không render view.
3.1 HandlerMapping — tìm handler từ request
RequestMappingHandlerMapping đọc tất cả @RequestMapping / @GetMapping / @PostMapping trên class + method lúc khởi động, build bảng routing:
GET /api/orders/{id} → OrderController.getOrder(Long)
GET /api/orders → OrderController.listOrders()
POST /api/orders → OrderController.createOrder(OrderRequest)
Khi request đến, mapper so URL + HTTP method với bảng routing rồi trả về HandlerExecutionChain. Nếu không entry nào match, Spring throw NoHandlerFoundException và Boot trả 404 mặc định.
Bài URL routing & request pipeline đào sâu PathPattern syntax ({id}, **, regex), ambiguous mapping, và cách Boot resolve conflict.
3.2 HandlerAdapter — invoke đúng cách
Spring hỗ trợ nhiều loại handler: method @RequestMapping, Servlet, HttpRequestHandler, WebFlux handler. Adapter pattern cho phép DispatcherServlet không biết loại handler — nó hỏi từng adapter supports(handler)?, adapter biết cách invoke.
RequestMappingHandlerAdapter xử lý method @RequestMapping:
- Resolve từng argument:
@PathVariabletừ URL,@RequestParamtừ query string,@RequestBodyquaMessageConverter. - Invoke method.
- Convert return value: nếu method có
@ResponseBodyhoặc returnResponseEntity, adapter dùngMessageConverterđể ghi body.
3.3 HttpMessageConverter — body ↔ object
HttpMessageConverter là bridge giữa HTTP byte stream và Java object:
- Đọc (
@RequestBody): vớiPOST /ordersbody{"total": 150}, converter parse JSON thànhOrderRequestobject trước khi method chạy. - Ghi (
@ResponseBody/ResponseEntity): controller returnOrderDto, converter serialize object đó thành JSON response body.
Boot register converter theo classpath. Chọn converter bằng content negotiation:
- Client gửi
Content-Type: application/jsonthì Spring đọc body bằng JSON converter. - Client gửi
Accept: application/xmlthì Spring ghi body bằng XML converter (nếu cójackson-dataformat-xml). - Khi không converter nào phù hợp, Spring trả 415 Unsupported Media Type (chiều đọc) hoặc 406 Not Acceptable (chiều ghi).
| Converter | Format | Có khi nào |
|---|---|---|
MappingJackson2HttpMessageConverter | application/json | spring-boot-starter-web (mặc định) |
StringHttpMessageConverter | text/plain | luôn có |
ByteArrayHttpMessageConverter | application/octet-stream | luôn có |
FormHttpMessageConverter | application/x-www-form-urlencoded | luôn có |
MappingJackson2XmlHttpMessageConverter | application/xml | thêm jackson-dataformat-xml |
3.4 HandlerExceptionResolver — convert exception thành response
Khi controller throw exception, DispatcherServlet catch rồi iterate qua list HandlerExceptionResolver theo @Order:
flowchart TB
Ex["Controller throws Exception"]
DS["DispatcherServlet catch"]
R1["ExceptionHandlerExceptionResolver<br/>@ExceptionHandler method"]
R2["ResponseStatusExceptionResolver<br/>@ResponseStatus annotation"]
R3["DefaultHandlerExceptionResolver<br/>Spring built-in exception"]
R500["500 error page (fallback)"]
DS --> R1
R1 -->|"khong match"| R2
R2 -->|"khong match"| R3
R3 -->|"khong match"| R500Bài exception handling & advice đào sâu @ControllerAdvice + Problem Details RFC 9457.
4. Boot tự setup thế nào
Không có Spring Boot, bạn phải register DispatcherServlet thủ công trong web.xml hoặc WebApplicationInitializer. Boot tự làm qua hai auto-configuration class:
DispatcherServletAutoConfiguration — register DispatcherServlet bean và đăng ký nó với Tomcat:
// Rut gon — spring-boot-autoconfigure
@AutoConfiguration
@ConditionalOnWebApplication(type = SERVLET)
@ConditionalOnClass(DispatcherServlet.class)
public class DispatcherServletAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public DispatcherServlet dispatcherServlet(WebMvcProperties props) {
DispatcherServlet ds = new DispatcherServlet();
ds.setDispatchOptionsRequest(props.isDispatchOptionsRequest());
// ... apply properties
return ds;
}
@Bean
@ConditionalOnMissingBean
public DispatcherServletRegistrationBean dispatcherServletRegistration(
DispatcherServlet ds, WebMvcProperties props) {
// Register voi Tomcat, URL pattern "/"
return new DispatcherServletRegistrationBean(ds, props.getServlet().getPath());
}
}
WebMvcAutoConfiguration — register 9 bean infrastructure:
@AutoConfiguration(after = DispatcherServletAutoConfiguration.class)
@ConditionalOnWebApplication(type = SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) // <-- KEY: disable neu @EnableWebMvc
public class WebMvcAutoConfiguration {
@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping(...) { ... }
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(...) { ... }
// ... 7 bean khac
}
Dòng @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) là then chốt: khi bạn thêm @EnableWebMvc, nó import DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport — condition này lập tức fail, kéo theo WebMvcAutoConfiguration bị skip hoàn toàn.
Customize mà không phá defaults — WebMvcConfigurer
Boot collect tất cả bean implement WebMvcConfigurer, gọi mỗi method để merge customization vào defaults:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new TimingInterceptor())
.addPathPatterns("/api/**");
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://olhub.vn");
}
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
// Them converter tuy chinh — KHONG replace default list
converters.add(new ProtobufHttpMessageConverter());
}
}
Khác biệt cốt lõi: WebMvcConfigurer contribute (thêm vào), @EnableWebMvc replace (xóa defaults rồi bắt bạn config từ đầu).
5. Filter vs Interceptor — chạy ở đâu trong flow
Cả hai intercept request — nhưng ở tầng khác nhau:
flowchart LR
Client["Client"] --> F["Filter Chain<br/>(Servlet API)"]
F --> DS["DispatcherServlet"]
DS --> IC["Interceptor preHandle<br/>(Spring MVC)"]
IC --> Ctrl["Controller method"]
Ctrl --> IC2["Interceptor postHandle<br/>+ afterCompletion"]
IC2 --> F2["Filter (response phase)"]
F2 --> Client| Filter | HandlerInterceptor | |
|---|---|---|
| Tầng | Servlet API (chuẩn Java EE) | Spring MVC |
| Chạy khi | Trước DispatcherServlet | Sau routing, trước handler |
| Biết handler nào | Không | Có — nhận Object handler |
| Short-circuit | Không gọi chain.doFilter() | Trả false từ preHandle() |
| Dùng cho | Auth, CORS, rate limit, request wrap | Audit log theo handler, timing per-controller |
Vì sao Filter phù hợp auth? Vì auth phải chạy trước routing — reject request trước khi DispatcherServlet thậm chí tìm handler. Spring Security implement toàn bộ qua filter chain cho lý do này.
Vì sao Interceptor phù hợp audit? Vì interceptor biết HandlerMethod — biết class controller, tên method, annotation trên method. Audit log "user X gọi OrderController.delete()" không thể làm từ filter (filter không biết handler).
6. Pitfall thực tế
❌ Nhầm 1 — @EnableWebMvc để "cấu hình MVC":
@Configuration
@EnableWebMvc // SAI — tat het Boot autoconfig
public class WebConfig implements WebMvcConfigurer {
// Moi thu khac mat: Jackson, static resource, error mapping...
}
✅ Bỏ @EnableWebMvc. Implement WebMvcConfigurer không cần annotation đó — Boot pick up tự động.
❌ Nhầm 2 — Tưởng DispatcherServlet là singleton toàn JVM:
✅ DispatcherServlet là bean trong WebApplicationContext — nó là singleton trong phạm vi context đó. Cơ chế giống singleton bean: một entry trong singletonObjects map của context. Mỗi request Tomcat dùng cùng instance servlet, nhưng mỗi request có HttpServletRequest/HttpServletResponse riêng — thread-safe vì không có state trên field servlet.
❌ Nhầm 3 — Tưởng interceptor chạy như filter:
// Interceptor trong WebConfig
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtInterceptor()); // CHUA DU cho auth
}
✅ Interceptor chạy SAU routing — nếu không match URL nào, NoHandlerFoundException xảy ra trước interceptor. Dùng filter nếu cần intercept mọi request kể cả 404. Dùng interceptor nếu chỉ cần intercept request đã match handler.
❌ Nhầm 4 — configureMessageConverters thay thế mặc định:
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(new ProtobufHttpMessageConverter());
// Jackson BIEN MAT -- list ban dau empty, boot default khong inject
}
✅ Dùng extendMessageConverters thay — list đã có default, method này chỉ thêm:
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new ProtobufHttpMessageConverter()); // them vao dau
}
7. Deep Dive
Source Spring Framework:
DispatcherServlet.java— đọc methoddoDispatch()(~80 dòng). Toàn bộ flow 9 bước nằm ở đây. Đọc 1 lần, mọi annotation MVC sau đó hiện ra logic.WebMvcAutoConfiguration.java— xem@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)để hiểu vì sao@EnableWebMvcphá Boot.
Spring Reference:
- Spring MVC — DispatcherServlet — overview chính thức, đủ đọc trước khi đọc source.
- Spring MVC — Special Bean Types — 9 bean infrastructure chính thức.
- Spring Boot — MVC Auto-configuration — cách Boot setup và cách customize.
Pattern gốc:
- Martin Fowler — Front Controller — pattern gốc 2002.
Liên hệ các bài khác
- refresh() 12 bước khởi tạo container:
DispatcherServletlà một bean trongWebApplicationContext— context phải refresh() xong trước khi servlet sẵn sàng nhận request. Biết 12 bước đó giúp hiểu vì sao lỗi cấu hình lộ ra lúc startup, không phải lúc request đầu tiên. - Bài 02 — URL routing & request pipeline: deep dive
HandlerMapping— PathPattern syntax, ambiguous mapping, header/param matching, và cáchRequestMappingHandlerAdapterresolve argument từ request. - Singleton bean & scopes:
DispatcherServletsingleton trong context — cùng cơ chếsingletonObjectsmap. Giải thích vì sao không có state trên field servlet mà nhiều request vẫn an toàn.
Tóm tắt
- Front Controller pattern: 1 servlet duy nhất nhận mọi request → cross-cutting (auth, log, i18n) tập trung 1 chỗ, handler là method Java thường không phải servlet.
DispatcherServletlàHttpServletthật, đăng ký với Tomcat URL pattern"/". Method cốt lõi:doDispatch().- 9 bước dispatch: Tomcat → HandlerMapping → HandlerExecutionChain → HandlerAdapter → Interceptor preHandle → Controller → MessageConverter → Interceptor afterCompletion.
- 9 bean infrastructure:
HandlerMapping,HandlerAdapter,HandlerInterceptor,HttpMessageConverter,HandlerExceptionResolver,ViewResolver,LocaleResolver,MultipartResolver,ThemeResolver. - Boot setup qua
DispatcherServletAutoConfiguration(register servlet) +WebMvcAutoConfiguration(register 9 bean) — cả 2 dùng@ConditionalOnMissingBean. - Customize qua
WebMvcConfigurer(contribute, giữ defaults). Không dùng@EnableWebMvc(replace, phá defaults). - Filter chạy trước DispatcherServlet (auth, CORS). Interceptor chạy sau routing, biết handler (audit, timing).
Tự kiểm tra
Q1Vì sao Front Controller pattern giải quyết được vấn đề cross-cutting concern lặp lại của page-controller? Giải thích theo cơ chế — không chỉ nói "tập trung vào một chỗ".▸
Page-controller: mỗi URL là 1 servlet riêng. Muốn thêm auth check, phải thêm code vào mỗi servlet — hoặc tạo base class và hy vọng mọi servlet extends đúng. 50 endpoint = 50 điểm cần sửa khi thay đổi logic auth.
Front Controller: 1 servlet duy nhất (DispatcherServlet) nhận mọi request. Cross-cutting đặt vào filter chain (chạy trước servlet) hoặc interceptor (chạy trong servlet trước/sau handler). Logic auth viết 1 lần trong filter, tự động áp dụng cho mọi request đi qua servlet đó.
Cơ chế: Tomcat gọi DispatcherServlet.doDispatch() cho mỗi request. Trước khi gọi handler, doDispatch iterate qua list interceptor và gọi preHandle() — bất kỳ interceptor nào trả false → flow dừng, controller không bao giờ chạy. Thêm cross-cutting mới = thêm 1 interceptor vào registry, không sửa controller nào.
Đây là lý do Spring Security implement qua filter chain và interceptor — viết security logic 1 lần, áp dụng cho mọi endpoint không cần annotation trên từng method (dù method-level security cũng hỗ trợ qua AOP).
Q2Boot có @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) trên WebMvcAutoConfiguration. @EnableWebMvc import class nào khiến condition này fail, dẫn đến hậu quả gì?▸
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) trên WebMvcAutoConfiguration. @EnableWebMvc import class nào khiến condition này fail, dẫn đến hậu quả gì?@EnableWebMvc import DelegatingWebMvcConfiguration, class này extends WebMvcConfigurationSupport. Ngay khi WebMvcConfigurationSupport xuất hiện trong context, condition @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) fail → WebMvcAutoConfiguration bị skip hoàn toàn.
Hậu quả cụ thể — mọi thứ Boot setup sẵn biến mất:
MappingJackson2HttpMessageConverterkhông register →@ResponseBodytrả JSON fail (trả chuỗi trống hoặc 406).- Static resource handler (
/static/,/public/) không active → file CSS/JS 404. - Error page mapping (
/error) không setup → exception lộ stack trace thô. - Content negotiation mặc định mất.
Bug nguy hiểm: app compile + start không lỗi — chỉ lộ khi test endpoint. JSON return empty, static file 404, khó debug nếu không biết nguyên nhân.
Cách đúng: implement WebMvcConfigurer không kèm @EnableWebMvc. Boot detect bean implement interface, gọi method để merge customization — defaults vẫn còn nguyên.
Q3Request đến GET /api/orders/42. Bạn có interceptor JwtInterceptor register với pattern /api/**. Request này không match bất kỳ controller nào. JwtInterceptor.preHandle() có chạy không? Vì sao?▸
GET /api/orders/42. Bạn có interceptor JwtInterceptor register với pattern /api/**. Request này không match bất kỳ controller nào. JwtInterceptor.preHandle() có chạy không? Vì sao?Không chạy. Interceptor chỉ chạy sau khi HandlerMapping tìm được handler thành công.
Flow thực tế: DispatcherServlet.doDispatch() gọi getHandler(request) trước. Không có controller match /api/orders/42 nên Spring throw NoHandlerFoundException. Exception này được bắt và xử lý bởi HandlerExceptionResolver — trả 404. Flow dừng tại đây, không bao giờ đến bước chạy interceptor.
Hệ quả: nếu dùng interceptor cho auth, request đến URL không tồn tại vẫn bị block bởi 404 (không phải bởi auth) — nhưng nếu có URL matching mà interceptor không cover đủ pattern, request lọt qua. Auth phải ở tầng filter (chạy trước routing) để đảm bảo mọi request đều qua auth check kể cả request đến URL không tồn tại.
Đây là lý do Spring Security dùng filter chain — hoạt động ngoài vòng DispatcherServlet, đảm bảo auth check trước cả routing.
Q4Client gửi POST /api/orders body JSON, header Accept: application/xml. App chỉ có Jackson JSON converter (Boot default). Điều gì xảy ra? Trả lời theo flow HttpMessageConverter.▸
POST /api/orders body JSON, header Accept: application/xml. App chỉ có Jackson JSON converter (Boot default). Điều gì xảy ra? Trả lời theo flow HttpMessageConverter.Đọc body: thành công. Request có Content-Type: application/json — MappingJackson2HttpMessageConverter đọc được (canRead(OrderRequest.class, application/json) = true). Body parse thành OrderRequest object bình thường.
Ghi response: fail — 406 Not Acceptable. Controller return OrderDto. HandlerAdapter cần ghi response với format phù hợp Accept: application/xml. Iterate converter list, hỏi canWrite(OrderDto.class, application/xml):
MappingJackson2HttpMessageConverter: hỗ trợ JSON, không XML →false.- Các converter khác (
StringHttpMessageConverter, v.v.) không matchOrderDtovớiapplication/xml.
Không converter nào phù hợp → Spring throw HttpMediaTypeNotAcceptableException → DefaultHandlerExceptionResolver map thành 406 Not Acceptable.
Fix: thêm jackson-dataformat-xml dependency → Boot autoconfig add MappingJackson2XmlHttpMessageConverter → content negotiation tự chọn XML converter khi client request XML. Hoặc đặt produces = "application/json" trên handler → client gửi Accept: application/xml không match handler → 406 rõ ràng hơn từ routing level.
Q5Bạn muốn log thời gian xử lý (millisecond) của mỗi request kèm tên controller method. Nên dùng Filter hay HandlerInterceptor? Tại sao? Viết đoạn code khung.▸
Dùng HandlerInterceptor — vì cần biết tên controller method, thông tin chỉ có sau khi routing xong.
Filter chạy trước DispatcherServlet — không biết handler nào sẽ chạy. Interceptor chạy sau routing, nhận Object handler có thể cast thành HandlerMethod để lấy tên method + class.
@Component
public class TimingInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
req.setAttribute("startTime", System.currentTimeMillis());
return true; // tiep tuc xu ly
}
@Override
public void afterCompletion(HttpServletRequest req, HttpServletResponse res,
Object handler, Exception ex) {
long elapsed = System.currentTimeMillis() - (long) req.getAttribute("startTime");
if (handler instanceof HandlerMethod hm) {
String ctrl = hm.getBeanType().getSimpleName();
String method = hm.getMethod().getName();
// log: "OrderController.getOrder took 42ms"
}
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired private TimingInterceptor timingInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(timingInterceptor)
.addPathPatterns("/api/**");
}
}afterCompletion chạy sau cả response write — đo đúng toàn bộ thời gian xử lý kể cả serialization. postHandle chạy trước write response — không đo serialization time.
Production: dùng Micrometer @Observed hoặc Actuator metrics http.server.requests — tự động record P50/P95/P99 per endpoint không cần viết tay.
Bài tiếp theo: URL routing & request pipeline
Bài này có giúp bạn hiểu bản chất không?
Hỏi đáp về bài này
Chưa có câu hỏi
Có gì chưa rõ trong bài? Đặt câu hỏi đầu tiên — câu trả lời từ cộng đồng giúp bạn (và người sau).
Đặt câu hỏi đầu tiên