Spring REST API & Data JPA
8/46
Bài 8 / 46~12 phútRequest & ResponseLộ trình · chặng 15/21Miễn phí… lượt xem

Request Binding: Ánh Xạ Dữ Liệu HTTP Vào Spring Controller

Khám phá 6 nguồn dữ liệu trong HTTP request, cơ chế binding của Spring qua @PathVariable, @RequestParam, @RequestBody và cạm bẫy validation.

TL;DR: Dữ liệu trong một HTTP request có thể đến từ 6 vị trí: đường dẫn URL, chuỗi query, thân request, tiêu đề header, cookie và thuộc tính nội bộ. Spring MVC cung cấp 6 annotation tương ứng để giải nén dữ liệu: @PathVariable, @RequestParam, @RequestBody, @RequestHeader, @CookieValue và @RequestAttribute. Riêng @RequestBody sử dụng HttpMessageConverter (mặc định là Jackson) để giải tuần tự hóa luồng byte thành Java Record. Quên đính kèm @Valid trước @RequestBody là lỗi thầm lặng nguy hiểm nhất khiến dữ liệu không hợp lệ lọt thẳng vào tầng nghiệp vụ.

6 vị trí chứa dữ liệu trong một HTTP request gồm những gì?

Một HTTP request mang theo dữ liệu phân tán ở nhiều phân vùng cấu trúc:

Plain Text
POST /orders/42?source=mobile HTTP/1.1
Host: api.olhub.vn
Authorization: Bearer eyJhbG...
Cookie: session_id=xyz789
Content-Type: application/json

{"total": 150.00, "note": "Giao giờ hành chính"}

Spring MVC thiết kế 6 annotation chuyên biệt tương ứng:

  • @PathVariable: Trích xuất định danh từ phân đoạn URL (/orders/{id}).
  • @RequestParam: Trích xuất tham số từ query string (?source=mobile) hoặc form data.
  • @RequestBody: Giải mã toàn bộ thân request ({"total": 150.00}).
  • @RequestHeader: Đọc các tiêu đề HTTP (Authorization, User-Agent).
  • @CookieValue: Đọc giá trị cookie gửi kèm từ trình duyệt.
  • @RequestAttribute: Nhận đối tượng trung gian được thiết lập trước đó bởi Filter hoặc Interceptor.

Việc khai báo tường minh giúp mã nguồn controller độc lập hoàn toàn với việc bóc tách chuỗi thô từ HttpServletRequest.

Cách trích xuất dữ liệu từ URL path segment bằng @PathVariable?

Biến đường dẫn được sử dụng khi giá trị đóng vai trò là định danh duy nhất của tài nguyên:

Java
@GetMapping("/orders/{id}")
public OrderDto getOrder(@PathVariable Long id) {
    return orderService.findById(id);
}

Spring tự động trích xuất chuỗi từ URL template {id} và chuyển đổi sang kiểu Long. Nếu client truyền giá trị không thể chuyển đổi (ví dụ: /orders/abc), hệ thống ném MethodArgumentTypeMismatchException và trả về mã lỗi 400 Bad Request.

Trường hợp tên biến Java khác với tên trong URL, bạn chỉ định tường minh:

Java
@GetMapping("/orders/{order_id}")
public OrderDto getOrder(@PathVariable("order_id") Long orderId) {
    return orderService.findById(orderId);
}

Khi nào nên dùng @RequestParam để phân tách query string?

Ngược lại với path variable, query parameters đóng vai trò là bộ lọc, sắp xếp hoặc phân trang cho tài nguyên:

Java
@GetMapping("/orders")
public List<OrderDto> searchOrders(
    @RequestParam(defaultValue = "ACTIVE") String status,
    @RequestParam(required = false) Long customerId,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size
) {
    return orderService.search(status, customerId, page, size);
}

Một số quy tắc cần lưu ý:

  • required = false: Tham số có thể vắng mặt. Khi thiếu, biến sẽ nhận giá trị null (chỉ áp dụng cho kiểu đối tượng Wrapper như Long, không dùng cho kiểu nguyên thủy long).
  • defaultValue: Cung cấp giá trị mặc định dạng chuỗi nếu request không truyền tham số.
  • Hỗ trợ mảng: Khai báo @RequestParam List<String> tags tự động gom các tham số lặp lại (?tags=java&tags=spring) thành một danh sách.

@RequestBody tuần tự hóa dữ liệu JSON qua HttpMessageConverter ra sao?

Khác với query string vốn là chuỗi ký tự sẵn có, thân request (Body) là một luồng byte nhị phân thô cần được phân tích cú pháp:

Hai cột đối chiếu: query param đọc ra đã là chuỗi nên chỉ cần ConversionService đổi kiểu, còn body là byte thô nên phải đọc header Content-Type rồi hỏi từng converter canRead trước khi MappingJackson2 dựng được object

  1. RequestMappingHandlerAdapter phát hiện tham số mang annotation @RequestBody.
  2. Đọc tiêu đề Content-Type của request (ví dụ: application/json).
  3. Duyệt danh sách các HttpMessageConverter để tìm kiếm bộ chuyển đổi phù hợp (MappingJackson2HttpMessageConverter).
  4. Sử dụng Jackson ObjectMapper để giải tuần tự hóa luồng byte thành đối tượng Java Record.

Java Record là cấu trúc dữ liệu chuẩn mực nhất cho DTO đầu vào:

Java
public record CreateOrderRequest(
    @NotBlank String customer,
    @NotNull @Positive BigDecimal total
) {}

Bẫy sai lầm nghiêm trọng nhất: Nếu bạn không đặt @Valid trước @RequestBody, Spring sẽ nạp dữ liệu vào DTO nhưng hoàn toàn bỏ qua việc kiểm tra ràng buộc. Dữ liệu rỗng vẫn đi thẳng vào tầng service:

