Spring Boot/Request binding — @PathVariable, @RequestParam, @RequestBody, @Valid
~24 phútREST API với Spring MVCMiễn phí

Request binding — @PathVariable, @RequestParam, @RequestBody, @Valid

Spring MVC tự bind dữ liệu từ HTTP request vào parameter Java. Bài này bóc 6 source binding (path, query, body, header, cookie, session), HandlerMethodArgumentResolver mechanism, type conversion, default value, và pattern bind record DTO + validation.

Module 02 bài 03 đã chỉ ra RequestMappingHandlerAdapter resolve method argument từ request. Bài này bóc tầng tiếp theo: 6 source binding (path, query, body, header, cookie, request attribute), HandlerMethodArgumentResolver mechanism, type conversion từ String → Long/Date/Enum, default value, optional binding, và pattern bind record DTO với @Valid.

Hiểu rồi, bạn đọc method signature controller như đọc map: @PathVariable Long id từ URL, @RequestParam Pageable page từ query, @RequestBody @Valid OrderRequest req từ body. Mỗi annotation = 1 resolver. Không magic.

1. 6 source binding cơ bản

flowchart LR
    Req[HTTP Request]
    Path["URL path<br/>/orders/42"]
    Query["Query string<br/>?status=active"]
    Body["Request body<br/>JSON"]
    Header["Headers<br/>Authorization"]
    Cookie["Cookies"]
    Attr["Request attributes<br/>(set bởi filter)"]

    Req --> Path
    Req --> Query
    Req --> Body
    Req --> Header
    Req --> Cookie
    Req --> Attr

    Path --> PV["@PathVariable"]
    Query --> RP["@RequestParam"]
    Body --> RB["@RequestBody"]
    Header --> RH["@RequestHeader"]
    Cookie --> CV["@CookieValue"]
    Attr --> RA["@RequestAttribute"]

Bảng tóm tắt:

AnnotationSourceVí dụ
@PathVariableURL path segment/orders/\{id\}
@RequestParamQuery string hoặc form-encoded body?status=active
@RequestBodyRequest body (JSON/XML)POST {"name":"..."}
@RequestHeaderHTTP headerAuthorization: Bearer ...
@CookieValueCookieJSESSIONID=abc
@RequestAttributeRequest attribute (set qua filter)req.setAttribute("user", ...)
@MatrixVariableMatrix params trong path/orders;status=active (rare)
@ModelAttributeForm data hoặc multiple parambind toàn bộ request → object

2. @PathVariable — bind từ URL path

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

Spring extract id từ path segment. Long id = type conversion tự động — Spring convert String → Long.

2.1 Multiple path variable

@GetMapping("/users/{userId}/orders/{orderId}")
public OrderDto get(@PathVariable Long userId, @PathVariable Long orderId) { ... }

2.2 Tên khác giữa annotation và parameter

Default: tên parameter Java = tên path variable. Khi khác:

@GetMapping("/orders/{order_id}")
public OrderDto get(@PathVariable("order_id") Long orderId) { ... }

2.3 Map cho tất cả path variable

@GetMapping("/orders/{id}/items/{itemId}")
public Object get(@PathVariable Map<String, String> vars) {
    String id = vars.get("id");
    String itemId = vars.get("itemId");
}

Hiếm dùng — code không type-safe. Chỉ khi path variable động.

2.4 Optional path variable

Path variable bắt buộc by default. Optional:

@GetMapping({"/orders", "/orders/{id}"})
public List<OrderDto> get(@PathVariable(required = false) Long id) {
    return id == null ? listAll() : List.of(findById(id));
}

Pattern này hiếm — thường tách 2 endpoint rõ ràng.

3. @RequestParam — bind từ query string

@GetMapping("/orders")
public List<OrderDto> list(
    @RequestParam(defaultValue = "active") String status,
    @RequestParam(required = false) Long minTotal,
    @RequestParam(defaultValue = "0") int page,
    @RequestParam(defaultValue = "20") int size
) { ... }

Request: GET /orders?status=pending&minTotal=100&page=2&size=50.

