Validation nâng cao — Groups, Custom Constraint, i18n, Service Layer
Bài atomic đào sâu 4 cơ chế validation nâng cao của Jakarta Bean Validation: validation groups để tái dùng DTO cho nhiều ngữ cảnh (Create/Update), custom @Constraint để đóng gói rule domain, i18n message qua messages.properties, và validation trong service layer qua @Validated. Giải thích tại sao mỗi cơ chế tồn tại và khi nào dùng.
TL;DR: Bốn cơ chế nâng cao của Jakarta Bean Validation giải quyết 4 bài toán khác nhau: (1) Validation groups — cùng một DTO, rule khác nhau tùy ngữ cảnh POST/PUT, dùng marker interface Create/Update + @Validated(Create.class); (2) Custom @Constraint — rule domain không nằm trong 23 built-in, viết annotation + ConstraintValidator class; (3) i18n message — message lỗi dịch theo locale qua messages.properties + placeholder {min} / {max}; (4) @Validated service layer — validate ở boundary service khi service được gọi từ nhiều entry point (Kafka, scheduler) ngoài controller. Hiểu cơ chế bên dưới (AOP proxy, ConstraintValidatorContext) giúp tránh silent-fail và anti-pattern inject DB vào validator.
Bài trước (Validation constraints cơ bản) giới thiệu 23 built-in constraint và @Valid cascade. Bài này đào sâu 4 cơ chế nâng cao — mỗi cơ chế giải quyết một nhóm bài toán thực tế mà built-in không đủ.
1. Validation groups — tại sao một DTO cho nhiều ngữ cảnh
Bài toán thực tế
API POST /orders (tạo mới) và PUT /orders/{id} (cập nhật) xử lý cùng kiểu dữ liệu nhưng có rule khác nhau:
POST:idphải null (server auto-gen),totalbắt buộc.PUT:idphải not null (chỉ định bản ghi cần sửa),totaltuỳ chọn (chỉ update field nào gửi lên).
Không có validation groups, bạn có 2 lựa chọn tệ: (a) viết 2 DTO riêng với field gần giống nhau, hoặc (b) để rule chung nhất rồi validate thủ công trong controller. Cả 2 đều giảm signal từ annotation và tăng code noise.
Cơ chế bên dưới — marker interface + group selection
Jakarta Bean Validation định nghĩa mỗi constraint có thuộc tính groups — một mảng class interface đóng vai trò nhãn. Khi bạn gọi @Validated(Create.class), Spring truyền nhãn đó xuống Validator.validate(object, Create.class), và Hibernate Validator chỉ evaluate những constraint nào khai báo groups = Create.class. Constraint không khai groups thuộc default group — apply cho mọi lần validate.
// Marker interface — chi can ton tai, khong can method
public interface Create {}
public interface Update {}
public record OrderRequest(
@Null(groups = Create.class) // POST: server tu gen, client khong gui
@NotNull(groups = Update.class) // PUT: phai co de xac dinh ban ghi
Long id,
@NotBlank(groups = {Create.class, Update.class})
@Size(min = 2, max = 100, groups = {Create.class, Update.class})
String customer,
@NotNull(groups = Create.class) // POST: bat buoc
@Positive(groups = {Create.class, Update.class})
BigDecimal total // PUT: optional (null = giu nguyen)
) {}
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@PostMapping
public OrderDto create(
@Validated(Create.class) @RequestBody OrderRequest req) {
return orderService.create(req);
}
@PutMapping("/{id}")
public OrderDto update(
@PathVariable Long id,
@Validated(Update.class) @RequestBody OrderRequest req) {
return orderService.update(id, req);
}
}
@Validated(Create.class) là annotation của Spring — khác @Valid Jakarta (không support group). Spring truyền class group xuống Hibernate Validator qua MethodValidationInterceptor.
Vì sao nhìn lại thường chọn 2 DTO riêng
Groups giải quyết được bài toán, nhưng có trade-off:
| Khía cạnh | Validation groups | 2 DTO riêng |
|---|---|---|
| Số class | 1 DTO + 2 marker interface | 2 DTO |
| OpenAPI schema | 1 schema khó express "field optional tuỳ verb" | 2 schema rõ ràng |
| Type safety | Service nhận 1 type, phải defensive-check null | Service biết chắc kiểu dữ liệu |
| Đọc constraint | Phải trace từng groups = attribute | Đọc field trực tiếp |
@Valid standard | Không dùng được — phải dùng @Validated Spring | Dùng @Valid Jakarta chuẩn |
Quy tắc thực tế: dùng 2 DTO riêng cho 90% case (POST vs PUT khác cấu trúc). Dùng validation groups khi có 5+ scenario (Draft, Review, Publish, Archive, ...) — quá nhiều DTO gây explosion.
Constraint không khai groups attribute thuộc Default group và apply cho mọi lần validate(). Khi gọi @Validated(Create.class), constraint thuộc Default group không apply — chỉ Create.class apply. Nếu muốn constraint chạy cho cả 2, phải khai rõ cả 2: groups = {Create.class, Update.class}.
2. Custom @Constraint — khi built-in không đủ
Tại sao cần constraint custom
23 built-in constraint của Jakarta Bean Validation xử lý rule syntactic (format, range, null). Nhưng domain rule của bạn thường semantic — không nằm ở format mà ở nghĩa nghiệp vụ:
- SKU phải match pattern riêng của công ty:
ABC-1234(3 chữ hoa + gạch + 4 số). - Ngày giao hàng phải sau ngày đặt ít nhất 1 ngày.
- Mã voucher phải thuộc danh sách active (database check — nhưng xem pitfall §2.3).
Rule này không dùng @Pattern + hard-code regex trong annotation được vì: (a) thiếu tên ngữ nghĩa, (b) không reusable, (c) message không đủ mô tả.
Cơ chế bên dưới — 3 phần tạo custom constraint
Custom constraint gồm 3 phần: annotation + validator class + sử dụng.
flowchart LR
A["@ValidSku annotation<br/>@Constraint(validatedBy=...)"] --> B["SkuValidator<br/>ConstraintValidator<ValidSku, String>"]
B --> C["isValid(value, ctx)<br/>return true/false"]
D["OrderItem DTO"] -- "@ValidSku String sku" --> ABước 1 — Annotation:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SkuValidator.class)
@Documented
public @interface ValidSku {
String message() default "Invalid SKU format (expected: ABC-1234)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// 3 attribute groups/payload/message la bat buoc theo JSR 380
}
Bước 2 — Validator class:
public class SkuValidator implements ConstraintValidator<ValidSku, String> {
private static final Pattern SKU_PATTERN =
Pattern.compile("^[A-Z]{3}-\\d{4}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // null la job cua @NotNull, khong phai ta
return SKU_PATTERN.matcher(value).matches();
}
}
Bước 3 — Sử dụng:
public record OrderItem(
@NotBlank
@ValidSku // domain rule
String sku,
@Min(1) @Max(99)
int quantity,
@NotNull @Positive
BigDecimal unitPrice
) {}
Cross-field constraint — validate nhiều field cùng lúc
Khi rule liên quan nhiều hơn 1 field (vd: ngày bắt đầu trước ngày kết thúc), đặt annotation ở class/record level thay field level:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = DateRangeValidator.class)
public @interface ValidDateRange {
String message() default "Start date must be before end date";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class DateRangeValidator
implements ConstraintValidator<ValidDateRange, OrderRequest> {
@Override
public boolean isValid(OrderRequest req, ConstraintValidatorContext ctx) {
if (req.orderDate() == null || req.deliveryDate() == null) return true;
boolean valid = !req.deliveryDate().isBefore(req.orderDate().plusDays(1));
if (!valid) {
// Gan loi vao field cu the thay vi object-level
ctx.disableDefaultConstraintViolation();
ctx.buildConstraintViolationWithTemplate(
"Delivery date must be at least 1 day after order date"
).addPropertyNode("deliveryDate").addConstraintViolation();
}
return valid;
}
}
@ValidDateRange // dat tren class/record
public record OrderRequest(
LocalDate orderDate,
LocalDate deliveryDate,
// ... other fields
) {}
Dùng ConstraintValidatorContext.addPropertyNode("deliveryDate") để gán lỗi vào field cụ thể — client nhận được violations[].field = "deliveryDate" thay vì lỗi ở object root.
Cho validation cross-field đơn giản hơn (dùng 1 lần), @AssertTrue method ngắn gọn hơn:
public record OrderRequest(LocalDate orderDate, LocalDate deliveryDate) {
@AssertTrue(message = "Delivery must be at least 1 day after order date")
public boolean isDeliveryDateValid() {
if (orderDate == null || deliveryDate == null) return true;
return !deliveryDate.isBefore(orderDate.plusDays(1));
}
}
@AssertTrue trên method is*() — Hibernate Validator invoke method, expect true là valid. Không cần viết annotation riêng.
Lỗi thường gặp là inject @Repository hoặc @Service vào ConstraintValidator để check database (vd kiểm tra SKU tồn tại). Việc này tạo coupling annotation validation vào DB và phá single responsibility. Hơn nữa, validator có thể được khởi tạo ngoài Spring context (unit test, standalone library) — bean inject là null.
Quy tắc: ConstraintValidator là pure function — chỉ check format/structure, không gọi DB hay external service. Business validation (SKU tồn tại trong DB, voucher còn hiệu lực) là trách nhiệm của service layer, throw domain exception.
3. i18n message — lỗi trả về đúng ngôn ngữ người dùng
Tại sao cần i18n message
Hardcode message "Customer name is required" trong annotation có 3 vấn đề: (a) không thể dịch theo locale của user, (b) khó maintain khi message cần thay đổi — phải sửa annotation ở nhiều chỗ, (c) không tận dụng placeholder constraint ({min}, {max}).
Cơ chế bên dưới — interpolation pipeline
Khi Hibernate Validator tạo violation message, nó qua pipeline interpolation:
flowchart LR
A["message = '{order.customer.size}'"] --> B["MessageInterpolator<br/>tra messages.properties<br/>theo Locale"]
B --> C["'Customer name must be<br/>between 2 and 100 chars'"]
B --> D["'Ten khach hang phai<br/>dai tu 2 den 100 ky tu'"]{order.customer.size} — curly braces là message key. {min}, {max} là constraint attribute placeholder — Hibernate Validator tự thay bằng giá trị min / max của @Size.
Cài đặt:
// DTO dung message key
public record OrderRequest(
@NotBlank(message = "{order.customer.required}")
@Size(min = 2, max = 100, message = "{order.customer.size}")
String customer,
@NotNull(message = "{order.total.required}")
@Positive(message = "{order.total.positive}")
BigDecimal total,
@NotNull(message = "{order.delivery.required}")
@Future(message = "{order.delivery.future}")
LocalDate deliveryDate
) {}
src/main/resources/messages.properties (default — tiếng Việt nếu app Vietnamese-first):
order.customer.required=Ten khach hang khong duoc trong
order.customer.size=Ten khach hang phai dai tu {min} den {max} ky tu
order.total.required=Tong tien la bat buoc
order.total.positive=Tong tien phai lon hon 0
order.delivery.required=Ngay giao hang la bat buoc
order.delivery.future=Ngay giao hang phai trong tuong lai
src/main/resources/messages_en.properties (tiếng Anh):
order.customer.required=Customer name is required
order.customer.size=Customer name must be between {min} and {max} characters
order.total.required=Total amount is required
order.total.positive=Total amount must be positive
order.delivery.required=Delivery date is required
order.delivery.future=Delivery date must be in the future
Spring Boot đọc messages.properties tự động qua MessageSource autoconfig. Locale được resolve theo Accept-Language header của request:
@Configuration
public class LocaleConfig {
@Bean
public LocaleResolver localeResolver() {
AcceptHeaderLocaleResolver resolver = new AcceptHeaderLocaleResolver();
resolver.setDefaultLocale(Locale.forLanguageTag("vi"));
resolver.setSupportedLocales(List.of(
Locale.forLanguageTag("vi"),
Locale.forLanguageTag("en")
));
return resolver;
}
}
Request với Accept-Language: en nhận English message. Accept-Language: vi nhận Vietnamese. Thiếu header hoặc locale không được support, Spring rơi về default vi.
Missing key trong 1 locale khiến user thấy raw key (order.customer.required thay tên field) — UX tệ. Thêm CI check:
diff <(grep -oP '^[\w.]+(?==)' src/main/resources/messages.properties | sort) \
<(grep -oP '^[\w.]+(?==)' src/main/resources/messages_en.properties | sort)
# Output khac -> build fail -> bat missing translation
4. Validation trong service layer — tại sao cần thêm một tầng
Vấn đề với chỉ validate ở controller
Controller validate request từ HTTP. Nhưng service thường được gọi từ nhiều entry point hơn:
flowchart TB
A["HTTP Controller<br/>@Valid @RequestBody"] --> S["OrderService"]
B["Kafka Consumer<br/>deserialize JSON"] --> S
C["Scheduled Job<br/>CSV import"] --> S
D["Internal API / RPC"] --> S
S --> DB[(Database)]Path B, C, D không đi qua controller — không có @Valid guard. Nếu service không tự validate, invalid data có thể vào DB từ path không phải HTTP.
Cơ chế bên dưới — AOP proxy
@Validated trên service class kích hoạt MethodValidationPostProcessor (Boot autoconfig). Post-processor wrap service bean trong một AOP proxy. Khi code gọi orderService.create(req), lời gọi thực ra đi qua proxy:
flowchart LR
Caller["Caller (Controller / Kafka)"] --> Proxy["AOP Proxy<br/>MethodValidationInterceptor"]
Proxy -- "constraint OK" --> Svc["OrderService.create(req)"]
Proxy -- "constraint fail" --> Ex["throw ConstraintViolationException"]MethodValidationInterceptor.invoke() gọi Validator.validate() trên các tham số có annotation constraint trước khi forward xuống method thật. Nếu có vi phạm, interceptor throw ConstraintViolationException (khác với MethodArgumentNotValidException của controller).
@Service
@Validated // bat AOP proxy cho method validation
public class OrderService {
public OrderDto create(@Valid OrderRequest req) {
// @Valid cascade validate OrderRequest + nested
// Neu fail: ConstraintViolationException truoc khi vao method body
return orderRepository.save(toEntity(req));
}
public OrderDto findById(@NotNull @Positive Long id) {
// Parameter constraint truc tiep (khong can @Valid)
return orderRepository.findById(id)
.map(this::toDto)
.orElseThrow(() -> new ResourceNotFoundException("Order", id));
}
}
Map ConstraintViolationException thành response
Service throw ConstraintViolationException — cần map sang HTTP 400 trong @RestControllerAdvice:
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public ProblemDetail handleConstraintViolation(ConstraintViolationException ex) {
ProblemDetail pd = ProblemDetail.forStatus(HttpStatus.BAD_REQUEST);
pd.setTitle("Validation failed");
pd.setDetail("One or more parameters did not pass validation");
List<Map<String, String>> violations = ex.getConstraintViolations().stream()
.map(v -> Map.of(
"path", v.getPropertyPath().toString(),
"message", v.getMessage()
))
.toList();
pd.setProperty("violations", violations);
return pd;
}
}
Khi nào nên / không nên validate service
Validate service có overhead (AOP proxy mỗi method call ~10-50µs). Không phải lúc nào cũng cần:
| Tình huống | Validate service? | Lý do |
|---|---|---|
| Service chỉ được gọi từ controller | Không cần | Controller đã guard |
| Service được gọi từ Kafka consumer | Cần | Consumer không qua controller |
| Service được gọi từ scheduled job | Cần | Scheduled job không qua controller |
| Service expose qua RPC / shared library | Cần | Caller external không trust |
| Hot path vượt 10k QPS | Cân nhắc | Overhead nhỏ nhưng cộng dồn |
Quy tắc: validate tại boundary — nơi input đến từ bên ngoài. Service là boundary khi có path ngoài controller.
5. Liên hệ các bài khác
- Validation constraints cơ bản: 23 built-in constraint,
@Validcascade,MethodArgumentNotValidException— nền tảng trước khi đọc bài này. - OpenAPI và springdoc: validation annotation (
@NotNull,@Size,@Pattern) được springdoc đọc và tự động sinh schema OpenAPI — constraint trên DTO trở thành documentation sống. Bài đó giải thích cơ chế mapping. - Bài 01 — Error handling và
ProblemDetail:ConstraintViolationExceptiontừ service layer cần map trong@RestControllerAdvice— xem pattern xử lý tập trung ở bài đó. - Bài 02 —
@ExceptionHandlervàGlobalExceptionHandler: nơi đặt handler cho cảMethodArgumentNotValidExceptionlẫnConstraintViolationException.
Tóm tắt
- Validation groups dùng marker interface (
Create,Update) +@Validated(Create.class)để apply rule khác nhau cho cùng DTO theo ngữ cảnh. Constraint không khaigroupsthuộc Default group — không apply khi chỉ định group cụ thể. - Prefer 2 DTO riêng (CreateRequest + UpdateRequest) hơn groups trong 90% case — type safety tốt hơn, OpenAPI schema rõ hơn, dùng
@ValidJakarta chuẩn. - Custom
@Constraintgồm 3 phần: annotation (@Constraint(validatedBy=...)), validator class (ConstraintValidator<A, T>), sử dụng. Validator là pure function — không gọi DB hay external service. - Cross-field: annotation đặt ở class-level, validator nhận toàn object.
@AssertTruemethodis*()là cách đơn giản hơn cho rule 1-shot. - i18n message: dùng key
{order.customer.required}thay hardcode string, resolve quamessages.propertiestheo localeAccept-Language. Placeholder{min}/{max}tự được Hibernate Validator substitute. - Service layer
@Validated: kích hoạt AOP proxy, validate method parameter trước khi vào method body. ThrowConstraintViolationException. Cần khi service là boundary của path ngoài controller (Kafka, scheduler, RPC).
Tự kiểm tra
Q1Cùng một OrderRequest DTO, field id phải null khi POST và not-null khi PUT. Dùng validation groups, viết annotation đúng cho field id và controller method signature tương ứng.▸
OrderRequest DTO, field id phải null khi POST và not-null khi PUT. Dùng validation groups, viết annotation đúng cho field id và controller method signature tương ứng.Khai báo 2 marker interface và annotate field id với cả 2 constraint, mỗi cái thuộc 1 group:
public interface Create {}
public interface Update {}
public record OrderRequest(
@Null(groups = Create.class) // POST: server auto-gen, client gui null
@NotNull(groups = Update.class) // PUT: bat buoc de xac dinh ban ghi
Long id,
// ... other fields
) {}Controller dùng @Validated(Create.class) / @Validated(Update.class) — không phải @Valid vì @Valid Jakarta không support group:
@PostMapping
public OrderDto create(
@Validated(Create.class) @RequestBody OrderRequest req) { ... }
@PutMapping("/{id}")
public OrderDto update(
@PathVariable Long id,
@Validated(Update.class) @RequestBody OrderRequest req) { ... }Khi POST: Hibernate Validator chỉ evaluate constraint thuộc Create.class — field id phải null. Khi PUT: chỉ evaluate Update.class — field id phải not-null. Constraint không khai groups thuộc Default group và không apply khi gọi @Validated(Create.class) — chỉ groups chỉ định mới chạy.
Q2Viết custom @ValidSku constraint cho field String sku: SKU phải match pattern ABC-1234 (3 chữ hoa + dấu gạch + 4 chữ số). Liệt kê 3 phần cần viết và pitfall null-handling.▸
@ValidSku constraint cho field String sku: SKU phải match pattern ABC-1234 (3 chữ hoa + dấu gạch + 4 chữ số). Liệt kê 3 phần cần viết và pitfall null-handling.3 phần:
1. Annotation — khai báo @Constraint(validatedBy=...) + 3 attribute bắt buộc theo JSR 380:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SkuValidator.class)
public @interface ValidSku {
String message() default "Invalid SKU (expected: ABC-1234)";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}2. Validator class — implement ConstraintValidator<ValidSku, String>:
public class SkuValidator implements ConstraintValidator<ValidSku, String> {
private static final Pattern SKU_PATTERN =
Pattern.compile("^[A-Z]{3}-\d{4}$");
@Override
public boolean isValid(String value, ConstraintValidatorContext ctx) {
if (value == null) return true; // null la job cua @NotNull
return SKU_PATTERN.matcher(value).matches();
}
}3. Sử dụng:
public record OrderItem(
@NotBlank @ValidSku String sku,
@Min(1) int quantity
) {}Pitfall null-handling: Pattern.matcher(null) throw NullPointerException. Rule: validator luôn return true khi value là null — để @NotNull (constraint riêng biệt) handle null check. Tách trách nhiệm giúp constraint compose độc lập.
Q3Giải thích tại sao không nên inject @Repository vào ConstraintValidator để kiểm tra SKU tồn tại trong DB. Nên xử lý loại validation này ở đâu?▸
@Repository vào ConstraintValidator để kiểm tra SKU tồn tại trong DB. Nên xử lý loại validation này ở đâu?Tại sao không inject DB vào validator — 3 lý do:
- Lifecycle không đảm bảo:
ConstraintValidatorcó thể được khởi tạo trước Spring context hoàn tất (vd khi validate entity trong Hibernate lifecycle). Bean inject là null →NullPointerExceptionkhó trace. - Không reusable ngoài Spring: Unit test, standalone tool, hay library dùng lại validator mà không có Spring context → injection fail.
- Phá single responsibility: Annotation validator chỉ nên kiểm tra format/structure. Business rule "SKU phải tồn tại" là domain logic, không phải validation syntactic.
Cách đúng — tách 2 lớp:
// Annotation: chi check format (pure, no DI)
@NotBlank @ValidSku String sku // "ABC-1234" format
// Service: check existence (business rule)
@Service
public class OrderService {
public OrderDto create(@Valid OrderRequest req) {
for (var item : req.items()) {
if (!skuRepository.existsById(item.sku())) {
throw new SkuNotFoundException(item.sku()); // domain exception
}
}
// ...
}
}Annotation validator là pure function, không có side effect, không phụ thuộc infrastructure. Business validation ném domain exception — caller biết đây là rule nghiệp vụ, không phải format error.
Q4Message '{order.customer.size}' trong @Size(min=2, max=100) được resolve ra sao? Placeholder '{min}' và '{max}' đến từ đâu?▸
'{order.customer.size}' trong @Size(min=2, max=100) được resolve ra sao? Placeholder '{min}' và '{max}' đến từ đâu?Pipeline interpolation của Hibernate Validator:
- Hibernate Validator đọc
messageattribute của constraint:'{order.customer.size}'— nhận ra đây là message key (curly braces, không phải literal). MessageInterpolatortramessages.propertiestheo locale của request (Accept-Languageheader →LocaleResolver). Tìm keyorder.customer.size.- Giá trị tìm được:
Customer name must be between {min} and {max} characters. - Hibernate Validator substitute
{min}và{max}bằng attribute của constraint:@Size(min=2, max=100)→min=2,max=100. - Kết quả:
Customer name must be between 2 and 100 characters.
Điểm quan trọng: {min} / {max} là **attribute placeholder** của constraint — Hibernate Validator tự inject, không cần khai trong messages.properties. Mỗi constraint có set placeholder riêng (@Pattern có {regexp}, @Min có {value}...).
Spring Boot autoconfig MessageSource để đọc messages.properties từ classpath. Nếu thiếu key → Hibernate Validator trả raw key làm message — đây là bug cần CI check bắt sớm.
Q5Service OrderService annotate @Validated. Cơ chế nào khiến constraint trên method parameter được validate? Exception nào throw khi fail — và tại sao khác với exception từ controller?▸
OrderService annotate @Validated. Cơ chế nào khiến constraint trên method parameter được validate? Exception nào throw khi fail — và tại sao khác với exception từ controller?Cơ chế — AOP proxy:
Khi Spring Boot thấy @Validated trên class, MethodValidationPostProcessor (được ValidationAutoConfiguration register) wrap service bean trong một AOP proxy. Mọi lời gọi method đi qua MethodValidationInterceptor trước khi forward xuống method thật:
Caller -> AOP Proxy (MethodValidationInterceptor)
-> Validator.validate(params, groups)
-> constraints OK? -> method body
-> constraints fail? -> throw ConstraintViolationExceptionLưu ý self-call: nếu method trong cùng class gọi method khác trong cùng class (this.findById(id)), lời gọi đi trực tiếp, không qua proxy — constraint không trigger. Đây là AOP proxy limitation chung.
Tại sao exception khác controller:
- Controller: Spring MVC bind + validate
@RequestBodytrước khi vào method, throwMethodArgumentNotValidException(Spring MVC exception, cóBindingResult). - Service: AOP interceptor validate method parameter, throw
ConstraintViolationException(Jakarta Bean Validation exception, cóSet<ConstraintViolation<?>>).
Cả 2 đều cần handler riêng trong @RestControllerAdvice — không dùng chung được vì API của 2 exception khác nhau.
Bài tiếp theo: OpenAPI & springdoc
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
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