@RestController và @RequestMapping — annotation-driven controller
@RestController = @Controller + @ResponseBody. Bài này bóc 3 generation của controller annotation, mapping pattern (class-level + method-level), 7 HTTP method shortcut, content negotiation qua produces/consumes, và cách MVC route 1 request đến đúng method.
Bài 01 bóc DispatcherServlet infrastructure. Bài này tập trung annotation cấp ứng dụng — @RestController + family — và cách Spring map URL → method ở mức bytecode.
Hiểu sau bài này: vì sao @RestController khác @Controller, khi nào dùng @RequestMapping vs shortcut @GetMapping, content negotiation qua produces/consumes hoạt động ra sao, và pattern URL versioning thực tế.
1. Lịch sử controller annotation — 3 generation
Generation 1 — Spring 1.x/2.x (XML controller)
<bean name="/orders" class="com.olhub.OrderController"/>
Class implement Controller interface với method handleRequest(req, res). Pre-annotation era. Legacy code 2003-2009.
Generation 2 — Spring 2.5+ (@Controller)
@Controller
@RequestMapping("/api/orders")
public class OrderController {
@RequestMapping(method = RequestMethod.GET, value = "/{id}")
@ResponseBody
public OrderDto getOrder(@PathVariable Long id) { ... }
@RequestMapping(method = RequestMethod.POST)
@ResponseBody
public OrderDto create(@RequestBody OrderRequest req) { ... }
}
Improvement: annotation-driven, không XML. Pain: verbose — @ResponseBody lặp mỗi method.
Generation 3 — Spring 4+ (@RestController + shortcut)
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderDto getOrder(@PathVariable Long id) { ... }
@PostMapping
public OrderDto create(@RequestBody OrderRequest req) { ... }
}
Đây là syntax 2026 standard. 2 cải tiến chính:
@RestController=@Controller+@ResponseBodyáp cho mọi method. Bỏ duplicate@ResponseBody.- Shortcut annotation:
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMappingthay@RequestMapping(method = ...).
2. @RestController — bóc tách
@RestController source:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
String value() default "";
}
Là meta-annotation (Module 01 bài 06). Spring scan chain:
@RestController→ kế thừa@Controller→ kế thừa@Component→ component scan tìm thấy.@RestController→ kế thừa@ResponseBody→ áp cho mọi method trả response.
Hệ quả: trong code business, dùng @RestController cho REST API, @Controller cho server-side rendering (Thymeleaf, JSP).
| Scenario | Annotation | Lý do |
|---|---|---|
| REST API trả JSON/XML | @RestController | @ResponseBody áp tất cả method |
| Server-side render với view | @Controller | Method trả String = view name, không phải body |
| Mix REST + view trong 1 class | @Controller + @ResponseBody per method | Hiếm — tránh mix |
3. @RequestMapping — annotation tổng quát
@RequestMapping(
value = "/orders", // URL pattern
method = RequestMethod.GET, // HTTP method
consumes = MediaType.APPLICATION_JSON_VALUE, // Content-Type filter
produces = MediaType.APPLICATION_JSON_VALUE, // Accept filter
headers = "X-API-Version=v2", // header constraint
params = "filter=active" // query param constraint
)
public List<OrderDto> list() { ... }
7 attribute chính. Quy tắc:
value(aliaspath): URL pattern. Có thể array{"/orders", "/api/orders"}.method: array{RequestMethod.GET, RequestMethod.HEAD}cho match nhiều method.consumes: filter request có Content-Type khớp. Mismatch → 415.produces: filter request có Accept khớp. Mismatch → 406.headers: match header cóX-Foo=bar(= chính xác) hoặcX-Foo!=bar(≠).params: match query param cókey=valuehoặckey(chỉ cần present).
@RequestMapping đặt được trên class và method. Class-level apply cho mọi method bên trong:
@RestController
@RequestMapping("/api/v1/orders") // class-level prefix
public class OrderController {
@GetMapping("/{id}") // method-level → /api/v1/orders/{id}
public OrderDto get(@PathVariable Long id) { ... }
@PostMapping // → /api/v1/orders
public OrderDto create(@RequestBody OrderRequest req) { ... }
}
Class-level + method-level concat. Empty method-level → URL = class-level.
4. 7 shortcut annotation
Spring 4.3+ thêm shortcut tương đương @RequestMapping(method = ...):
| Annotation | Tương đương | HTTP method |
|---|---|---|
@GetMapping | @RequestMapping(method = GET) | GET |
@PostMapping | @RequestMapping(method = POST) | POST |
@PutMapping | @RequestMapping(method = PUT) | PUT |
@DeleteMapping | @RequestMapping(method = DELETE) | DELETE |
@PatchMapping | @RequestMapping(method = PATCH) | PATCH |
| (không có) | OPTIONS, HEAD, TRACE | hiếm dùng |
Shortcut accept tất cả attribute của @RequestMapping trừ method (đã fix):
@PostMapping(value = "/orders",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE)
public OrderDto create(@RequestBody OrderRequest req) { ... }
Khuyến nghị: dùng shortcut. @RequestMapping chỉ cần khi method match nhiều HTTP method:
@RequestMapping(value = "/orders", method = {RequestMethod.GET, RequestMethod.HEAD})
public List<OrderDto> list() { ... }
5. URL pattern — PathPattern syntax
Spring 6 default PathPatternParser thay AntPathMatcher. Pattern syntax:
@GetMapping("/orders") // exact
@GetMapping("/orders/{id}") // path variable
@GetMapping("/orders/{id:\\d+}") // path variable + regex constraint
@GetMapping("/orders/{*path}") // capture remaining (multi-segment)
@GetMapping("/api/v?/orders") // single char wildcard
@GetMapping("/api/*/orders") // single segment wildcard
@GetMapping("/files/**") // multi-segment wildcard (legacy AntPathMatcher style)
Modern syntax (Spring 6 PathPatternParser):
| Pattern | Match |
|---|---|
/orders/\{id\} | /orders/42 (single segment) |
/orders/{id:\\d+} | /orders/42 (chỉ digits) |
/orders/\{*path\} | /orders/a/b/c (multi segment) |
/api/\{version\}/orders | /api/v1/orders, /api/v2/orders |
5.1 Quy tắc match khi nhiều handler match
@GetMapping("/orders/{id}") // (a)
@GetMapping("/orders/latest") // (b) — exact
Request /orders/latest:
- (a) match (
\{id\}= "latest"). - (b) match (exact).
Spring chọn most specific = (b). Quy tắc: literal segment > wildcard > regex > path variable.
Pattern thực tế: đặt route exact trước route variable trong cùng class — dễ đọc. Spring tự sort theo specificity, không phụ thuộc thứ tự khai báo.
6. Content negotiation — produces và consumes
6.1 produces — filter theo Accept
@GetMapping(value = "/orders/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
public OrderDto getJson(@PathVariable Long id) { ... }
@GetMapping(value = "/orders/{id}", produces = MediaType.APPLICATION_XML_VALUE)
public OrderDto getXml(@PathVariable Long id) { ... }
Client Accept: application/json → Spring chọn getJson. Client Accept: application/xml → getXml. Không match → 406 Not Acceptable.
6.2 consumes — filter theo Content-Type
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String uploadFile(@RequestParam("file") MultipartFile file) { ... }
@PostMapping(value = "/upload", consumes = MediaType.APPLICATION_JSON_VALUE)
public String uploadJson(@RequestBody UploadRequest req) { ... }
Same URL, same method, khác consumes → Spring chọn theo Content-Type request. Mismatch → 415 Unsupported Media Type.
6.3 API versioning qua Accept
Pattern phổ biến — versioning trong header thay path:
@GetMapping(value = "/orders/{id}", produces = "application/vnd.olhub.v1+json")
public OrderDtoV1 getV1(@PathVariable Long id) { ... }
@GetMapping(value = "/orders/{id}", produces = "application/vnd.olhub.v2+json")
public OrderDtoV2 getV2(@PathVariable Long id) { ... }
Client:
GET /orders/42
Accept: application/vnd.olhub.v2+json
Spring route đến getV2. Đây là media type versioning — RESTful purist style. Path versioning (/api/v1/orders) phổ biến hơn vì dễ debug.
7. Pattern URL versioning thực tế
7.1 Path versioning — phổ biến nhất
@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 { ... }
@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 { ... }
Pros:
- Dễ debug — URL nói rõ version.
- Cache CDN dễ — URL phân biệt.
- Test bằng curl trực tiếp.
Cons:
- Breaking change yêu cầu version mới — class mới (duplicate code).
- URL "không stable" nếu version đổi nhiều.
7.2 Header versioning
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping(value = "/{id}", headers = "X-API-Version=1")
public OrderDtoV1 getV1(@PathVariable Long id) { ... }
@GetMapping(value = "/{id}", headers = "X-API-Version=2")
public OrderDtoV2 getV2(@PathVariable Long id) { ... }
}
Pros:
- URL stable — version trong header.
- Easier to deprecate — change default version qua config.
Cons:
- Test phải set header — không dễ debug.
- CDN cache khó (cần cache by header).
7.3 So sánh nhanh
| Tiêu chí | Path | Header | Media Type |
|---|---|---|---|
| Phổ biến | ✅ | Trung bình | Hiếm |
| Debug dễ | ✅ | ❌ | ❌ |
| Cache CDN | ✅ | Vary header | Vary header |
| RESTful purity | ❌ | ✅ | ✅ |
| Recommended 2026 | ✅ | OK cho internal API | Edge case |
Khoá này dùng path versioning — TaskFlow capstone là /api/v1/.... Chuyển v2 khi cần breaking change.
8. Thứ tự match khi multiple controller có cùng URL
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@GetMapping("/{id}")
public OrderDto get(@PathVariable Long id) { ... }
}
@RestController
@RequestMapping("/api")
public class ApiController {
@GetMapping("/orders/{id}") // CUNG URL!
public Object alternate(@PathVariable Long id) { ... }
}
Spring throw IllegalStateException tại startup:
Ambiguous mapping. Cannot map 'apiController' method ...
to {GET [/api/orders/{id}]}: There is already 'orderController' bean method ...
Không có rule "first wins" — Spring force dev fix bug. Pattern: 1 URL ↔ 1 method.
Workaround: dùng produces/consumes/headers/params để phân biệt. Nhưng tốt nhất — clean URL space, đừng overlap.
9. Default method match — auto OPTIONS, HEAD
Spring tự handle 2 method khác nếu không khai báo:
OPTIONS: trả 200 với headerAllow: GET, POST, ...liệt kê method support cho URL. Boot config quaspring.mvc.dispatch-options-request=true(default true).HEAD: tương đương GET nhưng không return body. Auto-handle nếu có@GetMappingcho cùng URL.
Bạn ít khi viết handler riêng cho OPTIONS/HEAD. CORS preflight (OPTIONS) thường handle qua filter hoặc @CrossOrigin.
10. @CrossOrigin — CORS
@RestController
@RequestMapping("/api/orders")
@CrossOrigin(origins = {"https://olhub.org", "https://app.olhub.org"})
public class OrderController {
@GetMapping("/{id}")
@CrossOrigin // override class-level
public OrderDto get(@PathVariable Long id) { ... }
}
@CrossOrigin set CORS header tự động:
Access-Control-Allow-Origin: <origin>nếu request origin match.Access-Control-Allow-Methods,Access-Control-Allow-Headers.- Pre-flight
OPTIONSrequest handled tự động.
Production: CORS thường config qua filter global (CorsFilter) hoặc Spring Security CorsConfigurationSource thay @CrossOrigin rải rác.
12. Vận hành production — versioning, CORS, IDOR security
REST API public là contract — versioning sai làm break client. Section này cover patterns enterprise.
12.1 4 strategy versioning
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| Path | /api/v1/orders | Visible, cache-friendly | URL pollution |
| Subdomain | v1.api.olhub.org | Clean URL, route DNS | DNS overhead |
| Header | Accept: application/vnd.olhub.v1+json | RESTful purist | Hidden, hard debug |
| Query param | /api/orders?v=1 | Easy add | Cache fragmentation |
Khoá này TaskFlow: Path versioning — phổ biến nhất, RESTful đủ.
@RestController
@RequestMapping("/api/v1/orders")
public class OrderControllerV1 { ... }
@RestController
@RequestMapping("/api/v2/orders")
public class OrderControllerV2 { ... }
12.2 Deprecation header
Khi sunset v1:
@GetMapping("/api/v1/orders")
public ResponseEntity<List<OrderDto>> listV1() {
return ResponseEntity.ok()
.header("Deprecation", "true")
.header("Sunset", "2026-12-31")
.header("Link", "</api/v2/orders>; rel=\"successor-version\"")
.body(service.list());
}
Client tooling monitor Deprecation header → ticket auto-tạo. Spring 6 có @Deprecated annotation tự sinh header (Boot 4.0+).
12.3 CORS production setup
@CrossOrigin rải rác = khó audit. Centralize:
@Configuration
public class CorsConfig {
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOrigins(List.of(
"https://olhub.org",
"https://app.olhub.org"
));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
config.setAllowedHeaders(List.of("Authorization", "Content-Type"));
config.setExposedHeaders(List.of("X-Request-Id"));
config.setMaxAge(3600L);
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", config);
return source;
}
}
Cảnh báo: never setAllowedOrigins(List.of("*")) cùng setAllowCredentials(true) — security violation, browser refuse.
12.4 IDOR — Insecure Direct Object Reference
@GetMapping("/api/orders/{id}")
public OrderDto get(@PathVariable Long id) {
return service.findById(id); // KHONG check user own order!
}
Attacker bruteforce ID → access order khác user. IDOR vulnerability nguy hiểm.
Fix: filter qua user context, trả 404 thay 403 (không leak existence):
@GetMapping("/api/orders/{id}")
public OrderDto get(@PathVariable Long id, @AuthenticationPrincipal User user) {
return service.findByIdForUser(id, user.id())
.orElseThrow(() -> new OrderNotFoundException(id));
}
Spring Security @PreAuthorize (Module 05) tự động enforce ownership.
12.5 Endpoint discovery — Actuator mappings
Production debug tool: /actuator/mappings list mọi endpoint mapped. Useful:
- Audit "endpoint nào chưa có security?"
- Verify deploy mới có route không.
- Find conflicting URL pattern.
Bảo vệ qua management port internal-only + auth.
11. Pitfall tổng hợp
❌ Nhầm 1: Quên @ResponseBody với @Controller.
@Controller
public class C {
@GetMapping("/orders")
public List<OrderDto> list() { ... } // BUG — Spring tim view "OrderDto"
}
✅ Dùng @RestController thay @Controller cho REST API. Hoặc add @ResponseBody.
❌ Nhầm 2: Trùng URL giữa controller. ✅ Spring throw startup error. Refactor — 1 URL → 1 method.
❌ Nhầm 3: Sai type path variable.
@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable String id) { ... } // String thay Long
Đoạn code chạy nhưng convert String → Long cần manual. Nếu id không phải số → NumberFormatException, 400.
✅ Khai đúng type: @PathVariable Long id. Spring auto-convert + validate.
❌ Nhầm 4: Quên consumes cho POST nhận JSON.
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) { ... }
Code work với Content-Type application/json. Nhưng nếu client gửi text/plain hoặc multipart, request vẫn match → Spring throw HttpMessageNotReadableException runtime.
✅ Add consumes = MediaType.APPLICATION_JSON_VALUE để 415 sớm hơn, log clearer.
❌ Nhầm 5: Path pattern trùng class-level + method-level concat sai.
@RestController
@RequestMapping("/api/orders/") // trailing slash
public class C {
@GetMapping("/{id}") // → /api/orders//{id} ?
public ...
}
Spring 5+ tự handle trailing slash (configurable). Nhưng best practice: không trailing slash class-level.
✅ @RequestMapping("/api/orders") — no trailing slash.
❌ Nhầm 6: Mix @RequestMapping với shortcut.
@GetMapping(value = "/orders", method = RequestMethod.POST) // contradict
Compile fail — @GetMapping không có attribute method. Compiler error.
✅ Dùng @RequestMapping nếu cần multiple method, shortcut nếu single.
❌ Nhầm 7: Đặt @RestController mà controller render view.
@RestController
public class C {
@GetMapping("/page")
public String page() { return "home"; } // tra String "home" la BODY, khong phai view name
}
Response: home text content thay HTML view.
✅ Dùng @Controller cho view, @RestController cho REST.
13. 📚 Deep Dive Spring Reference
Spring Framework Reference:
- Spring MVC — @RequestMapping — đầy đủ attribute, pattern, content negotiation.
- Spring MVC — Annotated Controllers — overview controller layer.
- Spring MVC — URI Patterns — PathPattern syntax chi tiết.
- Spring MVC — Content Types —
consumes/produces. - Spring MVC — CORS —
@CrossOrigin+ global config.
Annotation source:
@RestController— meta-annotation source.RequestMappingHandlerMapping— URL routing engine.
API design references:
- Microsoft REST API Guidelines — versioning, naming, status code.
- GitHub REST API — example API design tốt — đọc cách họ đặt URL pattern.
Tool:
/actuator/mappings— runtime list all routes mapped.- IntelliJ "Endpoints" tool window — visualize.
- Postman / Bruno collection — test endpoint nhanh.
Ghi chú: đọc Microsoft REST API Guidelines 1 lần — applicable cho mọi REST API, không chỉ Microsoft. Pattern naming, status code, error format chuẩn industry.
14. Tóm tắt
- 3 generation controller annotation: XML (legacy) →
@Controller+@ResponseBody(Spring 2.5+) →@RestController+ shortcut (Spring 4+). @RestController=@Controller+@ResponseBodyáp tất cả method. Standard cho REST API 2026.@Controllercho server-side rendering (view): method trảString= view name.@RequestMapping7 attribute:value,method,consumes,produces,headers,params,name.- 5 shortcut annotation:
@GetMapping,@PostMapping,@PutMapping,@DeleteMapping,@PatchMapping. Dùng shortcut cho 99% case. - Class-level
@RequestMapping+ method-level concat URL. PathPatternsyntax (Spring 6+):/orders,/orders/\{id\},/orders/\{id:\d+\},/orders/\{*path\},/files/**.- Content negotiation:
producesfilter theoAccept,consumesfilter theoContent-Type. Mismatch → 406/415. - 3 pattern API versioning: path (
/api/v1/) — phổ biến nhất, header (X-API-Version) — clean URL, media type (application/vnd.olhub.v2+json) — RESTful purity. - Trùng URL giữa controller → Spring throw startup error. Force dev fix.
- Spring auto-handle OPTIONS (200 +
Allowheader) và HEAD (GET không body). @CrossOriginset CORS per-controller. Production thường config global qua filter.
15. Tự kiểm tra
Q1Đoạn sau có gì sai? Output là gì?@Controller
public class OrderController {
@GetMapping("/api/orders/{id}")
public OrderDto get(@PathVariable Long id) {
return orderService.findById(id);
}
}
▸
@Controller
public class OrderController {
@GetMapping("/api/orders/{id}")
public OrderDto get(@PathVariable Long id) {
return orderService.findById(id);
}
}Sai: dùng @Controller thay @RestController. Method trả OrderDto không có @ResponseBody → Spring xem OrderDto là view name, lookup ViewResolver để render template tên "OrderDto.html"/"OrderDto.jsp".
Output: tuỳ ViewResolver:
- Có Thymeleaf classpath: lookup
templates/OrderDto.html— không có → 500 với "Could not resolve view". - Không view resolver: 404 hoặc fall back to default error page.
2 cách fix:
- Đổi
@Controller→@RestController:Tự động@RestController public class OrderController { ... }@ResponseBodymọi method → return value serialize JSON. - Add
@ResponseBodymỗi method:Verbose nhưng work.@Controller public class OrderController { @GetMapping("/api/orders/{id}") @ResponseBody public OrderDto get(@PathVariable Long id) { ... } }
Quy tắc: REST API → @RestController. Server-side rendered HTML → @Controller + return view name. Đừng mix.
Q2Vì sao Spring throw startup error khi 2 controller declare cùng URL pattern (giả sử cùng method, không phân biệt qua produces/consumes)? Lý do thiết kế là gì?▸
Lý do thiết kế: fail-fast + force unambiguous mapping.
Nếu Spring "first wins" hoặc "last wins":
- Bug ngầm — dev không biết route nào active.
- Refactor: thêm controller mới có thể accidentally override existing → API broken.
- Test phụ thuộc thứ tự load class — fragile.
Spring chọn force dev resolve ambiguity tại startup:
Ambiguous mapping. Cannot map 'apiController' method
public java.lang.Object com.olhub.ApiController.alternate(java.lang.Long)
to {GET [/api/orders/{id}]}: There is already 'orderController' bean method
public com.olhub.OrderDto com.olhub.OrderController.get(java.lang.Long) mapped.3 cách fix:
- Refactor URL space: 2 endpoint khác URL hoàn toàn — dùng prefix khác (
/api/ordersvs/internal/orders). - Phân biệt qua produces/consumes:Different produces → no ambiguity.
@GetMapping(value = "/{id}", produces = "application/vnd.olhub.v1+json") @GetMapping(value = "/{id}", produces = "application/vnd.olhub.v2+json") - Phân biệt qua params/headers: 1 endpoint với
?detailed=true, 1 không. Hiếm dùng — confusing.
Best practice: URL space cleanup. Mỗi resource có 1 controller, mỗi action 1 method. Versioning qua path (/api/v1/orders) — class riêng, không trùng URL.
Q3Trong 3 cách versioning (path, header, media type), team enterprise thường chọn cái nào? Vì sao?▸
Path versioning (`/api/v1/orders`) — phổ biến nhất, ~80% enterprise dùng.
Lý do:
- Debugging dễ: mở browser, paste URL → thấy ngay version. Curl, Postman, log đều rõ.
- Cache CDN: CDN cache theo URL natively. Header versioning cần "Vary: X-API-Version" — phức tạp config.
- API documentation: Swagger/OpenAPI generate per-path tự nhiên.
/api/v1/ordersvà/api/v2/orderslà 2 path tách biệt — doc không lẫn lộn. - Tool integration: API gateway (Kong, AWS API Gateway) route theo path đơn giản. Header versioning yêu cầu config phức tạp hơn.
- Client code dễ read:
fetch("/api/v2/orders")rõ ràng version, không phụ thuộc header config global.
Trade-off:
- Path versioning không RESTful purity — purist nói "URL identifies resource, không identifies version". Practice không quan tâm.
- Class controller duplicate — v1 và v2 thường share 80% code. Mitigate bằng base class hoặc shared service layer.
Khi nào dùng header versioning:
- Internal API trong organization — ngại expose URL "/v1" public.
- Backwards compat tự động — old client không send header → default version.
- Microsoft Graph API, GitHub API có legacy header versioning support nhưng main là path.
Media type versioning hiếm — chỉ trong domain RESTful purist mạnh (vd Hypermedia API). 99% production app skip.
Khoá này: path versioning. TaskFlow capstone start với /api/v1/.
Q4Đoạn sau xử lý 2 endpoint upload — 1 cho file, 1 cho JSON. Giải thích cơ chế Spring chọn handler.@PostMapping(value = "/upload", consumes = "multipart/form-data")
public String uploadFile(@RequestParam("file") MultipartFile file) { ... }
@PostMapping(value = "/upload", consumes = "application/json")
public String uploadJson(@RequestBody UploadRequest req) { ... }
▸
@PostMapping(value = "/upload", consumes = "multipart/form-data")
public String uploadFile(@RequestParam("file") MultipartFile file) { ... }
@PostMapping(value = "/upload", consumes = "application/json")
public String uploadJson(@RequestBody UploadRequest req) { ... }Spring chọn handler theo Content-Type header của request — qua consumes attribute.
Flow:
RequestMappingHandlerMappingmatch URL/upload+ method POST.- Tìm 2 handler match → invoke
RequestMappingInfo.getMatchingCondition(request). - Check
Content-Typerequest:Content-Type: multipart/form-data; boundary=...→ match handler 1 (uploadFile).Content-Type: application/json→ match handler 2 (uploadJson).Content-Type: text/plain→ không match → 415 Unsupported Media Type.- Không có Content-Type → 415.
- Match → invoke handler đó.
Cơ chế bind argument khác nhau:
- uploadFile (multipart): Spring's
MultipartResolverparse body, exposeMultipartFile.@RequestParam("file")match form field name. - uploadJson:
HttpMessageConverter(Jackson) deserialize JSON body →UploadRequestobject.
Use case thực tế: API hỗ trợ 2 cách upload:
- Web browser upload form: multipart/form-data với input file.
- Programmatic API call: JSON với base64-encoded file content.
Cùng URL, cùng action — khác encoding. consumes cho phép single endpoint serve cả 2.
Best practice: mỗi handler check exact 1 Content-Type. Đừng dùng consumes = b trong 1 method — confusing argument binding.
Q5App có endpoint GET /orders/{id} nhưng team mới muốn add GET /orders/latest trả order mới nhất. Có conflict không? Cơ chế Spring resolve ra sao?▸
GET /orders/{id} nhưng team mới muốn add GET /orders/latest trả order mới nhất. Có conflict không? Cơ chế Spring resolve ra sao?Không conflict. Spring resolve qua most specific match rule.
Request /orders/latest:
/orders/{id}match —id= "latest" (treated as String hoặc fail convert to Long)./orders/latestmatch exact.
Spring chọn handler exact (/orders/latest). Quy tắc specificity (cao đến thấp):
- Literal segment (exact match) —
/orders/latest - Path variable không constraint —
/orders/{id} - Path variable có regex constraint —
/orders/{id:\d+} - Wildcard single —
/orders/* - Wildcard multi —
/orders/**
Specificity decoded từ URL pattern, không phụ thuộc thứ tự khai báo trong code.
Code đầy đủ:
@RestController
@RequestMapping("/orders")
public class OrderController {
@GetMapping("/latest") // Spring chon nay khi URL = /orders/latest
public OrderDto latest() {
return orderService.findLatest();
}
@GetMapping("/{id:\\d+}") // chi match digits — /orders/42 yes, /orders/abc no
public OrderDto get(@PathVariable Long id) {
return orderService.findById(id);
}
}Tại sao thêm regex \d+?
Mặc dù most specific resolve work, regex constraint thêm 1 layer guard:
/orders/abckhông match{id:\d+}→ 404 ngay (clean error).- Không có regex:
/orders/abcmatch{id}, Spring try convert "abc" → Long → fail → 400 (less clean).
Best practice: thêm regex cho path variable có format cố định (UUID, slug, numeric ID).
Q6Bạn deploy REST API public — có nên dùng @CrossOrigin trên controller hay setup CORS global qua filter? Tradeoff?▸
@CrossOrigin trên controller hay setup CORS global qua filter? Tradeoff?Production default: setup CORS global qua filter hoặc Spring Security CorsConfigurationSource.
So sánh 2 approach:
| Aspect | @CrossOrigin per-controller | Global CORS filter |
|---|---|---|
| Setup overhead | Add annotation 1 dòng | 1 config class |
| Consistency | Dễ miss controller mới | Apply mọi endpoint tự động |
| Centralized control | Rải rác 50 controller | 1 chỗ, dễ audit |
| Per-endpoint override | Có (override class-level) | Khó hơn — phải config exception |
| Spring Security tích hợp | Phải config riêng SS CORS | SS tích hợp tự nhiên |
| Test endpoint | Trong test slice | Phải @SpringBootTest full |
Setup global recommended (Boot 3.4):
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration cfg = new CorsConfiguration();
cfg.setAllowedOrigins(List.of("https://olhub.org", "https://app.olhub.org"));
cfg.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
cfg.setAllowedHeaders(List.of("Authorization", "Content-Type", "X-Request-Id"));
cfg.setExposedHeaders(List.of("X-Request-Id", "X-Total-Count"));
cfg.setAllowCredentials(true);
cfg.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/api/**", cfg);
return new CorsFilter(source);
}
}Hoặc qua Spring Security:
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource cors) {
http.cors(c -> c.configurationSource(cors));
return http.build();
}Khi dùng @CrossOrigin:
- Prototype/internal tool — quick setup.
- 1-2 controller cần CORS đặc biệt khác global config.
- Test slice —
@WebMvcTestkhông pull global filter.
Pitfall: mix global filter + @CrossOrigin → confusing. Pick 1 approach, document rõ.
Production CORS critical security: set AllowedOrigins explicit list, không "*". AllowCredentials=true + "*" origin = security violation (browser reject).
Bài tiếp theo: Request binding — @PathVariable, @RequestParam, @RequestBody, @Valid
Bài này có giúp bạn hiểu bản chất không?
Bình luận (0)
Đang tải...