Backend Development 18 min read

Mastering Spring Boot Controllers: Unified Validation, Response, and Exception Handling

This tutorial explains how to structure a Spring Boot controller layer, validate request parameters with @Validated, wrap responses in a standard ResultVo format, define unified status codes, and handle exceptions globally using @RestControllerAdvice and AOP, while also providing a way to skip wrapping for specific endpoints.

macrozheng
macrozheng
macrozheng
Mastering Spring Boot Controllers: Unified Validation, Response, and Exception Handling

Introduction

This article focuses on the controller layer in a Spring Boot backend, which consists of four parts: interface address (URL), request method (GET, POST, etc.), request data (header and body), and response data.

It addresses three main problems:

How to gracefully validate parameters when a request arrives.

How to uniformly process response data.

How to handle exceptions thrown during business logic execution.

1. Controller Parameter Reception (Basic, can be skipped)

Common requests are divided into

get

and

post

types.

<code>@RestController
@RequestMapping("/product/product-info")
public class ProductInfoController {

    @Autowired
    ProductInfoService productInfoService;

    @GetMapping("/findById")
    public ProductInfoQueryVo findById(Integer id) {
        // ...
    }

    @PostMapping("/page")
    public IPage findPage(Page page, ProductInfoQueryVo vo) {
        // ...
    }
}
</code>

@RestController is equivalent to @Controller + @ResponseBody , which tells Spring Boot to treat the class as a controller and serialize all returned values into the response body.

@RequestMapping defines a common URL prefix for all methods in the controller.

@GetMapping("/findById") maps a GET request to the

/findById

path.

@PostMapping("/page") maps a POST request to the

/page

path.

Method parameters are automatically bound from the incoming JSON payload to the corresponding Java objects (e.g.,

ProductInfoQueryVo

).

2. Unified Status Codes

2.1 Response Format

To cooperate smoothly with the frontend, the backend wraps its data with a status code and message. For example, a successful response uses code

1000

:

<code>{
  "code": 1000,
  "msg": "请求成功",
  "data": { ... }
}
</code>

Without wrapping, the raw data would look like:

<code>{
  "productId": 1,
  "productName": "泡脚",
  "productPrice": 100.00,
  "productDescription": "中药泡脚加按摩",
  "productStatus": 0
}
</code>

2.2 Defining Status Codes

<code>public interface StatusCode {
    int getCode();
    String getMsg();
}
</code>
<code>@Getter
public enum ResultCode implements StatusCode {
    SUCCESS(1000, "请求成功"),
    FAILED(1001, "请求失败"),
    VALIDATE_ERROR(1002, "参数校验失败"),
    RESPONSE_PACK_ERROR(1003, "response返回包装失败");

    private int code;
    private String msg;

    ResultCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}
</code>

2.3 Result Wrapper

<code>@Data
public class ResultVo {
    private int code;          // status code
    private String msg;        // status message
    private Object data;       // payload

    public ResultVo(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    // default success
    public ResultVo(Object data) {
        this.code = ResultCode.SUCCESS.getCode();
        this.msg = ResultCode.SUCCESS.getMsg();
        this.data = data;
    }

    public ResultVo(StatusCode statusCode, Object data) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = data;
    }

    public ResultVo(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = null;
    }
}
</code>

Controller methods now return

ResultVo

instead of raw data:

<code>@PostMapping("/findByVo")
public ResultVo findByVo(@Validated ProductInfoVo vo) {
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}
</code>

3. Unified Validation

3.1 Original Manual Checks (tedious)

Without a unified approach, each controller method contains repetitive

if

checks and throws

APIException

manually.

3.2 Using @Validated

By adding validation annotations to the VO class and annotating the method parameter with

@Validated

, Spring automatically validates the input and throws a

BindException

when constraints fail.

<code>@Data
public class ProductInfoVo {
    @NotNull(message = "商品名称不允许为空")
    private String productName;

    @Min(value = 0, message = "商品价格不允许为负数")
    private BigDecimal productPrice;

    private Integer productStatus;
}

@PostMapping("/findByVo")
public ProductInfo findByVo(@Validated ProductInfoVo vo) {
    // business logic
}
</code>

If validation fails, Spring returns a detailed error JSON (status 400). To keep the frontend contract, we intercept this exception and wrap it into our unified format.

3.3 Global Exception Handling for Validation

<code>@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler(BindException.class)
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }
}
</code>

The frontend now receives a response like:

<code>{
  "code": 1002,
  "msg": "参数校验失败",
  "data": "商品价格不允许为负数"
}
</code>

4. Unified Response

4.1 Automatic Wrapping with ResponseBodyAdvice

Instead of manually returning

new ResultVo(data)

in every method, we implement

ResponseBodyAdvice

to wrap any non‑ResultVo response automatically.

<code>@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // Skip if already ResultVo or annotated with @NotControllerResponseAdvice
        return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
                 || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest request, ServerHttpResponse response) {
        if (returnType.getGenericParameterType().equals(String.class)) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(new ResultVo(data));
            } catch (JsonProcessingException e) {
                throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
            }
        }
        return new ResultVo(data);
    }
}
</code>

Methods that return plain strings need special handling because they cannot be directly wrapped.

4.2 Skipping Wrapping for Specific Endpoints

Some endpoints (e.g., health checks) must return raw strings. Define a marker annotation:

<code>@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {}
</code>

Apply it to methods that should bypass the wrapper:

<code>@RestController
public class HealthController {
    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
}
</code>

5. Unified Exception Handling

Business exceptions are represented by custom codes (e.g., inventory cannot be negative). Define a base exception and corresponding status codes.

<code>@Getter
public enum AppCode implements StatusCode {
    APP_ERROR(2000, "业务异常"),
    PRICE_ERROR(2001, "价格异常");
    private int code;
    private String msg;
    AppCode(int code, String msg) {
        this.code = code;
        this.msg = msg;
    }
}

@Getter
public class APIException extends RuntimeException {
    private int code;
    private String msg;

    public APIException(StatusCode statusCode, String message) {
        super(message);
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
    }

    public APIException(String message) {
        super(message);
        this.code = AppCode.APP_ERROR.getCode();
        this.msg = AppCode.APP_ERROR.getMsg();
    }
}
</code>

Global handler for both validation and business exceptions:

<code>@RestControllerAdvice
public class ControllerExceptionAdvice {

    @ExceptionHandler(BindException.class)
    public ResultVo MethodArgumentNotValidExceptionHandler(BindException e) {
        ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
        return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
    }

    @ExceptionHandler(APIException.class)
    public ResultVo APIExceptionHandler(APIException e) {
        // TODO: log.error(e.getMessage(), e);
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}
</code>

Usage in service or controller code becomes concise:

<code>if (orderMaster == null) {
    throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}
</code>

The response will be:

<code>{
  "code": 2003,
  "msg": "订单不存在",
  "data": "订单号不存在:1998"
}
</code>

Illustrations

Unified response flow
Unified response flow
Health check example
Health check example
WeChat QR code (promotional, omitted)
WeChat QR code (promotional, omitted)
JavaException HandlingValidationSpring BootREST APIControllerResponse Wrapper
macrozheng
Written by

macrozheng

Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.

0 followers
Reader feedback

How this landed with the community

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