Spring bind:

  • status = "pending"
  • minTotal = 100L (auto convert String → Long)
  • page = 2
  • size = 50

3.1 Default value

defaultValue = "active" set value khi param không có. Default kiểu String — Spring convert sang type khác.

@RequestParam(defaultValue = "0") int page         // missing → 0
@RequestParam(defaultValue = "true") boolean activeOnly
@RequestParam(defaultValue = "ASC") SortDirection direction   // enum

3.2 Required vs optional

@RequestParam Long userId                          // required, missing → 400
@RequestParam(required = false) Long userId        // optional, null nếu missing
@RequestParam(defaultValue = "0") Long userId      // optional với default

3.3 Multiple values cho cùng param

@RequestParam List<String> tags                    // ?tags=java&tags=spring
@RequestParam Set<String> roles                    // ?roles=admin&roles=user

Spring detect collection type → bind tất cả value.

3.4 Map binding

@RequestParam Map<String, String> allParams        // bind tat ca param

Hiếm — debug/logging endpoint.

3.5 Pageable shortcut

@GetMapping("/orders")
public Page<OrderDto> list(Pageable pageable) { ... }

Spring Data tự bind ?page=0&size=20&sort=createdAt,descPageable. Module 04 sẽ đào sâu.

4. @RequestBody — bind body JSON/XML

@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) { ... }

public record OrderRequest(
    String customer,
    BigDecimal total,
    List<OrderItem> items
) {}

Request:

POST /api/orders
Content-Type: application/json

{
  "customer": "Alice",
  "total": 99.99,
  "items": [{"sku": "ABC", "quantity": 2}]
}

Spring flow:

  1. RequestMappingHandlerAdapter thấy @RequestBody.
  2. Resolve HttpMessageConverter cho Content-Type: application/jsonMappingJackson2HttpMessageConverter.
  3. Converter parse body qua Jackson → instantiate OrderRequest record.
  4. Pass vào method.

Lỗi parse → throw HttpMessageNotReadableException → 400 Bad Request.

4.1 Records vs class

Java 17 records perfect cho DTO:

public record OrderRequest(String customer, BigDecimal total) {}

So với class:

public class OrderRequest {
    private String customer;
    private BigDecimal total;
    public String getCustomer() { return customer; }
    public void setCustomer(String customer) { this.customer = customer; }
    // ... lặp cho mọi field
}

Record ngắn 10x. Immutable, thread-safe, equals/hashCode/toString tự sinh. Jackson 2.12+ support record native (cần jackson-module-parameter-names, có sẵn trong Boot starter).

4.2 Validation với @Valid

public record OrderRequest(
    @NotBlank String customer,
    @NotNull @Positive BigDecimal total,
    @NotEmpty @Valid List<OrderItem> items
) {}

public record OrderItem(
    @NotBlank String sku,
    @Min(1) int quantity
) {}

@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) { ... }

Bài 06 đào sâu validation. @Valid trigger Bean Validation. Constraint fail → throw MethodArgumentNotValidException → 400 với detail (Bài 05 Problem Details).

4.3 Optional @RequestBody

@PostMapping("/orders")
public OrderDto create(@RequestBody(required = false) OrderRequest req) {
    if (req == null) return defaultOrder();
}

Hiếm — POST thường có body. Nếu optional, dùng GET với query param thay.

5. @RequestHeader — bind từ header

@GetMapping("/orders/{id}")
public OrderDto get(
    @PathVariable Long id,
    @RequestHeader("Authorization") String authToken,
    @RequestHeader(value = "X-Request-Id", required = false) String requestId,
    @RequestHeader(defaultValue = "en") String acceptLanguage
) { ... }

Common pattern:

  • Authorization cho JWT.
  • X-Request-Id cho correlation tracing.
  • Accept-Language cho i18n.
  • User-Agent cho analytics.

5.1 Map binding

@RequestHeader Map<String, String> headers        // bind tat ca header
@RequestHeader HttpHeaders headers                 // Spring's wrapper

