SpringBoot Core Annotations: @ControllerAdvice, @RestControllerAdvice, @Validated

SpringBoot developers often misuse @ControllerAdvice, @RestControllerAdvice, and @Validated, leading to uncaught exceptions, ambiguous responses, and validation errors; this article explains each annotation’s underlying mechanism, proper usage for global exception handling, data binding, request preprocessing, and group‑based validation, and provides production‑ready code examples.

Java Tech Workshop
Java Tech Workshop
Java Tech Workshop
SpringBoot Core Annotations: @ControllerAdvice, @RestControllerAdvice, @Validated

Why the three annotations matter

In many SpringBoot projects developers add global exception handling, data binding, or validation superficially, which results in custom exceptions never reaching the handler, return values being view names instead of JSON, and validation failures returning HTTP 500 with stack traces. The root cause is an incomplete understanding of @ControllerAdvice, @RestControllerAdvice and @Validated.

@ControllerAdvice and @RestControllerAdvice

Core principle: AOP interception + exception matching

Both annotations are implemented with Spring AOP. They intercept every @Controller method execution; when an exception is thrown, Spring looks for an @ExceptionHandler method whose parameter type matches the exception exactly, then its superclass, and finally Exception.class. If none matches, the container’s default handling applies.

Production‑grade global exception handler

@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    @ExceptionHandler(BusinessException.class)
    public Result<Void> handleBusinessException(BusinessException e) {
        log.warn("业务异常:code={}, message={}", e.getCode(), e.getMessage());
        return Result.fail(e.getCode(), e.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<Void> handleValidationException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .findFirst()
                .map(FieldError::getDefaultMessage)
                .orElse("参数校验失败");
        log.warn("参数校验失败:{}", message);
        return Result.fail("PARAM_ERROR", message);
    }

    @ExceptionHandler(ConstraintViolationException.class)
    public Result<Void> handleConstraintViolationException(ConstraintViolationException e) {
        String message = e.getConstraintViolations().stream()
                .findFirst()
                .map(ConstraintViolation::getMessage)
                .orElse("参数校验失败");
        log.warn("约束违反:{}", message);
        return Result.fail("PARAM_ERROR", message);
    }

    @ExceptionHandler(HttpMessageNotReadableException.class)
    public Result<Void> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        log.warn("请求体解析失败:{}", e.getMessage());
        return Result.fail("PARAM_ERROR", "请求体格式错误");
    }

    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public Result<Void> handleMethodNotSupported(HttpRequestMethodNotSupportedException e) {
        log.warn("请求方式不支持:{}", e.getMethod());
        return Result.fail("METHOD_NOT_ALLOWED", "不支持的请求方式:" + e.getMethod());
    }

    @ExceptionHandler(Exception.class)
    public Result<Void> handleException(Exception e) {
        log.error("系统异常", e);
        return Result.fail("SYSTEM_ERROR", "系统繁忙,请稍后重试");
    }
}

Custom business exception

@Getter
public class BusinessException extends RuntimeException {
    private final String code;

    public BusinessException(String code, String message) {
        super(message);
        this.code = code;
    }

    public BusinessException(ResultCode resultCode) {
        super(resultCode.getMessage());
        this.code = resultCode.getCode();
    }
}

Service code can simply throw new BusinessException(...) without a try‑catch; the global handler captures it and returns a unified error format.

Additional capabilities of @ControllerAdvice

Beyond exception handling, @ControllerAdvice can provide global data binding with @ModelAttribute and request preprocessing with @InitBinder.

@ControllerAdvice
public class GlobalDataAdvice {
    @ModelAttribute("currentUser")
    public UserInfo getCurrentUser() {
        return SecurityUtils.getCurrentUser();
    }
}
@ControllerAdvice
public class GlobalInitBinderAdvice {
    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.registerCustomEditor(String.class, new StringTrimmerEditor(true));
        binder.registerCustomEditor(LocalDate.class, new LocalDateEditor());
    }
}

@Validated group validation

Why group validation?

When a single DTO is shared between create, update and delete operations, field‑level constraints often conflict (e.g., id must be null on create but non‑null on update). Without groups developers duplicate DTO classes, leading to maintenance overhead.

Define group marker interfaces

public interface CreateGroup {}
public interface UpdateGroup {}
public interface DeleteGroup {}

Apply groups to DTO fields

@Data
public class UserDTO {
    @NotNull(message = "用户ID不能为空", groups = {UpdateGroup.class, DeleteGroup.class})
    @Null(message = "新增时用户ID必须为空", groups = CreateGroup.class)
    private Long id;

    @NotBlank(message = "用户名不能为空", groups = {CreateGroup.class, UpdateGroup.class})
    @Size(min = 2, max = 20, message = "用户名长度2-20", groups = {CreateGroup.class, UpdateGroup.class})
    private String username;

    @NotBlank(message = "密码不能为空", groups = CreateGroup.class)
    @Size(min = 6, max = 32, message = "密码长度6-32", groups = CreateGroup.class)
    private String password;

    @Email(message = "邮箱格式不正确", groups = {CreateGroup.class, UpdateGroup.class})
    private String email;

