Spring REST API & Data JPA/@RestController & Request Mapping — controller annotation-driven
4/46
Bài 4 / 46~12 phútSpring MVC CoreMiễn phí lượt xem

@RestController & Request Mapping — controller annotation-driven

@RestController = @Controller + @ResponseBody. Bài này giải thích tại sao @ResponseBody tách ra thành meta-annotation, cách @RequestMapping + 7 shortcut map URL đến handler method, và PathPattern syntax Spring 6.

TL;DR: @RestController là meta-annotation gộp @Controller + @ResponseBody — áp @ResponseBody cho mọi method trong class, loại bỏ duplicate annotation từng method. @RequestMapping là annotation gốc 7 attribute (value, method, consumes, produces, headers, params, name); 5 shortcut @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping thu gọn cho 99% case. Spring 6 dùng PathPatternParser thay AntPathMatcher — hỗ trợ path variable {id}, regex constraint {id:\d+}, capture-remaining {*path}. Pitfall cốt lõi: dùng @Controller thay @RestController cho REST API khiến return value bị lookup ViewResolver thay serialize JSON — 500 tại runtime.

Bài trước (URL routing & DispatcherServlet) bóc cơ sở hạ tầng DispatcherServlet + HandlerMapping. Bài này đi xuống một tầng ứng dụng: annotation mà bạn viết để khai báo controller và route.

1. Lịch sử controller annotation — 3 generation

Hiểu lịch sử giải thích vì sao @RestController tồn tại và tại sao nó không thể đơn giản hóa hơn nữa.

Generation 1 — Spring 1.x/2.x (XML mapping)

<bean name="/orders" class="com.olhub.OrderController"/>

Class implement interface Controller với method duy nhất handleRequest(HttpServletRequest, HttpServletResponse). Mapping URL trong XML — mọi thứ đều verbose. Legacy code 2003-2009; không còn gặp trong code mới.

Generation 2 — Spring 2.5+ (@Controller + @ResponseBody)

@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) { ... }
}

Cải thiện lớn: annotation-driven, không XML. Nhưng pain point rõ ràng: @ResponseBody phải lặp ở mỗi method. Nếu class có 10 method, phải viết 10 lần.

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 chuẩn 2026. Hai thay đổi trọng tâm:

  1. @RestController gộp @ResponseBody áp cho toàn bộ class — không còn lặp.
  2. Shortcut @GetMapping, @PostMapping,... thay thế @RequestMapping(method = ...) dài dòng.

2. @RestController — tại sao cần @ResponseBody

Để hiểu @RestController, cần hiểu trước tại sao @ResponseBody tồn tại và bản chất của nó.

@ResponseBody làm gì

Khi một handler method trả về object Java (vd OrderDto), Spring MVC có hai lựa chọn:

  1. Không có @ResponseBody: Spring xem return value là tên view — tìm template OrderDto.html, OrderDto.jsp qua ViewResolver. Đây là hành vi mặc định cho server-side rendering (Thymeleaf, JSP).
  2. @ResponseBody: Spring bypass ViewResolver, gọi HttpMessageConverter (thường là Jackson) để serialize object thành JSON/XML rồi viết thẳng vào HTTP response body.

Tóm gọn: @ResponseBody = "serialize trực tiếp vào body, không qua view".

Tại sao chọn serialize trực tiếp

Trước khi REST API phổ biến (pre-2010), Spring MVC dùng chủ yếu để render HTML qua ViewResolver. @ResponseBody được thêm vào sau khi developer cần trả JSON/XML cho AJAX và mobile client — một "escape hatch" khỏi View layer.

Khi REST API trở thành default (post-2015), cần thiết kế annotation thể hiện intent toàn class: "class này là REST controller — mọi method đều serialize". Đó là lý do @RestController ra đời ở Spring 4.

@RestController source

// org/springframework/web/bind/annotation/RestController.java (rut gon)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    String value() default "";
}

@RestControllermeta-annotation — annotation tổng hợp từ 2 annotation khác. Spring xử lý meta-annotation theo chain:

  • @RestController kế thừa @Controller, vốn kế thừa @Component — nhờ vậy component scan tìm thấy class và đăng ký bean.
  • @RestController kế thừa @ResponseBody, và annotation này áp cho mọi method trong class.

