Standardizing Spring Boot Controller Responses and Error Handling
This guide explains how to structure a Spring Boot controller layer, unify request handling, wrap responses with a consistent ResultVo format, apply @Validated for parameter checks, and use @RestControllerAdvice to centralize exception handling and response formatting across the application.
Overview
The article introduces the four parts of a complete backend request (URL, HTTP method, request data, response data) and focuses on solving three problems in the controller layer: elegant parameter validation, unified response formatting, and consistent exception handling.
Controller Layer Basics
A sample @RestController with @RequestMapping("/product/product-info") demonstrates @GetMapping("/findById") and @PostMapping("/page") methods returning domain objects.
@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, causing Spring Boot to serialize returned objects directly to the response body.
@RequestMapping defines a common URL prefix for all methods in the controller.
@GetMapping and @PostMapping specify the HTTP method and endpoint.
Unified Status Code and Result Wrapper
To provide a consistent response structure, a ResultVo class is introduced with fields code, msg, and data. The StatusCode interface defines getCode() and getMsg(). An enum ResultCode implements this interface with values such as SUCCESS(1000, "请求成功"), FAILED(1001, "请求失败"), and VALIDATE_ERROR(1002, "参数校验失败").
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 final int code;
private final 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(int code, String msg, Object data) { this.code = code; this.msg = msg; this.data = data; }
public ResultVo(Object data) { this.code = ResultCode.SUCCESS.getCode(); this.msg = ResultCode.SUCCESS.getMsg(); this.data = data; }
public ResultVo(StatusCode statusCode) { this.code = statusCode.getCode(); this.msg = statusCode.getMsg(); this.data = null; }
public ResultVo(StatusCode statusCode, Object data) { this.code = statusCode.getCode(); this.msg = statusCode.getMsg(); this.data = data; }
}Controllers now return new ResultVo(data) instead of raw entities, ensuring every response includes code and msg.
Parameter Validation with @Validated
Using Bean Validation annotations such as @NotNull and @Min on a VO class together with @Validated on controller method parameters enables automatic validation. Invalid inputs trigger a BindException.
public class ProductInfoVo {
@NotNull(message = "商品名称不允许为空")
private String productName;
@Min(value = 0, message = "商品价格不允许为负数")
private BigDecimal productPrice;
private Integer productStatus;
}
@PostMapping("/findByVo")
public ResultVo findByVo(@Validated ProductInfoVo vo) {
// business logic
return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}When validation fails, Spring returns a detailed error JSON containing field, rejected value, and message.
Centralized Exception Handling
A @RestControllerAdvice class intercepts BindException and custom APIException to wrap them into ResultVo with appropriate status codes.
@RestControllerAdvice
public class ControllerExceptionAdvice {
@ExceptionHandler(BindException.class)
public ResultVo handleBindException(BindException e) {
ObjectError error = e.getBindingResult().getAllErrors().get(0);
return new ResultVo(ResultCode.VALIDATE_ERROR, error.getDefaultMessage());
}
@ExceptionHandler(APIException.class)
public ResultVo handleAPIException(APIException e) {
// log.error(e.getMessage(), e);
return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
}
}The custom APIException carries a StatusCode and a detailed message, allowing business‑level errors to be reported uniformly.
ResponseBodyAdvice for Automatic Wrapping
Implementing ResponseBodyAdvice<Object> with @RestControllerAdvice enables automatic wrapping of any controller return value (except ResultVo itself or methods annotated with a custom @NotControllerResponseAdvice) into ResultVo. Special handling is provided for String return types.
@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);
}
@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 mapper = new ObjectMapper();
try {
return mapper.writeValueAsString(new ResultVo(data));
} catch (JsonProcessingException e) {
throw new APIException(ResultCode.RESPONSE_PACK_ERROR, e.getMessage());
}
}
return new ResultVo(data);
}
}A custom annotation @NotControllerResponseAdvice can be placed on methods that must bypass this automatic wrapping, e.g., health‑check endpoints that need to return plain strings.
@RestController
public class HealthController {
@GetMapping("/health")
@NotControllerResponseAdvice
public String health() { return "success"; }
}Business Exception Example
An enum AppCode implements StatusCode for business‑level errors, and APIException is thrown with an AppCode and a detailed message.
@Getter
public enum AppCode implements StatusCode {
APP_ERROR(2000, "业务异常"),
PRICE_ERROR(2001, "价格异常");
private final int code;
private final String msg;
AppCode(int code, String msg) { this.code = code; this.msg = msg; }
}
@Getter
public class APIException extends RuntimeException {
private final int code;
private final 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();
}
}When such an exception is thrown, the advice returns a JSON like:
{
"code": 2003,
"msg": "订单不存在",
"data": "订单号不存在:1998"
}This unified approach gives developers a clean way to write controller logic without worrying about response formats or error handling, while front‑end teams receive predictable status codes and messages.
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.
Programmer DD
A tinkering programmer and author of "Spring Cloud Microservices in Action"
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.