HttpHeaders cleaner — có method tiện như getContentType(), getAccept().

@GetMapping("/dashboard")
public DashboardDto get(
    @CookieValue("JSESSIONID") String sessionId,
    @CookieValue(value = "theme", defaultValue = "light") String theme
) { ... }

Hiếm dùng REST API — cookie thuộc session-based auth. JWT API không cần.

7. @RequestAttribute — bind từ request attribute

@Component
public class AuthFilter extends OncePerRequestFilter {
    protected void doFilterInternal(HttpServletRequest req, ...) {
        Authentication auth = jwtService.validate(req.getHeader("Authorization"));
        req.setAttribute("currentUser", auth.getPrincipal());
        chain.doFilter(req, res);
    }
}

@GetMapping("/profile")
public UserDto profile(@RequestAttribute("currentUser") User user) { ... }

Filter set attribute → controller bind. Pass data qua filter chain mà không hardcode request.getAttribute(...).

Pattern hiếm — Spring Security có @AuthenticationPrincipal chuẩn hơn cho user auth.

8. HandlerMethodArgumentResolver — mechanism

Mỗi annotation tương ứng 1 implementation HandlerMethodArgumentResolver:

AnnotationResolver class
@PathVariablePathVariableMethodArgumentResolver
@RequestParamRequestParamMethodArgumentResolver
@RequestBodyRequestResponseBodyMethodProcessor
@RequestHeaderRequestHeaderMethodArgumentResolver
@CookieValueServletCookieValueMethodArgumentResolver
@RequestAttributeRequestAttributeMethodArgumentResolver

Interface:

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

Flow:

  1. RequestMappingHandlerAdapter invoke method.
  2. Mỗi parameter, iterate resolver list, hỏi supportsParameter().
  3. Resolver nào support → invoke resolveArgument() → return value.
  4. Pass value vào method.

8.1 Custom resolver

public class CurrentUserResolver implements HandlerMethodArgumentResolver {
    public boolean supportsParameter(MethodParameter param) {
        return param.hasParameterAnnotation(CurrentUser.class);
    }

    public Object resolveArgument(MethodParameter param, ...) {
        SecurityContext ctx = SecurityContextHolder.getContext();
        return ctx.getAuthentication().getPrincipal();
    }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(new CurrentUserResolver());
    }
}

// Use:
@GetMapping("/profile")
public UserDto profile(@CurrentUser User user) { ... }

Pattern: encapsulate logic resolve user vào annotation custom. Cleaner hơn @RequestAttribute.

9. Type conversion

Spring tự convert String → type khác qua Converter/Formatter:

SourceTargetMechanism
StringLong/Integer/DoubleBuilt-in
StringLocalDateJackson + Boot 3 default ISO format
StringLocalDateTimeTương tự
StringUUIDBuilt-in
StringEnumname() matching
StringCustom type@Converter interface

9.1 Date format custom

@GetMapping("/orders")
public List<OrderDto> list(
    @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
    @RequestParam @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate to
) { ... }

Request: ?from=2026-04-01&to=2026-04-15.

Default Boot 3 dùng ISO format — không cần @DateTimeFormat nếu format là ISO.

9.2 Enum

public enum OrderStatus { PENDING, ACTIVE, COMPLETED, CANCELLED }

@GetMapping("/orders")
public List<OrderDto> list(@RequestParam OrderStatus status) { ... }

Request: ?status=ACTIVE → bind OrderStatus.ACTIVE. Case-sensitive default. Khác → 400.

Custom case-insensitive: implement Converter<String, OrderStatus>.

9.3 Custom converter

@Component
public class OrderStatusConverter implements Converter<String, OrderStatus> {
    public OrderStatus convert(String source) {
        return OrderStatus.valueOf(source.toUpperCase());
    }
}

Spring auto-register Converter bean. Bean này active cho mọi binding OrderStatus.

10. Pattern thực tế

10.1 List + filter + paging