Không có "magic" nào khác. Bổ sung @ResponseBody một lần trên class thay vì N lần trên N method — đó là toàn bộ giá trị.

Giá trị đó chỉ lộ ra khi nhìn điều gì xảy ra lúc thiếu @ResponseBody. Cùng một dòng return orderDto, hai kết cục hoàn toàn khác nhau:

Hai cột đối chiếu cùng một dòng return OrderDto: cột có @ResponseBody bỏ qua ViewResolver, đi qua HttpMessageConverter Jackson và trả 200 kèm body JSON; cột quên @ResponseBody coi giá trị trả về là tên view, ViewResolver đi tìm template OrderDto.html và kết thúc bằng lỗi 500

Bên phải chính là pitfall 1 ở phần 7 — và là lý do @RestController đáng dùng: dán một lần trên class thì không method nào còn cơ hội bị quên.

Khi nào dùng @Controller vs @RestController

ScenarioAnnotationLý do
REST API trả JSON/XML@RestController@ResponseBody áp tất cả method
Server-side render HTML (Thymeleaf)@ControllerMethod trả String = view name
Mix REST method + view method trong 1 class@Controller + @ResponseBody từng methodHiếm, tránh mix nếu được

3. @RequestMapping — annotation gốc

@RequestMapping là annotation tổng quát, có thể đặt trên class (class-level prefix) hoặc method (method-level pattern).

@RequestMapping(
    value    = "/orders",                            // URL pattern; alias: path
    method   = RequestMethod.GET,                    // HTTP method filter
    consumes = MediaType.APPLICATION_JSON_VALUE,     // Content-Type filter
    produces = MediaType.APPLICATION_JSON_VALUE,     // Accept header filter
    headers  = "X-API-Version=2",                   // request header constraint
    params   = "filter=active"                       // query param constraint
)
public List<OrderDto> list() { ... }

7 attribute chính và hành vi của từng attribute:

AttributeMô tảMismatch HTTP status
value (alias path)URL pattern, có thể là array
methodArray RequestMethod405 Method Not Allowed
consumesFilter theo Content-Type request415 Unsupported Media Type
producesFilter theo Accept header request406 Not Acceptable
headersHeader constraint Key=Value hoặc !Key400 / no match
paramsQuery param constraint key=value hoặc !keyno match
nameTên để lookup URL qua MvcUriComponentsBuilder

Class-level + method-level concat

@RestController
@RequestMapping("/api/v1/orders")           // class-level prefix
public class OrderController {

    @GetMapping("/{id}")                    // concat -> /api/v1/orders/{id}
    public OrderDto get(@PathVariable Long id) { ... }

    @PostMapping                            // concat -> /api/v1/orders
    public OrderDto create(@RequestBody OrderRequest req) { ... }

    @GetMapping                             // concat -> /api/v1/orders
    public List<OrderDto> list() { ... }
}

Class-level và method-level cộng lại thành URL cuối. Khi method-level để rỗng, URL cuối chính là class-level prefix.

4. 7 shortcut annotation — tại sao cần

Spring 4.3 thêm 5 shortcut tương đương @RequestMapping(method = ...):

ShortcutTương đươngHTTP methodSafeIdempotent
@GetMapping@RequestMapping(method = GET)GET
@PostMapping@RequestMapping(method = POST)POSTKhôngKhông
@PutMapping@RequestMapping(method = PUT)PUTKhông
@DeleteMapping@RequestMapping(method = DELETE)DELETEKhông
@PatchMapping@RequestMapping(method = PATCH)PATCHKhôngKhông

Safe = không thay đổi state server. Idempotent = gọi nhiều lần kết quả giống gọi một lần.

Tại sao shortcut cần thiết (không chỉ là syntactic sugar)

Ngoài việc giảm verbosity, shortcut annotation ràng buộc intent tại compile time. Khi viết @GetMapping, không thể vô tình truyền method = RequestMethod.POST vào — attribute method không tồn tại trong shortcut. Lỗi này được bắt bởi compiler, không phải runtime.