    @Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确", groups = {CreateGroup.class, UpdateGroup.class})
    private String phone;
}

Use groups in controller methods

@RestController
@RequestMapping("/users")
@RequiredArgsConstructor
public class UserController {
    private final UserService userService;

    @PostMapping
    public Result<UserVO> createUser(@RequestBody @Validated(CreateGroup.class) UserDTO dto) {
        return Result.success(userService.create(dto));
    }

    @PutMapping
    public Result<UserVO> updateUser(@RequestBody @Validated(UpdateGroup.class) UserDTO dto) {
        return Result.success(userService.update(dto));
    }

    @DeleteMapping
    public Result<Void> deleteUser(@RequestBody @Validated(DeleteGroup.class) UserDTO dto) {
        userService.delete(dto.getId());
        return Result.success();
    }
}

Group inheritance for flexible organization

public interface WriteGroup extends CreateGroup, UpdateGroup {}
public interface AllGroup extends WriteGroup, DeleteGroup {}

Controllers can now validate against multiple groups at once:

@PostMapping
public Result<UserVO> create(@RequestBody @Validated(WriteGroup.class) UserDTO dto) {
    // triggers both CreateGroup and UpdateGroup validation
}

Combining group validation with cascading validation

@Data
public class OrderDTO {
    @NotNull(message = "订单ID不能为空", groups = UpdateGroup.class)
    private Long id;

    @NotBlank(message = "订单号不能为空", groups = CreateGroup.class)
    private String orderNo;

    @NotEmpty(message = "订单项不能为空", groups = CreateGroup.class)
    @Valid
    private List<OrderItemDTO> items;
}

@Data
public class OrderItemDTO {
    @NotNull(message = "商品ID不能为空", groups = CreateGroup.class)
    private Long productId;

    @Min(value = 1, message = "数量至少为1", groups = CreateGroup.class)
    private Integer quantity;
}

Method‑level parameter validation

@RestController
@RequestMapping("/users")
@Validated
public class UserController {
    @GetMapping("/{id}")
    public Result<UserVO> getUserById(@PathVariable @NotNull(message = "ID不能为空") @Min(value = 1, message = "ID必须大于0") Long id) {
        return Result.success(userService.getById(id));
    }

    @GetMapping("/page")
    public Result<PageResult<UserVO>> page(@RequestParam @Min(value = 1, message = "页码最小为1") Integer pageNum,
                                             @RequestParam @Min(value = 1, message = "每页条数最小为1") @Max(value = 100, message = "每页条数最大为100") Integer pageSize) {
        return Result.success(userService.page(pageNum, pageSize));
    }
}

Method‑level validation throws ConstraintViolationException, which the global handler already covers.

Full request processing chain

客户端发起请求
    │
    ▼
DispatcherServlet 分发到 Controller
    │
    ▼
@InitBinder 全局数据预处理(字符串去空格、日期转换)
    │
    ▼
@Validated 分组校验(参数校验)
    ├── 校验通过 → 执行业务逻辑
    └── 校验失败 → 抛出 MethodArgumentNotValidException / ConstraintViolationException
    │
    ▼
Controller 方法执行业务逻辑
    ├── 正常返回 → @ModelAttribute 注入公共数据 → 序列化成 JSON 返回
    └── 抛出异常 → @ExceptionHandler 全局异常处理 → 统一错误格式返回

Common pitfalls and solutions

Custom exceptions should extend RuntimeException rather than Exception to avoid mandatory throws declarations and to work correctly with Spring AOP.

Exception‑handler ordering matters; ensure the most specific handler is defined before the generic Exception.class handler, or use @Order to control execution order.

Method‑level validation requires MethodValidationPostProcessor; the controller must be a Spring bean (annotated with @RestController) and not invoke its own methods directly (self‑invocation bypasses the proxy).

When using group validation, any constraint without an explicit groups attribute belongs to the default group and will not be triggered by @Validated(SomeGroup.class). Either assign groups to all constraints or let custom groups extend Default.

Validation failures often return HTTP 500; add @ResponseStatus(HttpStatus.BAD_REQUEST) to the corresponding @ExceptionHandler if you need proper 4xx codes. @RestControllerAdvice also intercepts NoHandlerFoundException (404) when spring.mvc.throw-exception-if-no-handler-found=true is set. Configure this property only when you want custom 404 handling.

Conclusion

The three annotations— @ControllerAdvice / @RestControllerAdvice for global cross‑cutting concerns and @Validated for fine‑grained, group‑based request validation—are foundational to clean SpringBoot applications. Understanding their AOP mechanics, proper exception matching, and how to combine them with data binding and preprocessing lets developers move from “just add the annotation” to a disciplined, maintainable architecture.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

exception handlingSpringBootControllerAdviceGroup ValidationValidatedRestControllerAdvice
Java Tech Workshop
Written by

Java Tech Workshop

Focused on Java backend technologies, sharing fundamentals, multithreading, JVM, the Spring ecosystem, microservices, distributed systems, high concurrency, source‑code analysis, and practical experience. Continuously delivers high‑quality original content, interview guides, and learning roadmaps to help Java developers progress from beginner to advanced, enhancing technical skills and core competitiveness.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.