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.
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.
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";
}
}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"
}Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
