Mastering Unified Responses and Exception Handling in Spring Boot Controllers
This article walks through building a robust Spring Boot controller layer by standardizing parameter binding, status codes, response wrapping, validation, and exception handling using annotations, enums, ResultVo, ResponseBodyAdvice, and custom exception advice to deliver consistent API responses.
Introduction
This article introduces the four parts of a typical backend request—URL, HTTP method, request data, and response data—and outlines three problems to solve: elegant parameter validation, unified response formatting, and unified exception handling.
1. Controller Parameter Reception
Common GET and POST mappings are shown with @RestController, @RequestMapping, @GetMapping, and @PostMapping. Parameters are bound automatically to a VO object.
@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 : combines @Controller and @ResponseBody.
@RequestMapping : sets a common URL prefix.
@GetMapping and @PostMapping : declare request methods.
Parameter binding : JSON fields map to VO properties automatically.
2. Unified Status Code
Responses are wrapped with a standard structure containing code, msg, and data. Example of raw data and wrapped data are shown.
{
"productId": 1,
"productName": "泡脚",
"productPrice": 100.00,
"productDescription": "中药泡脚加按摩",
"productStatus": 0
} {
"code": 1000,
"msg": "请求成功",
"data": {
"productId": 1,
"productName": "泡脚",
"productPrice": 100.00,
"productDescription": "中药泡脚加按摩",
"productStatus": 0
}
}Define a StatusCode interface, a ResultCode enum, and a ResultVo wrapper class.
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; }
} @Data
public class ResultVo {
private int code;
private String msg;
private Object data;
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;
}
}Controller methods now return new ResultVo(data) instead of raw objects.
@PostMapping("/findByVo")
public ResultVo findByVo(@Validated ProductInfoVo vo) {
ProductInfo productInfo = new ProductInfo();
BeanUtils.copyProperties(vo, productInfo);
return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}3. Unified Validation
Manual validation with if statements is replaced by Bean Validation annotations and @Validated.
@Data
public class ProductInfoVo {
@NotNull(message = "商品名称不允许为空")
private String productName;
@Min(value = 0, message = "商品价格不允许为负数")
private BigDecimal productPrice;
private Integer productStatus;
}When validation fails, Spring throws BindException, which is handled globally.
{
"timestamp": "2020-04-19T03:06:37.268+0000",
"status": 400,
"error": "Bad Request",
"errors": [{
"defaultMessage": "商品价格不允许为负数",
"field": "productPrice",
"rejectedValue": -1,
"code": "Min"
}],
"message": "Validation failed for object='productInfoVo'. Error count: 1",
"path": "/leilema/product/product-info/findByVo"
}The global advice converts this to the unified format:
{
"code": 1002,
"msg": "参数校验失败",
"data": "商品价格不允许为负数"
}4. Unified Response
A ResponseBodyAdvice implementation automatically wraps non‑ ResultVo responses.
@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return !methodParameter.getParameterType().isAssignableFrom(ResultVo.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)) {
try {
return new ObjectMapper().writeValueAsString(new ResultVo(data));
} catch (JsonProcessingException e) {
throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
}
}
return new ResultVo(data);
}
}For endpoints that must return raw data (e.g., health checks), a custom NotControllerResponseAdvice annotation excludes them from wrapping.
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {} @RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
|| methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
}
// ... same beforeBodyWrite as above
}5. Unified Exception
Business exception codes are defined in an AppCode enum implementing StatusCode. An APIException class carries the code, a high‑level message, and a detailed message.
@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; }
} 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();
}
}A global @RestControllerAdvice handles both validation and business exceptions, returning a ResultVo with appropriate code, msg, and data.
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler(BindException.class)
public ResultVo handleBindException(BindException e) {
ObjectError objectError = e.getBindingResult().getAllErrors().get(0);
return new ResultVo(ResultCode.VALIDATE_ERROR, objectError.getDefaultMessage());
}
@ExceptionHandler(APIException.class)
public ResultVo handleAPIException(APIException e) {
return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
}
}Usage example:
if (orderMaster == null) {
throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}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.
Su San Talks Tech
Su San, former staff at several leading tech companies, is a top creator on Juejin and a premium creator on CSDN, and runs the free coding practice site www.susan.net.cn.
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.
