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:
| Annotation | Source | Ví dụ |
|---|---|---|
@PathVariable | URL path segment | /orders/\{id\} |
@RequestParam | Query string hoặc form-encoded body | ?status=active |
@RequestBody | Request body (JSON/XML) | POST {"name":"..."} |
@RequestHeader | HTTP header | Authorization: Bearer ... |
@CookieValue | Cookie | JSESSIONID=abc |
@RequestAttribute | Request attribute (set qua filter) | req.setAttribute("user", ...) |
@MatrixVariable | Matrix params trong path | /orders;status=active (rare) |
@ModelAttribute | Form data hoặc multiple param | bind 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= 2size= 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,desc → Pageable. 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:
RequestMappingHandlerAdapterthấy@RequestBody.- Resolve
HttpMessageConverterchoContent-Type: application/json→MappingJackson2HttpMessageConverter. - Converter parse body qua Jackson → instantiate
OrderRequestrecord. - 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:
Authorizationcho JWT.X-Request-Idcho correlation tracing.Accept-Languagecho i18n.User-Agentcho 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().
6. @CookieValue — bind từ cookie
@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:
| Annotation | Resolver class |
|---|---|
@PathVariable | PathVariableMethodArgumentResolver |
@RequestParam | RequestParamMethodArgumentResolver |
@RequestBody | RequestResponseBodyMethodProcessor |
@RequestHeader | RequestHeaderMethodArgumentResolver |
@CookieValue | ServletCookieValueMethodArgumentResolver |
@RequestAttribute | RequestAttributeMethodArgumentResolver |
Interface:
public interface HandlerMethodArgumentResolver {
boolean supportsParameter(MethodParameter parameter);
Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) throws Exception;
}
Flow:
RequestMappingHandlerAdapterinvoke method.- Mỗi parameter, iterate resolver list, hỏi
supportsParameter(). - Resolver nào support → invoke
resolveArgument()→ return value. - 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:
| Source | Target | Mechanism |
|---|---|---|
String | Long/Integer/Double | Built-in |
String | LocalDate | Jackson + Boot 3 default ISO format |
String | LocalDateTime | Tương tự |
String | UUID | Built-in |
String | Enum | name() matching |
String | Custom 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
Spring Framework Reference:
- Spring MVC — Method Arguments — bảng đầy đủ argument support.
- Spring MVC — @PathVariable
- Spring MVC — @RequestParam
- Spring MVC — @RequestBody
- Spring MVC — Type Conversion —
Converter/FormatterSPI.
Source:
RequestMappingHandlerAdapter— invoke handler + bind argument.HandlerMethodArgumentResolver— interface SPI.
Pattern:
- Microsoft REST Guidelines — Filtering & Sorting
- JSON:API spec — pagination + filtering convention.
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
HandlerMethodArgumentResolverresolve runtime. @PathVariablecho URL segment. Multiple variable + tên không trùng → khai explicit.@RequestParamcho query string. SupportdefaultValue,required,List,Map,Pageable.@RequestBodycho 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). @DateTimeFormatcho 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ặcJsonPatch. @Validtrigger Bean Validation cascade — bài 06 đào sâu.
15. 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
) { ... }
▸
@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 —@RequestParamvới default "ACTIVE" enum.tenantId: HTTP header —@RequestHeader.pageable: Spring Data tự bind từ query parampage,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.
Q2Vì sao records (Java 17+) tốt hơn class POJO cho `@RequestBody` DTO? Cho 4 lợi ích cụ thể.▸
- 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) {} - Immutable: field
final. Không setter. DTO request không bị mutate trong service layer — fewer bug từ accidental mutation. - Thread-safe: immutable → 100% thread-safe. App với virtual threads / async, DTO chia sẻ giữa threads không cần sync.
- 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. - Validation tích hợp:Constraint annotation trên component — Spring validate khi
public record OrderRequest( @NotBlank String customer, @NotNull @Positive BigDecimal total ) {}@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);
}
▸
@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);
}- 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); }@Validtrigger Spring validation. Constraint fail → throwMethodArgumentNotValidException→ 400 Problem Details (Bài 05). Validation logic ở DTO — single source of truth. - Throw
BadRequestExceptioncustom: 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:
Hoặc throw@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); } }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.
Q4Bạ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?▸
@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.
| Approach | Pros | Cons |
|---|---|---|
(a) @RequestAttribute | Standard Spring | Cần Filter set attribute trước. String key dễ typo. Không type-safe. |
(b) SecurityContextHolder | Type-safe | Code (User) ctx.getAuth().getPrincipal() verbose. Lặp 100 controller. Khó test (ThreadLocal). |
(c) @CurrentUser custom | Concise, type-safe, declarative, mock dễ trong test | Setup 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) { ... }
▸
// 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:
| Aspect | Cách 1 (rải rác) | Cách 2 (filter object) |
|---|---|---|
| Method signature | 5+ param verbose | 2 param clean |
| Add filter mới | Sửa method signature | Thêm field record |
| Reuse | Copy 5 param mỗi endpoint | Reuse OrderFilter ở list/export/count |
| Validation | Constraint trên từng param | Constraint trên record + cross-field validation |
| Test | Mock 5 value mỗi test | Build 1 OrderFilter instance |
| Logging | Log 5 var | Log filter.toString() 1 dòng |
| Documentation | OpenAPI: 5 query param tương đối flat | OpenAPI: 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=100 → OrderFilter(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.
Q6App 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
Optionalin 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...