Cùng một body sai đi vào hai nhánh: nhánh không có @Valid bind xong rồi gọi thẳng service với dữ liệu sai, nhánh có @Valid bind y hệt rồi thêm bước WebDataBinder validate và ném MethodArgumentNotValidException trả 400

Java
// Correct: Always combine @Valid with @RequestBody
@PostMapping("/orders")
public OrderDto createOrder(@Valid @RequestBody CreateOrderRequest req) {
    return orderService.create(req);
}

Làm thế nào để khai thác @RequestHeader, @CookieValue và @RequestAttribute?

Ba annotation bổ trợ giúp tiếp cận các phân vùng siêu dữ liệu của request:

Annotation @RequestHeader đọc các thông tin kỹ thuật như mã xác thực hoặc định danh truy vết:

Java
@GetMapping("/orders/{id}")
public OrderDto getOrder(
    @PathVariable Long id,
    @RequestHeader("Authorization") String authToken,
    @RequestHeader(value = "X-Request-Id", required = false) String requestId
) {
    return orderService.findById(id);
}

Annotation @CookieValue trích xuất thông tin phiên hoặc tùy chọn người dùng:

Java
@GetMapping("/settings")
public UserSettings getSettings(
    @CookieValue(value = "theme", defaultValue = "dark") String theme
) {
    return userSettingsService.getByTheme(theme);
}

Annotation @RequestAttribute nhận đối tượng đã được tiền xử lý bởi Filter:

Java
@GetMapping("/profile")
public UserProfile getProfile(@RequestAttribute("currentUser") UserPrincipal user) {
    return profileService.getProfile(user.id());
}

HandlerMethodArgumentResolver vận hành ở tầng thấp như thế nào?

Cơ chế giải nén tham số của Spring MVC không sử dụng reflection cứng nhắc mà được thiết kế linh hoạt qua interface HandlerMethodArgumentResolver:

Java
public interface HandlerMethodArgumentResolver {
    boolean supportsParameter(MethodParameter parameter);
    Object resolveArgument(MethodParameter parameter,
                           ModelAndViewContainer mavContainer,
                           NativeWebRequest webRequest,
                           WebDataBinderFactory binderFactory) throws Exception;
}

Mỗi annotation tương ứng với một resolver riêng:

  • @PathVariable -> PathVariableMethodArgumentResolver
  • @RequestParam -> RequestParamMethodArgumentResolver
  • @RequestBody -> RequestResponseBodyMethodProcessor

Khi chuẩn bị gọi phương thức, adapter duyệt tuần tự qua danh sách resolver. Bộ phân giải đầu tiên trả về true cho hàm supportsParameter() sẽ được trao quyền trích xuất dữ liệu.

Những lỗi sai kinh điển nào thường gặp khi binding dữ liệu?

Dưới đây là các cạm bẫy cần loại bỏ trong quá trình lập trình:

  1. Quên @Valid trước @RequestBody: Khiến các annotation kiểm tra như @NotNull, @NotBlank hoàn toàn mất tác dụng mà không có cảnh báo nào.
  2. Dùng kiểu nguyên thủy long hoặc int với required = false: Khi client không truyền tham số, Spring cố gắng gán null vào kiểu nguyên thủy gây lỗi runtime. Luôn dùng kiểu Wrapper Long, Integer.
  3. Gửi Request Body trong phương thức GET: Mặc dù chuẩn HTTP không cấm tuyệt đối, hầu hết các proxy và thư viện client đều xóa bỏ body của request GET. Luôn dùng query parameter cho thao tác tìm kiếm.
  4. Tên biến Java lệch với tên tham số query: Khai báo @RequestParam Long min nhưng client truyền ?minTotal=100 khiến biến nhận giá trị null.

Tóm tắt

  • Spring MVC ánh xạ 6 phân vùng dữ liệu của HTTP request thành 6 annotation tương ứng.
  • Phân biệt rõ: Path variable dùng để định danh tài nguyên; Query parameter dùng để bổ trợ lọc và phân trang.
  • @RequestBody kết hợp cùng HttpMessageConverter để chuyển đổi luồng byte thành Java Record.
  • Luôn đặt @Valid đi kèm @RequestBody để đảm bảo cơ chế kiểm tra tính hợp lệ được kích hoạt sớm.
  • Mỗi tham số được phân giải độc lập thông qua chuỗi các HandlerMethodArgumentResolver.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Một phương thức controller khai báo: public UserDto create(@RequestBody CreateUserRequest req). Trong DTO có gắn @NotBlank String username. Nếu client gửi username rỗng, điều gì sẽ xảy ra?
  2. Q2
    Tại sao @RequestBody cần sự hỗ trợ của HttpMessageConverter (như Jackson) trong khi @RequestParam thì không?
  3. Q3
    Theo nguyên chuẩn RESTful, khi nào nên sử dụng @PathVariable thay vì @RequestParam?
  4. Q4
    Nếu khai báo @RequestParam(required = false) int page và client không truyền tham số page, điều gì sẽ xảy ra?
  5. Q5
    Thành phần cốt lõi nào trong Spring MVC chịu trách nhiệm trích xuất dữ liệu từ HTTP request và nạp vào từng tham số của phương thức controller?

Bài tiếp theo: HandlerMethodArgumentResolver & Type Conversion

Bài này có giúp bạn hiểu bản chất không?

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.

Lộ trình Junior Java Backend Developer

Chặng 15/21 · REST API với Spring MVC — bài 8/21 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

ArgumentResolver: Chuỗi Phân Giải Tham Số Spring MVC