// Compile error -- @GetMapping khong co attribute method:
@GetMapping(value = "/orders", method = RequestMethod.POST)

// OK -- @RequestMapping dung khi can nhieu HTTP method:
@RequestMapping(value = "/orders", method = {RequestMethod.GET, RequestMethod.HEAD})

Shortcut accept tất cả attribute @RequestMapping trừ method:

@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 cho 99% case. Dùng @RequestMapping trực tiếp chỉ khi cần match nhiều HTTP method cùng URL.

5. PathPattern syntax — URL matching Spring 6

Spring 6 thay AntPathMatcher bằng PathPatternParser làm default. PathPatternParser parse pattern tại startup (compile-time), không phải per-request — nhanh hơn và strict hơn về cú pháp.

Các pattern cơ bản

@GetMapping("/orders")              // exact -- chi match /orders
@GetMapping("/orders/{id}")         // path variable -- /orders/42, /orders/abc
@GetMapping("/orders/{id:\\d+}")    // path variable + regex -- chi digits: /orders/42
@GetMapping("/orders/{*path}")      // capture remaining -- /orders/a/b/c
@GetMapping("/api/v?/orders")       // single char wildcard -- /api/v1/orders, /api/v2/orders
@GetMapping("/api/*/orders")        // single segment wildcard -- /api/v1/orders (1 segment)
@GetMapping("/files/**")            // multi-segment wildcard -- /files/a/b/c
PatternMatchesKhong match
"/orders/{id}"/orders/42, /orders/abc/orders/42/items
"/orders/{id:\\d+}"/orders/42/orders/abc
"/orders/{*path}"/orders/a, /orders/a/b/c/orders
"/api/v?/orders"/api/v1/orders, /api/v2/orders/api/v10/orders

Most-specific match khi nhiều pattern trùng

@GetMapping("/orders/latest")   // (A) -- exact
@GetMapping("/orders/{id}")     // (B) -- path variable
@GetMapping("/orders/{id:\\d+}") // (C) -- regex constraint

Request GET /orders/latest:

  • (A) match exact.
  • (B) match với id = "latest".

Spring chọn most specific = (A). Thứ tự specificity (cao xuống thấp): literal segment > regex constraint > path variable > single wildcard > multi wildcard. Bài Content negotiation & versioning có sơ đồ thả một URL thật vào cả năm bậc để thấy từng bậc khớp hay không.

Spring sort theo specificity tại startup, không phụ thuộc thứ tự khai báo trong code.

6. Cơ chế bên dưới — request đến handler

Để hiểu toàn bộ luồng, cần biết HandlerMapping đóng vai trò gì khi matching annotation.

Khi @RestController được scan vào container, RequestMappingHandlerMapping đọc tất cả annotation trên class và method, build một registry — map từ RequestMappingInfo (URL pattern + method + consumes + produces + headers + params) đến handler method. Đây là bước startup, không phải per-request.

Khi request đến:

  1. DispatcherServlet hỏi RequestMappingHandlerMapping: "request này match handler nào?"
  2. HandlerMapping tra registry, chạy specificity ranking nếu nhiều handler match.
  3. Handler method được invoke, return value đi qua HttpMessageConverter nếu có @ResponseBody.

Hai pha thời gian: lúc khởi động Spring quét bean có @RestController, đọc @RequestMapping trên class lẫn method, dựng registry RequestMappingInfo tới HandlerMethod, và ném IllegalStateException nếu hai handler trùng key; mỗi request chỉ tra registry, xếp hạng specificity rồi trả HandlerExecutionChain

Cột trái chạy đúng một lần rồi đóng băng — đây là lý do trùng URL throw IllegalStateException lúc khởi động, không phải lúc request đến. Cũng vì thế không có cách nào thêm route khi app đang chạy.

7. Pitfall

Nhầm 1 — Dùng @Controller cho REST API:

@Controller
public class OrderController {
    @GetMapping("/api/orders/{id}")
    public OrderDto get(@PathVariable Long id) {
        return orderService.findById(id);   // BUG: Spring tim view "OrderDto"
    }
}

Spring xem OrderDto là view name và đem hỏi ViewResolver; không template nào tên đó tồn tại nên request kết thúc bằng 500. Fix: đổi @Controller thành @RestController.