@GetMapping("/orders")
public Page<OrderDto> list(
    @RequestParam(required = false) OrderStatus status,
    @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate from,
    @RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate to,
    @RequestParam(defaultValue = "0") @Min(0) int page,
    @RequestParam(defaultValue = "20") @Min(1) @Max(100) int size,
    @RequestParam(defaultValue = "createdAt,desc") String sort
) { ... }

Hoặc dùng Pageable + filter object:

public record OrderFilter(
    OrderStatus status,
    LocalDate from,
    LocalDate to
) {}

@GetMapping("/orders")
public Page<OrderDto> list(@Valid OrderFilter filter, Pageable pageable) { ... }

@Valid trên filter trigger validation nếu filter có constraint.

10.2 Bulk operation

@PostMapping("/orders/bulk")
public List<OrderDto> bulkCreate(@RequestBody @Valid List<OrderRequest> requests) {
    return orderService.createAll(requests);
}

@Valid cascade — validate mỗi item trong list.

10.3 Update qua PUT vs PATCH

@PutMapping("/orders/{id}")
public OrderDto update(@PathVariable Long id, @RequestBody @Valid OrderRequest req) {
    // PUT replace toan bo - require all fields
    return orderService.replace(id, req);
}

@PatchMapping("/orders/{id}")
public OrderDto patch(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
    // PATCH partial - chi field present
    return orderService.patch(id, updates);
}

PUT idempotent (full replace), PATCH partial. Map<String, Object> cho PATCH cho phép sparse update.

12. Vận hành production — payload limits, mass assignment, type conversion

Request binding là attack surface — malformed input có thể break parser, exhaust memory, bypass logic. Section này cover production hardening.

12.1 Payload size limits

server:
  max-http-request-header-size: 8KB
  tomcat:
    max-swallow-size: 2MB                 # Spring read fail-fast neu vuot
    max-http-form-post-size: 10MB

spring:
  servlet:
    multipart:
      max-file-size: 10MB
      max-request-size: 50MB

Vượt limit → 413 Payload Too Large hoặc MaxUploadSizeExceededException. Tránh attacker upload 10GB exhaust disk.

12.2 JSON parser limits — DoS protection

Boot 3 + Jackson default không có depth limit cho nested JSON → exploit DoS:

@Bean
public ObjectMapper objectMapper() {
    return JsonMapper.builder()
        .configure(StreamReadConstraints.builder()
            .maxNestingDepth(20)
            .maxNumberLength(1000)
            .maxStringLength(20_000_000)        // 20MB max string
            .build())
        .build();
}

Nested JSON 100k level → StackOverflow. Limit depth ngăn DoS.

12.3 Mass assignment vulnerability

DTO có field client không được set:

public record UserUpdateRequest(
    String name,
    String email,
    boolean isAdmin            // CLIENT khong nen set!
) {}

Client gửi {"name": "...", "isAdmin": true} → set admin flag.

Fix: split DTO theo authority — public DTO không có field nhạy cảm:

public record UserPublicUpdate(String name, String email) {}
public record UserAdminUpdate(String name, String email, boolean isAdmin) {}

@PutMapping("/users/{id}")
@PreAuthorize("hasRole('USER')")
public UserDto update(@RequestBody UserPublicUpdate req) { ... }

@PutMapping("/admin/users/{id}")
@PreAuthorize("hasRole('ADMIN')")
public UserDto adminUpdate(@RequestBody UserAdminUpdate req) { ... }

Hoặc Jackson @JsonIgnore field nhạy cảm (less safe — dễ quên).

12.4 Type conversion fail-fast

Request ?status=foobar (invalid enum) → Spring throw MethodArgumentTypeMismatchException → 400 Problem Details. Default behavior an toàn.

Custom message:

@ExceptionHandler(MethodArgumentTypeMismatchException.class)
public ProblemDetail handleTypeMismatch(MethodArgumentTypeMismatchException ex) {
    ProblemDetail pd = ProblemDetail.forStatus(400);
    pd.setDetail("Invalid value for parameter " + ex.getName() + ": " + ex.getValue());
    pd.setProperty("parameter", ex.getName());
    pd.setProperty("requiredType", ex.getRequiredType().getSimpleName());
    return pd;
}

