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

DispatcherServlet Spring MVC — Front Controller

DispatcherServlet: servlet duy nhất nhận mọi HTTP request trong Spring MVC. 9 bean hạ tầng, vòng đời request, và cách Spring Boot tự cấu hình Front Controller.

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:

Cột trái bốn servlet, mỗi servlet lặp lại cùng một dòng auth log i18n; cột phải một Filter và Interceptor duy nhất đứng trước DispatcherServlet, sau đó tới controller là method Java thường

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ó, và cũng không cần biết: mọi thứ "Spring" nằm bên trong doDispatch(), phía dưới hợp đồng servlet chuẩn.

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

Chín bước của doDispatch xếp dọc: Tomcat gọi service, getHandler hỏi từng HandlerMapping, trả HandlerExecutionChain, chọn HandlerAdapter, chạy interceptor preHandle, adapter resolve tham số rồi gọi controller, HttpMessageConverter ghi JSON, cuối cùng postHandle và afterCompletion chạy theo thứ tự ngược

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:

Exception từ controller đi lần lượt qua ExceptionHandlerExceptionResolver rồi ResponseStatusExceptionResolver rồi DefaultHandlerExceptionResolver, mỗi resolver bắt được thì trả status tương ứng; không cái nào khớp thì rơi xuống trang lỗi 500 mặc định

Chuỗi này dừng ở resolver đầu tiên nhận diện được exception — nên viết @ExceptionHandler chính là chen vào tầng trên cùng, không cần đụng hai tầng dưới.

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:

Filter chain nằm ngoài khung DispatcherServlet nên chạy cho mọi request kể cả request sắp nhận 404; bên trong khung là HandlerMapping getHandler rồi interceptor preHandle rồi controller method, ba bước chỉ chạy khi URL đã khớp handler

Đường viền trong hình là toàn bộ khác biệt — mọi dòng trong bảng dưới đều đọc ra được từ chỗ mỗi thứ đứng so với đường viền đó:

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
0/5 câu đã trả lời
  1. 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ỗ".
  2. 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ì?
  3. 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?
  4. 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.
  5. 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.

Bài tiếp theo: URL routing & request pipeline

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

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

Bài tiếp theo

URL Routing Pipeline — HandlerMapping, HandlerAdapter, và PathPattern