Master Unified Responses, Validation, and Exception Handling in Spring Boot Controllers

This article explains how to structure Spring Boot controller requests, apply elegant parameter validation, wrap responses with a consistent ResultVo format, handle validation and business exceptions uniformly using @RestControllerAdvice, and selectively disable wrapping for specific endpoints, providing a complete backend development guide.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
Master Unified Responses, Validation, and Exception Handling in Spring Boot Controllers

Introduction

Today we discuss handling the Controller layer in Java development. A complete backend request consists of four parts: URL, request method, request data (header and body), and response data.

This article solves three problems:

How to gracefully validate parameters when a request is received.

How to uniformly process response data.

How to handle exceptions thrown during business logic execution.

Controller Layer Parameter Reception (Basic, can skip)

Common requests are divided into GET and POST:

@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) {
        ...
    }
}

RestController : Equivalent to @Controller + @ResponseBody, so Spring Boot treats the class as a controller and puts all returned values into the response body.

@RequestMapping : Defines the URL prefix for all endpoints in this controller.

@GetMapping("/findById") : Marks a GET request accessible via /findById.

@PostMapping("/page") : Marks a POST request; the request body is automatically mapped to the VO object.

size : 1
current : 1

productId : 1
productName : 泡脚

Unified Status Code

Response Format

To cooperate smoothly with the frontend, we wrap backend data with a status code and message. Assuming 1000 means success, the raw data looks like:

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

After wrapping:

{
  "code": 1000,
  "msg": "请求成功",
  "data": {
    "productId": 1,
    "productName": "泡脚",
    "productPrice": 100.00,
    "productDescription": "中药泡脚加按摩",
    "productStatus": 0,
  }
}

Wrap ResultVo

Define a status‑code interface and an enum for predefined codes:

public interface StatusCode {
    int getCode();
    String getMsg();
}
@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;
    }
}

ResultVo provides several constructors for different scenarios:

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

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

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

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

    // custom status code without data
    public ResultVo(StatusCode statusCode) {
        this.code = statusCode.getCode();
        this.msg = statusCode.getMsg();
        this.data = null;
    }
}

Controllers now return new ResultVo(data) instead of raw entities.

Unified Validation

Original Approach

Without a unified validation, each endpoint contains manual checks:

@Data
public class ProductInfoVo {
    // 商品名称
    private String productName;
    // 商品价格
    private BigDecimal productPrice;
    // 上架状态
    private Integer productStatus;
}

@PostMapping("/findByVo")
public ProductInfo findByVo(ProductInfoVo vo) {
    if (StringUtils.isNotBlank(vo.getProductName())) {
        throw new APIException("商品名称不能为空");
    }
    if (vo.getProductPrice() != null && vo.getProductPrice().compareTo(new BigDecimal(0)) < 0) {
        throw new APIException("商品价格不能为负数");
    }
    // ...
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}

This approach is verbose and error‑prone.

@Validated Parameter Validation

Using @Validated together with Bean Validation annotations simplifies validation:

@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) {
    ProductInfo productInfo = new ProductInfo();
    BeanUtils.copyProperties(vo, productInfo);
    return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}

If validation fails, Spring throws BindException with details such as:

{
  "timestamp": "2020-04-19T03:06:37.268+0000",
  "status": 400,
  "error": "Bad Request",
  "errors": [{
    "codes": ["Min.productInfoVo.productPrice", "Min.productPrice", "Min.java.math.BigDecimal", "Min"],
    "defaultMessage": "商品价格不允许为负数",
    "field": "productPrice",
    "rejectedValue": -1
  }],
  "message": "Validation failed for object='productInfoVo'. Error count: 1"
}

Optimized Exception Handling

We intercept BindException with @RestControllerAdvice and wrap it into ResultVo using the predefined VALIDATE_ERROR 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());
    }
}

The frontend receives a consistent response:

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

Unified Response

Wrap Response Automatically

Implement ResponseBodyAdvice to wrap any non‑ResultVo return value:

@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) {
        // Special handling for String return type
        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);
    }
}

Now developers can simply return an entity, and the advice will automatically produce the standard JSON structure.

Unified response diagram
Unified response diagram

Non‑Unified Response

Some endpoints (e.g., health checks) must return raw strings. We introduce a custom annotation @NotControllerResponseAdvice to exclude such methods from automatic wrapping.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {}

Update the advice to respect this annotation (see the supports method above). Example:

@RestController
public class HealthController {
    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
}
Health endpoint
Health endpoint

Unified Exception

Define business exception codes by implementing StatusCode:

@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;
    }
}

Create a custom runtime exception that carries both the enum code and a detailed message:

@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();
    }
}

Handle both validation and business exceptions in a single advice:

@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());
    }
}

Usage example in service or controller:

if (orderMaster == null) {
    throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}
{
  "code": 2003,
  "msg": "订单不存在",
  "data": "订单号不存在:1998"
}
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.

JavaException HandlingvalidationSpring BootControllerUnified response
Java High-Performance Architecture
Written by

Java High-Performance Architecture

Sharing Java development articles and resources, including SSM architecture and the Spring ecosystem (Spring Boot, Spring Cloud, MyBatis, Dubbo, Docker), Zookeeper, Redis, architecture design, microservices, message queues, Git, etc.

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.