Spring REST API & Data JPA/DispatcherServlet & Front Controller — một servlet nhận mọi request
2/46
Bài 2 / 46~12 phútSpring MVC CoreMiễn phí lượt xem

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: DispatcherServletFront 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 configweb.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"| H3

1 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| HS

Khi 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ự:

  1. Tomcat nhận request, dispatch đến DispatcherServlet.doDispatch().
  2. getHandler(request) hỏi danh sách HandlerMapping — tìm handler match URL + HTTP method.
  3. Trả về HandlerExecutionChain — wrapper chứa handler method + list interceptor áp dụng.
  4. getHandlerAdapter(handler) chọn adapter tương thích loại handler.
  5. Chạy preHandle() trên mỗi interceptor theo thứ tự. Interceptor nào trả false sẽ short-circuit flow — request dừng tại đó, không bao giờ đến controller.
  6. Adapter invoke handler — resolve argument từ request, gọi method controller.
  7. Controller return Object hoặc ResponseEntity.
  8. Adapter dùng HttpMessageConverter serialize return value thành HTTP response body.
  9. 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 interfaceVai tròDefault impl (Boot)
HandlerMappingURL + HTTP method → handlerRequestMappingHandlerMapping
HandlerAdapterInvoke handler đúng cáchRequestMappingHandlerAdapter
HandlerInterceptorCross-cutting trước/sau handler(custom, đăng ký qua WebMvcConfigurer)
HttpMessageConverterHTTP body ↔ Java objectMappingJackson2HttpMessageConverter
HandlerExceptionResolverConvert exception → HTTP responseExceptionHandlerExceptionResolver
ViewResolverTên view → View object (cho MVC truyền thống)ContentNegotiatingViewResolver
LocaleResolverDetect locale từ requestAcceptHeaderLocaleResolver
MultipartResolverParse multipart/form-dataStandardServletMultipartResolver
ThemeResolverTheme 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:

  1. Resolve từng argument: @PathVariable từ URL, @RequestParam từ query string, @RequestBody qua MessageConverter.
  2. Invoke method.
  3. Convert return value: nếu method có @ResponseBody hoặc return ResponseEntity, adapter dùng MessageConverter để ghi body.

3.3 HttpMessageConverter — body ↔ object

HttpMessageConverter là bridge giữa HTTP byte stream và Java object:

  • Đọc (@RequestBody): với POST /orders body {"total": 150}, converter parse JSON thành OrderRequest object trước khi method chạy.
  • Ghi (@ResponseBody / ResponseEntity): controller return OrderDto, 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/json thì Spring đọc body bằng JSON converter.
  • Client gửi Accept: application/xml thì 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).
ConverterFormatCó khi nào
MappingJackson2HttpMessageConverterapplication/jsonspring-boot-starter-web (mặc định)
StringHttpMessageConvertertext/plainluôn có
ByteArrayHttpMessageConverterapplication/octet-streamluôn có
FormHttpMessageConverterapplication/x-www-form-urlencodedluôn có
MappingJackson2XmlHttpMessageConverterapplication/xmlthê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"| R500

Bà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
FilterHandlerInterceptor
TầngServlet API (chuẩn Java EE)Spring MVC
Chạy khiTrước DispatcherServletSau routing, trước handler
Biết handler nàoKhôngCó — nhận Object handler
Short-circuitKhông gọi chain.doFilter()Trả false từ preHandle()
Dùng choAuth, CORS, rate limit, request wrapAudit 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

Tài liệu chính chủ

Source Spring Framework:

  • DispatcherServlet.java — đọc method doDispatch() (~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 @EnableWebMvc phá Boot.

Spring Reference:

Pattern gốc:

Liên hệ các bài khác

  • refresh() 12 bước khởi tạo container: DispatcherServlet là một bean trong WebApplicationContext — 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ách RequestMappingHandlerAdapter resolve argument từ request.
  • Singleton bean & scopes: DispatcherServlet singleton trong context — cùng cơ chế singletonObjects map. 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.
  • DispatcherServletHttpServlet thậ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

Tự kiểm tra
Q1
Vì 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).

Q2
Boot 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ì?

@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:

  • MappingJackson2HttpMessageConverter không register → @ResponseBody trả 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.

Q3
Request đế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?

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.

Q4
Client 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.

Đọc body: thành công. Request có Content-Type: application/jsonMappingJackson2HttpMessageConverter đọ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 match OrderDto với application/xml.

Không converter nào phù hợp → Spring throw HttpMediaTypeNotAcceptableExceptionDefaultHandlerExceptionResolver 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.

Q5
Bạ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

Đặt 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