12.5 Failure runbook

Mode 1 — HttpMessageNotReadableException spike:

  • Triệu chứng: 400 rate cao bất thường endpoint cụ thể.
  • Diagnose: client gửi malformed JSON, hoặc Content-Type wrong.
  • Remediate: log raw payload (sanitize PII trước log) → identify pattern → contact client team.

Mode 2 — MaxUploadSizeExceededException:

  • Triệu chứng: 413 từ user upload.
  • Remediate: tăng limit hoặc UX rõ ràng giới hạn (frontend validate trước upload).

Mode 3 — Path traversal attack:

  • Triệu chứng: path variable ../../../etc/passwd.
  • Remediate: validate path variable, không pass trực tiếp vào filesystem call. Whitelist allowed paths.

13. Pitfall tổng hợp

Nhầm 1: @RequestParam thay vì @RequestBody cho form-encoded. ✅ Form application/x-www-form-urlencoded dùng @RequestParam. Body JSON dùng @RequestBody. Spring không tự detect — annotation explicit.

Nhầm 2: Default value String không match type.

@RequestParam(defaultValue = "abc") int page    // 400 runtime, "abc" → int fail

✅ Default phải parse được sang type: defaultValue = "0" cho int.

Nhầm 3: @PathVariable thiếu khi path có biến.

@GetMapping("/orders/{id}")
public OrderDto get(Long id) { ... }    // QUEN @PathVariable → null

Spring 5+ với parameter-names Jackson module đôi khi work, nhưng best practice: explicit @PathVariable. ✅ Always annotate explicit.

Nhầm 4: Trùng tên path variable + query param.

@GetMapping("/orders/{id}")
public OrderDto get(@PathVariable Long id, @RequestParam(required = false) Long id2) { ... }

2 tham số khác nhau. Nếu cùng tên id, Spring confusion. ✅ Tên rõ ràng: id cho path, parentId/relatedId cho query.

Nhầm 5: @RequestBody cho GET.

@GetMapping("/search")
public List<OrderDto> search(@RequestBody SearchRequest req) { ... }

GET không có body theo HTTP spec. Spring 5+ vẫn parse được nhưng client không nên gửi body GET. ✅ GET với query param. Body GET → POST /search.

Nhầm 6: Date không khai @DateTimeFormat.

@RequestParam LocalDate from    // Boot 3 ok với ISO, nhưng nếu format khác sẽ fail

✅ Boot 3 default ISO. Khác ISO → @DateTimeFormat(pattern = "...").

Nhầm 7: Quên @Valid trên @RequestBody.

public OrderDto create(@RequestBody OrderRequest req) { ... }    // KHONG validate

Constraint trên record không trigger. ✅ @Valid @RequestBody OrderRequest req.

13. 📚 Deep Dive Spring Reference

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

Spring Framework Reference:

Source:

Pattern:

Tool:

  • IntelliJ HTTP Client / Bruno / Postman cho test endpoint.
  • Spring Boot DevTools — hot reload khi đổi controller signature.

14. Tóm tắt

  • 6 source binding chính: path (@PathVariable), query (@RequestParam), body (@RequestBody), header (@RequestHeader), cookie (@CookieValue), attribute (@RequestAttribute).
  • Mỗi annotation = 1 HandlerMethodArgumentResolver resolve runtime.
  • @PathVariable cho URL segment. Multiple variable + tên không trùng → khai explicit.
  • @RequestParam cho query string. Support defaultValue, required, List, Map, Pageable.
  • @RequestBody cho body JSON/XML. Pattern modern: record DTO + @Valid.
  • Records (Java 17+) clean cho DTO — immutable, thread-safe, Jackson native support.
  • Type conversion auto: String → Long/UUID/Enum/Date. Custom qua Converter<String, T>.
  • Custom argument resolver wrap logic vào annotation custom (@CurrentUser, @TenantId).
  • @DateTimeFormat cho date format không phải ISO. Boot 3 default ISO.
  • PUT replace toàn bộ, PATCH partial. Convention: PUT với full DTO, PATCH với Map<String, Object> hoặc JsonPatch.
  • @Valid trigger Bean Validation cascade — bài 06 đào sâu.