Nhầm 2 — Trùng URL giữa controller:

@RestController @RequestMapping("/api/orders")
public class OrderController {
    @GetMapping("/{id}") public OrderDto get(...) { ... }
}

@RestController @RequestMapping("/api")
public class ApiController {
    @GetMapping("/orders/{id}") public Object alt(...) { ... }  // trung URL!
}

Spring throw IllegalStateException tại startup. Không có rule "first wins". Fix: refactor URL space hoặc phân biệt qua produces/consumes.

Nhầm 3 — Sai type cho path variable:

@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable String id) { ... }  // String thay Long

Code chạy nhưng cần convert manual. Nếu id không phải số, convert thủ công throw NumberFormatException và client nhận 400. Fix: khai đúng type @PathVariable Long id để Spring auto-convert.

Nhầm 4 — Trailing slash trong class-level mapping:

@RequestMapping("/api/orders/")   // trailing slash
public class OrderController {
    @GetMapping("/{id}")           // -> /api/orders//{id} double slash

Spring 6 strict hơn với double slash. Fix: bỏ trailing slash ở class-level: @RequestMapping("/api/orders").

Nhầm 5 — Truyền method vào shortcut annotation:

@GetMapping(value = "/orders", method = RequestMethod.POST)  // compile error

Shortcut không có attribute method. Dùng @RequestMapping nếu cần nhiều method.

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

  • URL routing & DispatcherServlet: bài trước giải thích DispatcherServlet + HandlerMapping nhận request và tra cứu handler — bài này đi vào annotation bạn dùng để khai báo handler trong registry đó.
  • Content negotiation & versioning: bài tiếp theo đào sâu produces/consumes, chiến lược versioning (path/header/media type), và Accept negotiation — bài này chỉ giới thiệu attribute, bài sau giải thích cơ chế chi tiết.

Tóm tắt

  • @ResponseBody báo Spring serialize return value trực tiếp vào HTTP response body qua HttpMessageConverter, bypass ViewResolver. Thiếu nó, Spring xem return value là view name và request fail 500.
  • @RestController = @Controller + @ResponseBody áp toàn class — meta-annotation loại bỏ duplicate @ResponseBody mỗi method. Standard cho REST API từ Spring 4.
  • @RequestMapping có 7 attribute; @GetMapping/@PostMapping/@PutMapping/@DeleteMapping/@PatchMapping là shortcut fix HTTP method tại compile time.
  • Class-level @RequestMapping đặt prefix; method-level concat thêm suffix.
  • PathPatternParser (Spring 6 default): {id} path variable, {id:\d+} regex, {*path} capture-remaining, ** multi-segment wildcard.
  • Khi nhiều handler match cùng URL, Spring chọn most specific (literal trước regex, regex trước path var, path var trước wildcard). Literal segment luôn thắng.
  • Registry build tại startup, nên trùng URL gây IllegalStateException ngay khi boot, không phải lúc request đến.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Vì sao Spring 4 đưa ra @RestController thay vì để developer tiếp tục dùng @Controller + @ResponseBody mỗi method? Cơ chế bên dưới thay đổi gì?
  2. Q2
    Đoạn sau có gì sai? Output khi gọi GET /api/orders/42 là gì?
    @Controller
    public class OrderController {
      @GetMapping("/api/orders/{id}")
      public OrderDto get(@PathVariable Long id) {
          return orderService.findById(id);
      }
    }
  3. Q3
    App đã có GET /orders/{id}. Team muốn thêm GET /orders/latest trả order mới nhất. Hai route có conflict không? Spring chọn handler theo cơ chế nào?
  4. Q4
    Vì sao Spring throw IllegalStateException tại startup khi có 2 controller map cùng URL + method, thay vì throw lúc request đến URL đó lần đầu?
  5. Q5
    Giải thích sự khác nhau giữa consumesproduces trong @RequestMapping. Mỗi attribute filter dựa trên header nào của request và trả HTTP status gì khi mismatch?

Bài tiếp theo: Content negotiation, versioning & CORS

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

Content Negotiation, API Versioning, và CORS — Spring MVC routing nâng cao