URL Routing Pipeline — HandlerMapping, HandlerAdapter, và PathPattern
Bài này bóc chính xác pipeline URL routing bên trong DispatcherServlet: HandlerMapping match URL + HTTP method ra handler, HandlerAdapter invoke handler đúng cách, PathPatternParser parse pattern. Tại sao tách hai tầng? Open-Closed: thêm handler type mới không cần sửa DispatcherServlet.
TL;DR: Khi request đến, DispatcherServlet uỷ quyền cho HandlerMapping — tra map (URL + HTTP method) → handler và trả về HandlerExecutionChain. Tiếp theo, HandlerAdapter nhận chain đó và biết cách invoke handler đúng cách (resolve argument, gọi method, convert return value). Tách hai tầng này là quyết định thiết kế theo nguyên tắc Open-Closed: Spring có thể thêm loại handler mới (Servlet, HttpRequestHandler, Kotlin coroutine…) chỉ bằng cách thêm adapter — không sửa DispatcherServlet. URL syntax dùng PathPatternParser (Spring 6+, default) thay AntPathMatcher — parser nhanh hơn 6-8x và reject ambiguous pattern tại startup.
Bài trước (DispatcherServlet) đã phác 9 bước tổng thể. Bài này thu hẹp vào bước 2-4: routing pipeline — từ khi DispatcherServlet cầm HttpServletRequest đến khi handler method bắt đầu chạy. Bài sau (@RestController & request mapping) sẽ đào sâu cú pháp annotation trên controller.
1. Bức tranh lớn — routing pipeline trong 4 bước
Nhìn lại sequence từ bài trước và zoom vào bước 2-5:
flowchart TD Req["HttpServletRequest<br/>POST /api/orders"] --> DS["DispatcherServlet.doDispatch()"] DS --> HM["HandlerMapping.getHandler(request)<br/>tra map URL+method -> handler"] HM --> Chain["HandlerExecutionChain<br/>(handler + interceptors)"] Chain --> HA["HandlerAdapter.supports(handler)?<br/>chon adapter phu hop"] HA --> Invoke["HandlerAdapter.handle()<br/>resolve args, goi method, convert return"] Invoke --> Resp["ModelAndView / ResponseEntity"]
Pipeline chia rõ hai trách nhiệm:
HandlerMapping: biết URL + method → handler nào. Không biết invoke ra sao.HandlerAdapter: biết invoke handler ra sao (resolve@PathVariable, gọi method, serialize response). Không biết routing.
Tách biệt này là chủ đích — phần 2 giải thích tại sao.
2. Tại sao tách HandlerMapping và HandlerAdapter — Open-Closed
Câu hỏi hợp lý: DispatcherServlet biết handler rồi, sao không invoke thẳng?
Nhìn Spring 1.x — handler có thể là:
Controllerinterface (Spring cổ, implementhandleRequest(req, res))HttpRequestHandlerinterface (serve static content)- Method
@RequestMapping(Spring 3+) Servletinstance thực thụ (tích hợp legacy)- Kotlin coroutine suspend function (Spring 5.2+)
Mỗi loại có cách invoke khác nhau. Nếu DispatcherServlet tự invoke, nó phải chứa if/else instanceof cho từng loại — vi phạm Open-Closed Principle: thêm loại handler = sửa DispatcherServlet.
Thay vào đó, Spring dùng Adapter pattern:
flowchart LR DS["DispatcherServlet"] -->|"supports(handler)?"| A1["RequestMappingHandlerAdapter<br/>cho @RequestMapping method"] DS -->|"supports(handler)?"| A2["HttpRequestHandlerAdapter<br/>cho HttpRequestHandler"] DS -->|"supports(handler)?"| A3["SimpleControllerHandlerAdapter<br/>cho Controller interface"] A1 -->|handle| M["method invoke"] A2 -->|handle| HR["handleRequest()"] A3 -->|handle| HC["handleRequest()"]
DispatcherServlet chỉ làm:
// Simplified from DispatcherServlet source
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
Thêm loại handler mới (vd gRPC handler) chỉ cần thêm HandlerAdapter implement — DispatcherServlet không đổi. Đây là Open for extension, closed for modification đúng nghĩa.
Pattern HandlerMapping/HandlerAdapter xuất hiện xuyên suốt Spring: ArgumentResolver, ReturnValueHandler, MessageConverter đều theo cùng triết lý — mỗi tầng có interface rõ, extend bằng cách thêm implement, không sửa core.
3. HandlerMapping — cơ chế tra map
3.1 RequestMappingHandlerMapping — đăng ký route lúc startup
RequestMappingHandlerMapping là implementation mặc định (Boot tự register qua WebMvcAutoConfiguration). Tại startup, nó scan toàn bộ bean tìm class có @Controller hoặc @RequestMapping, rồi build map:
RequestMappingInfo → HandlerMethod
(URL pattern + HTTP method + consumes + produces + headers + params) → (bean + method)
Ví dụ app có controller:
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderDto getOrder(@PathVariable Long id) { ... }
@PostMapping
public ResponseEntity<OrderDto> createOrder(@RequestBody OrderRequest req) { ... }
}
Map sau startup:
GET /api/orders/{id} → OrderController.getOrder(Long)
POST /api/orders → OrderController.createOrder(OrderRequest)
Map này bất biến sau startup — RequestMappingHandlerMapping không reload. Nếu cần dynamic routing (hot-reload route), cần custom HandlerMapping hoặc framework khác (Spring Cloud Gateway).
3.2 doDispatch() — tìm handler cho request đến
Khi request đến, DispatcherServlet.doDispatch() gọi:
// AbstractHandlerMapping.getHandler() flow
HandlerExecutionChain chain = handlerMapping.getHandler(request);
Bên trong, RequestMappingHandlerMapping tra map theo độ cụ thể giảm dần (most specific match first):
- Exact path match:
/api/orders/42match/api/orders/{id}→ score cao hơn wildcard. - HTTP method match:
GETphải match@GetMapping. consumesmatch:Content-Typeheader.producesmatch:Acceptheader.headers/paramsmatch nếu khai báo.
Kết quả trả về HandlerExecutionChain:
public class HandlerExecutionChain {
private final Object handler; // HandlerMethod (controller method)
private final List<HandlerInterceptor> interceptorList; // interceptors
}
Nếu không match handler nào: throw NoHandlerFoundException → Boot trả 404.
3.3 Ambiguous handler — lỗi tại startup, không tại runtime
Nếu hai handler có cùng mức độ cụ thể (ambiguous), Spring ném IllegalStateException ngay khi khởi động — fail fast, không âm thầm route sai:
Ambiguous handler methods mapped for '/api/orders':
{GET /api/orders, produces [application/json]} OrderController#list()
{GET /api/orders, produces [application/json]} OrderControllerV2#list()
Đây là lựa chọn thiết kế chủ đích — lỗi routing phải lộ sớm nhất có thể.
4. PathPattern — engine parse URL
4.1 PathPatternParser vs AntPathMatcher
Spring 6+ (Boot 3+) default dùng PathPatternParser thay AntPathMatcher (legacy):
| Aspect | AntPathMatcher | PathPatternParser |
|---|---|---|
| Parse khi nào | Mỗi request — parse string lại từ đầu | Một lần lúc startup — compile thành PathPattern object |
| Tốc độ match | O(N) string scan | O(độ sâu path) với trie-like structure |
| Benchmark | baseline | 6-8x nhanh hơn trên path phức tạp |
| Reject ambiguous | Không — silent wrong match | Có — ném lỗi tại startup |
| Trailing slash | Accept /orders/ = /orders | Mặc định không accept — phải config rõ |
PathPatternParser compile pattern một lần thành PathPattern object tại startup. Mỗi request chỉ cần gọi pattern.matches(path) — không parse string lại. Đây là lý do cải thiện tốc độ lớn.
4.2 PathPattern syntax đầy đủ
@GetMapping("/orders") // exact match
@GetMapping("/orders/{id}") // path variable — capture 1 segment
@GetMapping("/orders/{id:\\d+}") // path variable + regex constraint
@GetMapping("/orders/{*path}") // capture ALL remaining segments (greedy)
@GetMapping("/api/v?") // ? = match exactly 1 char (v1, v2, vX)
@GetMapping("/api/*/orders") // * = match exactly 1 segment
@GetMapping("/files/**") // ** = match 0 or more segments
@GetMapping({"/orders", "/api/orders"}) // multiple patterns for same handler
Phân biệt {*path} vs /**:
{*path}: capture phần còn lại vào biến, dùng@PathVariable String pathđể đọc. Vd/files/a/b/c.txt→path = "/a/b/c.txt"./**: wildcard không capture — chỉ match, không truyền giá trị.
Regex constraint trong {id:\\d+}:
@GetMapping("/orders/{id:\\d+}")
public OrderDto getOrder(@PathVariable Long id) { ... }
// /orders/42 → match, id = 42
// /orders/abc → NO match → 404 (không 400)
Lưu ý: regex mismatch trả 404 (no handler found), không 400 (bad request). Nếu muốn trả 400 với message rõ, validate @PathVariable bằng @Min/@Pattern và @Validated tại controller — bài Validation đào sâu.
4.3 Trailing slash — thay đổi breaking từ Spring 6
Trước Spring 6: /orders/ và /orders coi là match nhau (trailing slash insensitive).
Spring 6+ (Boot 3+): mặc định strict — /orders/ và /orders là hai URL khác nhau. Client gửi /orders/ → 404 nếu handler chỉ map /orders.
Cấu hình lại nếu cần backward compat:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
configurer.setUseTrailingSlashMatch(true); // deprecated, Spring 6.x only
}
}
Tuy nhiên, cách tốt hơn là sửa client gửi đúng URL. setUseTrailingSlashMatch(true) deprecated và có thể bị xoá trong Spring 7.
Upgrade Boot 2 lên Boot 3: một số endpoint bỗng 404 với body rỗng. Debug: bật logging.level.org.springframework.web.servlet.DispatcherServlet=TRACE — log show "No mapping for GET /orders/". Nguyên nhân: trailing slash. Fix: sửa URL client hoặc thêm handler /orders/ explicit.
5. HandlerAdapter — invoke handler đúng cách
5.1 RequestMappingHandlerAdapter — adapter chính cho @RequestMapping
RequestMappingHandlerAdapter là adapter xử lý method có @RequestMapping (tức @GetMapping, @PostMapping, etc.). Khi DispatcherServlet đưa HandlerMethod (bean + method) cho nó, adapter làm 4 việc theo thứ tự:
flowchart TD HA["RequestMappingHandlerAdapter.handle()"] HA --> R["1. Resolve method arguments<br/>(HandlerMethodArgumentResolver)<br/>@PathVariable, @RequestBody, @RequestParam..."] R --> I["2. Invoke method<br/>controller.getOrder(42L)"] I --> RV["3. Handle return value<br/>(HandlerMethodReturnValueHandler)<br/>@ResponseBody, ResponseEntity, Mono..."] RV --> W["4. Write response<br/>(HttpMessageConverter)"]
Resolve arguments — mỗi loại argument có HandlerMethodArgumentResolver riêng:
| Argument type | Resolver |
|---|---|
@PathVariable Long id | PathVariableMethodArgumentResolver |
@RequestParam String q | RequestParamMethodArgumentResolver |
@RequestBody OrderRequest req | RequestResponseBodyMethodProcessor |
@RequestHeader String auth | RequestHeaderMethodArgumentResolver |
HttpServletRequest | ServletRequestMethodArgumentResolver |
@Valid @RequestBody | Wrapper + javax.validation.Validator |
Spring 6 có 30+ resolver. Thứ tự resolver quan trọng — Spring iterate list, dùng resolver đầu tiên trả true cho supportsParameter().
Handle return value — tương tự, mỗi return type có handler:
| Return type | Handler |
|---|---|
ResponseEntity<T> | HttpEntityMethodProcessor |
Object có @ResponseBody | RequestResponseBodyMethodProcessor |
String (view name) | ViewNameMethodReturnValueHandler |
void | VoidMethodReturnValueHandler |
Mono<T> (reactive) | ReactiveTypeReturnValueHandler |
5.2 Vì sao cần HandlerAdapter — không phải chỉ gọi Method.invoke()?
Vấn đề: method controller có argument từ nhiều nguồn (@PathVariable, @RequestBody, @RequestParam), return type đa dạng (ResponseEntity, String, Mono). Java Method.invoke() chỉ nhận Object[] args — ai resolve arguments?
RequestMappingHandlerAdapter trả lời câu hỏi đó:
// Simplified logic inside RequestMappingHandlerAdapter
private Object invokeHandlerMethod(HttpServletRequest request, Method method, Object bean) {
Object[] args = resolveMethodArguments(request, method.getParameters());
Object returnValue = method.invoke(bean, args);
handleReturnValue(returnValue, method.getReturnType(), response);
}
Nếu muốn custom argument (vd inject @CurrentUser từ JWT), thêm HandlerMethodArgumentResolver — không cần sửa adapter hay dispatcher:
@Component
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class);
}
@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
String token = webRequest.getHeader("Authorization");
return jwtService.extractUser(token); // return UserPrincipal
}
}
// Register:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver(jwtService));
}
}
6. Cơ chế bên dưới — doDispatch() source walk
Đọc DispatcherServlet.doDispatch() (~80 dòng) để thấy pipeline không magic:
// Simplified from DispatcherServlet.doDispatch() — Spring 6 source
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HandlerExecutionChain mappedHandler = null;
try {
// Buoc 2: tim handler
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response); // 404
return;
}
// Buoc 4: chon adapter
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Buoc 5: pre-handle interceptors
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return; // interceptor tra false -> short-circuit
}
// Buoc 6: adapter invoke handler
ModelAndView mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
// Buoc 7-8: post-handle + render
mappedHandler.applyPostHandle(processedRequest, response, mv);
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
} catch (Exception ex) {
processDispatchResult(processedRequest, response, mappedHandler, mv, ex);
}
}
Hai điểm cần nhớ từ source:
getHandler()iterate listHandlerMappingtheo thứ tự@Order— dừng tại mapping đầu tiên trả non-null. Default Boot có 2 mapping:RequestMappingHandlerMapping(order -1, cho@RequestMapping) vàRouterFunctionMapping(order 3, cho functional endpoints).getHandlerAdapter()iterate listHandlerAdaptertheo@Order, gọisupports(handler)— dừng tại adapter đầu tiên trả true.
7. Production — thread sizing và slow request
7.1 Routing không phải bottleneck — nhưng interceptor thì có
RequestMappingHandlerMapping.getHandler() là tra HashMap — O(1) amortized. Với 1000 endpoint, routing thêm vài micro-giây — không đáng kể.
Bottleneck thực tế hay gặp là interceptor trong preHandle:
// Anti-pattern: query DB trong preHandle moi request
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
User user = userRepository.findByToken(token); // DB query moi request!
if (user == null) { res.setStatus(401); return false; }
req.setAttribute("user", user);
return true;
}
Fix: cache user lookup (Redis/Caffeine), hoặc dùng JWT tự-verify (không cần DB).
7.2 Virtual threads và routing
Boot 3.2 + Java 21 với spring.threads.virtual.enabled=true: mỗi request chạy trên virtual thread riêng. doDispatch() vẫn blocking — virtual thread block I/O trong handler (DB call) sẽ unmount, platform thread tiếp tục phục vụ request khác.
Lưu ý: RequestMappingHandlerMapping dùng ConcurrentHashMap internally — đọc map an toàn đa thread. Không có lock contention tại routing bước.
7.3 Diagnose request "không đến controller"
Khi handler method không chạy nhưng không rõ lý do:
# application.yml — bật trace cho routing
logging:
level:
org.springframework.web.servlet.DispatcherServlet: TRACE
org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping: TRACE
Log sẽ show:
TRACE DispatcherServlet - GET "/api/orders/abc", parameters={}
TRACE RequestMappingHandlerMapping - Mapped to OrderController#getOrder(Long)
Hoặc khi không match:
TRACE DispatcherServlet - No mapping for GET /api/orders/abc
Kết hợp /actuator/mappings endpoint (Boot Actuator) để xem toàn bộ route map lúc runtime.
Pitfall
❌ Nhầm 1 — tưởng @PathVariable với regex {id:\\d+} trả 400 khi sai format:
@GetMapping("/orders/{id:\\d+}")
public OrderDto get(@PathVariable Long id) { ... }
// GET /orders/abc → THỰC RA 404, không 400
✅ Regex constraint là routing condition — không match → "không có handler" → 404. Để trả 400 với thông báo rõ, bỏ regex trong URL và dùng @Validated + @Min(1) tại method argument.
❌ Nhầm 2 — thêm custom HandlerMethodArgumentResolver nhưng không hoạt động:
// Register nhung khong work vi priority sai
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new CurrentUserArgumentResolver());
}
✅ Custom resolver qua addArgumentResolvers được thêm sau resolver built-in. Nếu built-in resolver claim argument trước (vd @RequestBody resolver), custom không chạy. Check supportsParameter() không overlap với resolver có sẵn; hoặc implement WebMvcConfigurer.configureArgumentResolvers() để thay thế toàn bộ list (mạnh hơn, cẩn thận mất built-in).
❌ Nhầm 3 — tưởng trailing slash tự động redirect:
@GetMapping("/orders") // handler
// Client GET /orders/ → 404, không redirect
✅ Spring 6+ không redirect trailing slash mặc định. Nếu muốn redirect, tự config:
// Them handler explicit, hoac dung URL rewrite filter
@GetMapping({"/orders", "/orders/"})
public List<OrderDto> list() { ... }
❌ Nhầm 4 — tạo HandlerMapping bean và tưởng nó replace default:
@Bean
public HandlerMapping myHandlerMapping() {
return new SimpleUrlHandlerMapping(urlMap, 1);
}
✅ Bean mới được thêm vào danh sách, không replace RequestMappingHandlerMapping. Thứ tự quyết định bởi @Order/Ordered.getOrder(). RequestMappingHandlerMapping có order -1 — để override nó cần order nhỏ hơn -1.
Liên hệ các bài khác
- Bài 01 — DispatcherServlet: phác 9 bước tổng thể; bài này đào sâu bước 2-4 (HandlerMapping + HandlerAdapter). Đọc bài 01 trước để có context full pipeline.
- Bài 03 — @RestController & request mapping: cú pháp
@GetMapping,@PostMapping,@RequestMappingtrên controller — phần "khai báo route" màRequestMappingHandlerMappingscan và index. - Bài 04 — Response handling:
HandlerAdaptersau khi invoke method sẽ gọiReturnValueHandler→MessageConverter. Bài 04 mổ tầng này —ResponseEntity, status code, header tùy chỉnh. - Bài 06 — Validation:
@Valid @RequestBodylà argument được resolve bởiRequestResponseBodyMethodProcessor(đồng thời làHandlerMethodArgumentResolver+HandlerMethodReturnValueHandler). Bài 06 giải thích validation pipeline được kéo vào tại đây.
Tóm tắt
- Pipeline routing:
DispatcherServlet→HandlerMapping.getHandler()→HandlerExecutionChain→HandlerAdapter.handle()→ method invoked. HandlerMappingtra map(URL + HTTP method + consumes + produces) → HandlerMethod. Default:RequestMappingHandlerMappingscan@RequestMappinglúc startup.HandlerAdapterbiết cách invoke handler: resolve arguments (@PathVariable,@RequestBody...), gọi method, convert return value. Default:RequestMappingHandlerAdapter.- Tách hai tầng là Open-Closed: thêm loại handler mới chỉ cần thêm adapter, không sửa
DispatcherServlet. PathPatternParser(Spring 6+): compile pattern một lần lúc startup, match 6-8x nhanh hơnAntPathMatcher. Strict trailing slash —/orders/và/orderskhác nhau.- Ambiguous handler →
IllegalStateExceptiontại startup, không runtime silent wrong route. - Custom argument: implement
HandlerMethodArgumentResolver, register quaWebMvcConfigurer.addArgumentResolvers(). - Diagnose "request không đến handler":
logging.level.org.springframework.web.servlet=TRACE+/actuator/mappings.
Tự kiểm tra
Q1Vì sao DispatcherServlet không tự invoke handler method mà phải delegate qua HandlerAdapter? Giải thích bằng nguyên tắc thiết kế cụ thể.▸
DispatcherServlet không tự invoke handler method mà phải delegate qua HandlerAdapter? Giải thích bằng nguyên tắc thiết kế cụ thể.Spring hỗ trợ nhiều loại handler: @RequestMapping method, HttpRequestHandler interface, Controller interface cổ, Kotlin coroutine, reactive Mono return… Mỗi loại có cách invoke khác nhau — resolve argument khác, gọi khác, convert return value khác.
Nếu DispatcherServlet tự invoke, nó phải chứa if/else instanceof cho từng loại. Thêm loại handler mới = sửa DispatcherServlet — vi phạm Open-Closed Principle (open for extension, closed for modification).
Giải pháp: Adapter pattern. DispatcherServlet chỉ hỏi "adapter nào supports handler này?" rồi gọi ha.handle() — không biết cụ thể invoke ra sao. Thêm loại handler mới chỉ cần thêm HandlerAdapter implement, không sửa dispatcher. Spring đã thêm ReactiveTypeReturnValueHandler, Kotlin coroutine adapter theo đúng cách này.
Q2Request GET /api/orders/abc đến controller có @GetMapping("/api/orders/{id:\\d+}"). Spring trả status code nào và vì sao?▸
GET /api/orders/abc đến controller có @GetMapping("/api/orders/{id:\\d+}"). Spring trả status code nào và vì sao?Spring trả 404 Not Found, không phải 400 Bad Request.
Lý do: regex constraint \d+ trong PathPattern là routing condition — nó quyết định pattern có match request không. abc không match \d+ → pattern không match → RequestMappingHandlerMapping coi như "không có handler cho URL này" → NoHandlerFoundException → 404.
Nếu muốn trả 400 với thông báo rõ ràng, bỏ regex khỏi URL (@GetMapping("/api/orders/{id}")) và validate tại method argument bằng @Validated + @Min(1) hoặc convert exception MethodArgumentTypeMismatchException trong @ExceptionHandler.
Q3Kể 3 bước RequestMappingHandlerAdapter thực hiện sau khi nhận HandlerMethod. Tại sao cần HandlerMethodArgumentResolver riêng thay vì đọc thẳng từ HttpServletRequest?▸
RequestMappingHandlerAdapter thực hiện sau khi nhận HandlerMethod. Tại sao cần HandlerMethodArgumentResolver riêng thay vì đọc thẳng từ HttpServletRequest?3 bước chính:
- Resolve arguments: iterate
HandlerMethodArgumentResolverlist, mỗi resolver xử lý 1 loại argument (@PathVariable,@RequestBody,@RequestParam…). Kết quả là mảngObject[]truyền vào method. - Invoke method: gọi
method.invoke(bean, args)— method controller chạy, trả về Object. - Handle return value:
HandlerMethodReturnValueHandlerxử lý return — serialize thành JSON (quaHttpMessageConverter), set status code, header.
Vì sao cần Resolver riêng: HttpServletRequest chỉ expose raw data (header string, query string, body stream). Adapter phải biết: đây là @PathVariable (parse từ URL đã match) hay @RequestBody (deserialize JSON) hay @RequestParam (parse query string với type conversion)? Mỗi nguồn có logic xử lý khác nhau. Tách ra resolver riêng cho phép extend (custom @CurrentUser resolver) mà không sửa adapter.
Q4PathPatternParser (Spring 6+) cải thiện gì so với AntPathMatcher? Tại sao trailing slash lại là breaking change khi upgrade Boot 3?▸
PathPatternParser (Spring 6+) cải thiện gì so với AntPathMatcher? Tại sao trailing slash lại là breaking change khi upgrade Boot 3?Cải thiện chính:
- Parse một lần lúc startup:
PathPatternParsercompile mỗi pattern thànhPathPatternobject tại khởi động.AntPathMatcherparse string lại mỗi request. Kết quả: 6-8x nhanh hơn trên path phức tạp. - Reject ambiguous tại startup:
AntPathMatchercó thể silent wrong match;PathPatternParserném lỗi ngay — fail fast.
Trailing slash breaking change: Boot 2 (với AntPathMatcher) mặc định treat /orders/ và /orders như nhau — client gửi trailing slash vẫn đến handler. Boot 3 (với PathPatternParser) strict: hai URL khác nhau. App upgrade lên Boot 3 mà client gửi trailing slash sẽ nhận 404 — không báo lỗi compile, không warning runtime. Đây là lý do cần test integration sau upgrade và bật TRACE log để detect.
Q5App có 2 handler: @GetMapping("/orders") và @GetMapping(value="/orders", produces="application/json"). Request GET /orders với Accept: application/json đến — Spring chọn handler nào? Và nếu có Accept: */*?▸
@GetMapping("/orders") và @GetMapping(value="/orders", produces="application/json"). Request GET /orders với Accept: application/json đến — Spring chọn handler nào? Và nếu có Accept: */*?Với Accept: application/json:
Spring dùng quy tắc most specific match. Handler 1 không khai produces → match mọi Accept. Handler 2 khai produces="application/json" → match chính xác application/json → cụ thể hơn. Spring chọn handler 2.
Với Accept: */*:
*/* nghĩa client chấp nhận bất kỳ format. Handler 1 không khai produces → match. Handler 2 khai application/json → cũng match (vì */* bao gồm json). Cả 2 đều match với specificity tie trên produces. Spring có thể ném IllegalStateException ("Ambiguous handler") hoặc chọn theo order đăng ký — đây là trường hợp nên tránh. Best practice: không define 2 handler cùng URL khác nhau chỉ ở produces trừ khi có lý do rõ ràng (API versioning).
Bài tiếp theo: @RestController & request mapping
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