15. Tự kiểm tra

Tự kiểm tra
Q1
Đoạn sau bind argument từ source nào? Request mẫu trông như thế nào?
@GetMapping("/users/{userId}/orders")
public Page<OrderDto> list(
  @PathVariable Long userId,
  @RequestParam(defaultValue = "ACTIVE") OrderStatus status,
  @RequestHeader("X-Tenant-Id") String tenantId,
  Pageable pageable
) { ... }

Source binding:

  • userId: URL path segment — @PathVariable.
  • status: query string — @RequestParam với default "ACTIVE" enum.
  • tenantId: HTTP header — @RequestHeader.
  • pageable: Spring Data tự bind từ query param page, size, sort.

Request mẫu:

GET /users/42/orders?status=PENDING&page=0&size=20&sort=createdAt,desc HTTP/1.1
Host: api.olhub.org
X-Tenant-Id: tenant-abc
Authorization: Bearer eyJ...

Spring resolve:

  • userId = 42L (auto convert từ "42").
  • status = OrderStatus.PENDING (enum lookup).
  • tenantId = "tenant-abc".
  • pageable = PageRequest.of(0, 20, Sort.by("createdAt").descending()).

Method invoke với 4 argument đã resolve. Mỗi argument do 1 HandlerMethodArgumentResolver handle độc lập — Spring iterate resolver list cho từng parameter.

Q2
Vì sao records (Java 17+) tốt hơn class POJO cho `@RequestBody` DTO? Cho 4 lợi ích cụ thể.
  1. Concise: 1 dòng cho 5 field thay 30 dòng (constructor + getters + setters + equals + hashCode + toString).
    public record OrderRequest(String customer, BigDecimal total, List<Item> items) {}
  2. Immutable: field final. Không setter. DTO request không bị mutate trong service layer — fewer bug từ accidental mutation.
  3. Thread-safe: immutable → 100% thread-safe. App với virtual threads / async, DTO chia sẻ giữa threads không cần sync.
  4. Jackson native support (2.12+): Jackson detect record canonical constructor, deserialize via jackson-module-parameter-names (Boot starter có sẵn). Không cần @JsonCreator/@JsonProperty.
  5. Validation tích hợp:
    public record OrderRequest(
      @NotBlank String customer,
      @NotNull @Positive BigDecimal total
    ) {}
    Constraint annotation trên component — Spring validate khi @Valid.

Khi nào KHÔNG nên dùng record:

  • JPA Entity — record không support entity lifecycle (Hibernate yêu cầu mutable).
  • Form binding với "default constructor + setter" pattern — record không có default constructor (cần canonical).
  • Cần inheritance — record cannot extend (chỉ implement interface).

Quy tắc 2026: DTO request/response → record. Entity → class. Service → class.

Q3
Đoạn code sau có 2 vấn đề. Chỉ ra + fix.
@PostMapping("/orders")
public OrderDto create(@RequestBody OrderRequest req) {
  if (req == null) throw new BadRequestException();
  if (req.customer() == null || req.customer().isBlank()) {
      throw new BadRequestException("customer required");
  }
  if (req.total() == null || req.total().signum() <= 0) {
      throw new BadRequestException("total must be positive");
  }
  // 20 dong validation manual khac
  return orderService.create(req);
}
  1. Validation manual: 20 dòng if-throw lặp lại pattern. Khi thêm field, phải thêm validation trong controller. Code lan ra mọi endpoint.

    Fix: dùng Bean Validation:

    public record OrderRequest(
      @NotBlank String customer,
      @NotNull @Positive BigDecimal total,
      @NotEmpty List<@Valid OrderItem> items
    ) {}
    
    @PostMapping("/orders")
    public OrderDto create(@Valid @RequestBody OrderRequest req) {
      return orderService.create(req);
    }
    @Valid trigger Spring validation. Constraint fail → throw MethodArgumentNotValidException → 400 Problem Details (Bài 05). Validation logic ở DTO — single source of truth.
  2. Throw BadRequestException custom: không phải standard exception. Spring không biết map nào → 500 default.

    Fix: dùng standard exception hoặc setup global exception handler:

    @ControllerAdvice
    public class GlobalExceptionHandler {
      @ExceptionHandler(MethodArgumentNotValidException.class)
      public ResponseEntity<ProblemDetail> handleValidation(MethodArgumentNotValidException ex) {
          ProblemDetail pd = ProblemDetail.forStatus(400);
          pd.setTitle("Validation failed");
          pd.setProperty("violations", ex.getBindingResult().getAllErrors());
          return ResponseEntity.badRequest().body(pd);
      }
    }
    Hoặc throw ResponseStatusException(HttpStatus.BAD_REQUEST, ...) chuẩn Spring.

