Spring Boot/@RestController và @RequestMapping — annotation-driven controller
~22 phútREST API với Spring MVCMiễn phí

@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, @PatchMapping thay @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 "";
}

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).

ScenarioAnnotationLý do
REST API trả JSON/XML@RestController@ResponseBody áp tất cả method
Server-side render với view@ControllerMethod trả String = view name, không phải body
Mix REST + view trong 1 class@Controller + @ResponseBody per methodHiế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 (alias path): 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ặc X-Foo!=bar (≠).
  • params: match query param có key=value hoặc key (chỉ cần present).

@RequestMapping đặt được trên class 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 = ...):

AnnotationTương đươngHTTP 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, TRACEhiế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):

PatternMatch
/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 — producesconsumes

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/xmlgetXml. 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íPathHeaderMedia Type
Phổ biếnTrung bìnhHiếm
Debug dễ
Cache CDNVary headerVary header
RESTful purity
Recommended 2026OK cho internal APIEdge 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 header Allow: GET, POST, ... liệt kê method support cho URL. Boot config qua spring.mvc.dispatch-options-request=true (default true).
  • HEAD: tương đương GET nhưng không return body. Auto-handle nếu có @GetMapping cho 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 OPTIONS request 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

StrategyExampleProsCons
Path/api/v1/ordersVisible, cache-friendlyURL pollution
Subdomainv1.api.olhub.orgClean URL, route DNSDNS overhead
HeaderAccept: application/vnd.olhub.v1+jsonRESTful puristHidden, hard debug
Query param/api/orders?v=1Easy addCache 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 StringLong 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

📚 Tài liệu chính chủ

Spring Framework Reference:

Annotation source:

API design references:

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.
  • @Controller cho server-side rendering (view): method trả String = view name.
  • @RequestMapping 7 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.
  • PathPattern syntax (Spring 6+): /orders, /orders/\{id\}, /orders/\{id:\d+\}, /orders/\{*path\}, /files/**.
  • Content negotiation: produces filter theo Accept, consumes filter theo Content-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 + Allow header) và HEAD (GET không body).
  • @CrossOrigin set CORS per-controller. Production thường config global qua filter.

15. Tự kiểm tra

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);
  }
}

Sai: dùng @Controller thay @RestController. Method trả OrderDto không có @ResponseBody → Spring xem OrderDtoview 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:

  1. Đổi @Controller@RestController:
    @RestController
    public class OrderController { ... }
    Tự động @ResponseBody mọi method → return value serialize JSON.
  2. Add @ResponseBody mỗi method:
    @Controller
    public class OrderController {
      @GetMapping("/api/orders/{id}")
      @ResponseBody
      public OrderDto get(@PathVariable Long id) { ... }
    }
    Verbose nhưng work.

Quy tắc: REST API → @RestController. Server-side rendered HTML → @Controller + return view name. Đừng mix.

Q2
Vì 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:

  1. Refactor URL space: 2 endpoint khác URL hoàn toàn — dùng prefix khác (/api/orders vs /internal/orders).
  2. Phân biệt qua produces/consumes:
    @GetMapping(value = "/{id}", produces = "application/vnd.olhub.v1+json")
    @GetMapping(value = "/{id}", produces = "application/vnd.olhub.v2+json")
    Different produces → no ambiguity.
  3. 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.

Q3
Trong 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/orders/api/v2/orders là 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) { ... }

Spring chọn handler theo Content-Type header của request — qua consumes attribute.

Flow:

  1. RequestMappingHandlerMapping match URL /upload + method POST.
  2. Tìm 2 handler match → invoke RequestMappingInfo.getMatchingCondition(request).
  3. Check Content-Type request:
    • 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.
  4. Match → invoke handler đó.

Cơ chế bind argument khác nhau:

  • uploadFile (multipart): Spring's MultipartResolver parse body, expose MultipartFile. @RequestParam("file") match form field name.
  • uploadJson: HttpMessageConverter (Jackson) deserialize JSON body → UploadRequest object.

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.

Q5
App 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?

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/latest match exact.

Spring chọn handler exact (/orders/latest). Quy tắc specificity (cao đến thấp):

  1. Literal segment (exact match) — /orders/latest
  2. Path variable không constraint — /orders/{id}
  3. Path variable có regex constraint — /orders/{id:\d+}
  4. Wildcard single — /orders/*
  5. 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/abc không match {id:\d+} → 404 ngay (clean error).
  • Không có regex: /orders/abc match {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).

Q6
Bạn deploy REST API public — có nên dùng @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-controllerGlobal CORS filter
Setup overheadAdd annotation 1 dòng1 config class
ConsistencyDễ miss controller mớiApply mọi endpoint tự động
Centralized controlRải rác 50 controller1 chỗ, dễ audit
Per-endpoint overrideCó (override class-level)Khó hơn — phải config exception
Spring Security tích hợpPhải config riêng SS CORSSS tích hợp tự nhiên
Test endpointTrong test slicePhả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 — @WebMvcTest khô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...