Code sạch:

@PostMapping("/orders")
public OrderDto create(@Valid @RequestBody OrderRequest req) {
  return orderService.create(req);          // 1 dong
}

Validation declarative trong DTO. Exception handling tập trung trong @ControllerAdvice. Controller chỉ orchestrate.

Q4
Bạn cần inject "current user" (từ JWT auth) vào nhiều controller method. Có 3 cách: (a) @RequestAttribute, (b) SecurityContextHolder.getContext().getAuthentication().getPrincipal(), (c) custom @CurrentUser annotation. Cái nào tốt nhất, vì sao?

(c) Custom @CurrentUser — best practice 2026.

ApproachProsCons
(a) @RequestAttributeStandard SpringCần Filter set attribute trước. String key dễ typo. Không type-safe.
(b) SecurityContextHolderType-safeCode (User) ctx.getAuth().getPrincipal() verbose. Lặp 100 controller. Khó test (ThreadLocal).
(c) @CurrentUser customConcise, type-safe, declarative, mock dễ trong testSetup 1 lần — viết resolver.

Implement @CurrentUser:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {}

@Component
public class CurrentUserResolver implements HandlerMethodArgumentResolver {
  public boolean supportsParameter(MethodParameter param) {
      return param.hasParameterAnnotation(CurrentUser.class);
  }

  public Object resolveArgument(MethodParameter param, ...) {
      Authentication auth = SecurityContextHolder.getContext().getAuthentication();
      if (auth == null || !auth.isAuthenticated()) {
          throw new IllegalStateException("Not authenticated");
      }
      return auth.getPrincipal();   // assume User type
  }
}

@Configuration
public class WebConfig implements WebMvcConfigurer {
  @Autowired CurrentUserResolver resolver;

  public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
      resolvers.add(resolver);
  }
}

Use:

@GetMapping("/profile")
public UserDto profile(@CurrentUser User user) {
  return UserDto.from(user);
}

@PostMapping("/orders")
public OrderDto create(@CurrentUser User user, @Valid @RequestBody OrderRequest req) {
  return orderService.create(user, req);
}

Lợi ích:

  • Concise — declarative annotation.
  • Type-safe — compile error nếu type mismatch.
  • Test friendly — mock resolver hoặc inject test User trong test slice.
  • Encapsulate logic — tương lai đổi từ Spring Security sang custom JWT, sửa 1 chỗ resolver.

Note: Spring Security có @AuthenticationPrincipal built-in tương đương — dùng nếu app dùng SS standard. Custom @CurrentUser khi cần logic resolve khác (vd merge data từ multiple source).

Q5
Đoạn sau xử lý filter + paging. Có 2 cách design API — cái nào RESTful hơn?
// Cach 1: rai rac param
@GetMapping("/orders")
public Page<OrderDto> list(
  @RequestParam(required = false) OrderStatus status,
  @RequestParam(required = false) LocalDate from,
  @RequestParam(required = false) LocalDate to,
  @RequestParam(required = false) BigDecimal minTotal,
  Pageable pageable
) { ... }

// Cach 2: filter object
public record OrderFilter(
  OrderStatus status,
  LocalDate from,
  LocalDate to,
  BigDecimal minTotal
) {}

@GetMapping("/orders")
public Page<OrderDto> list(@Valid OrderFilter filter, Pageable pageable) { ... }

Cả 2 cách RESTful — không khác URL/HTTP semantics.

Khác biệt nằm ở code design + maintainability:

AspectCách 1 (rải rác)Cách 2 (filter object)
Method signature5+ param verbose2 param clean
Add filter mớiSửa method signatureThêm field record
ReuseCopy 5 param mỗi endpointReuse OrderFilter ở list/export/count
ValidationConstraint trên từng paramConstraint trên record + cross-field validation
TestMock 5 value mỗi testBuild 1 OrderFilter instance
LoggingLog 5 varLog filter.toString() 1 dòng
DocumentationOpenAPI: 5 query param tương đối flatOpenAPI: Schema reusable

Recommend cách 2 cho 3+ filter param. 1-2 param đơn giản — cách 1 OK.

Cross-field validation với filter object:

public record OrderFilter(
  OrderStatus status,
  @DateTimeFormat(iso = ISO.DATE) LocalDate from,
  @DateTimeFormat(iso = ISO.DATE) LocalDate to,
  @PositiveOrZero BigDecimal minTotal
) {

  @AssertTrue(message = "from must be before to")
  public boolean isDateRangeValid() {
      return from == null || to == null || !from.isAfter(to);
  }
}

Cross-field validation natural trong record — không thể làm với rải rác param (constraint annotation không cross-field built-in).

Spring binding mechanism: Spring tự bind query param → record component theo tên. ?status=ACTIVE&from=2026-04-01&minTotal=100OrderFilter(ACTIVE, 2026-04-01, null, 100).

Note: filter object pattern tương đương Spring's @ModelAttribute implicit binding — Spring detect non-annotated parameter là form/query binding object.

Q6
App nhận PUT vs PATCH cho update order. Difference semantic + binding?

HTTP semantic:

  • PUT: idempotent, replace toàn bộ resource. Client gửi full state mới. Server replace.
  • PATCH: partial update. Client gửi chỉ field thay đổi. Server merge.

Spring binding pattern:

// PUT - full replace
@PutMapping("/orders/{id}")
public OrderDto update(@PathVariable Long id, @Valid @RequestBody OrderRequest req) {
  // req PHAI co full fields, validate yeu cau @NotNull...
  return orderService.replace(id, req);
}

// PATCH - partial
@PatchMapping("/orders/{id}")
public OrderDto patch(@PathVariable Long id, @RequestBody Map<String, Object> updates) {
  // updates chi co field client gui
  return orderService.patch(id, updates);
}

Vấn đề Map<String, Object>: không type-safe. Field "total" có thể là String "100", Integer 100, BigDecimal 100.0. Phải convert manual.

Pattern tốt hơn — Optional fields trong record:

public record OrderPatch(
  Optional<String> customer,
  Optional<BigDecimal> total,
  Optional<List<OrderItem>> items
) {}

@PatchMapping("/orders/{id}")
public OrderDto patch(@PathVariable Long id, @RequestBody OrderPatch patch) {
  // patch.customer().isPresent() → update field
  return orderService.patch(id, patch);
}

Jackson handle Optional: missing field → Optional.empty(), present → Optional.of(value). Type-safe.

Pattern xịn nhất — JSON Patch (RFC 6902):

PATCH /orders/42
Content-Type: application/json-patch+json

[
{ "op": "replace", "path": "/customer", "value": "Bob" },
{ "op": "add", "path": "/items/-", "value": {"sku": "X", "quantity": 1} }
]

Standard chính thức. Cần lib json-patch + custom controller logic. Phổ biến trong API enterprise (vd GitHub API).

Quy tắc 2026:

  • Resource đơn giản → PUT với full DTO. Đơn giản, ít bug.
  • Resource phức tạp với nhiều field optional → PATCH với Optional in record.
  • Production API enterprise → JSON Patch RFC 6902.

Bài tiếp theo: Response — ResponseEntity, status code, header

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