Standardizing the Controller Layer: Parameter Validation, Unified Response, and Exception Handling in Spring Boot
This article explains how to structure a Spring Boot controller by describing the four parts of a request, demonstrating elegant parameter validation, implementing a unified response wrapper with status codes, and handling both validation and business exceptions through centralized advice and custom exception classes.
Preface
This article introduces the handling of the controller layer; a complete backend request consists of four parts:
Interface address (URL)
Request method (GET, POST, PUT, DELETE, etc.)
Request data (headers and body)
Response data
The 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
1. Controller Layer Parameter Reception (very basic, can be skipped)
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; Spring Boot treats the class as a controller and puts all returned values into the response body.
@RequestMapping: Prefix for all requests under this controller, e.g., /product/product-info.
@GetMapping("/findById"): Marks a GET request accessible via /findById.
@PostMapping("/page"): Marks a POST request.
Parameters: Declaring ProductInfoQueryVo allows Spring to map incoming JSON fields to the object automatically.
size : 1
current : 1
productId : 1
productName : 泡脚2. Unified Status Code
1. Return Format
To cooperate smoothly with the front end, we usually wrap the response with a status code and message. For example, code 1000 means success.
If not wrapped, the response 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
}
}2. Packaging ResultVo
Status codes should be defined in an enum rather than hard‑coded.
First, define a StatusCode interface:
public interface StatusCode {
int getCode();
String getMsg();
}Then create an enum implementing it:
@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 class with convenient constructors:
@Data
public class ResultVo {
// status code
private int code;
// status message
private String msg;
// 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;
}
// only status code
public ResultVo(StatusCode statusCode) {
this.code = statusCode.getCode();
this.msg = statusCode.getMsg();
this.data = null;
}
}Usage changes from return data; to return new ResultVo(data); .
3. Unified Validation
1. Original Approach
Without a unified validation, you would manually check each field:
@Data
public class ProductInfoVo {
// product name
private String productName;
// product price
private BigDecimal productPrice;
// product status
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)));
}2. @Validated Parameter Validation
Adding validation annotations to the VO simplifies the 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) {
ProductInfo productInfo = new ProductInfo();
BeanUtils.copyProperties(vo, productInfo);
return new ResultVo(productInfoService.getOne(new QueryWrapper(productInfo)));
}If validation fails, Spring throws a BindException. Example error response (code 1002):
{
"code": 1002,
"msg": "参数校验失败",
"data": "商品价格不允许为负数"
}3. Optimizing Exception Handling
BindException is intercepted by a @RestControllerAdvice:
@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());
}
}4. Unified Response
1. Unified Wrapper
Implement ResponseBodyAdvice to wrap any non‑ResultVo return value:
@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter methodParameter, Class
> aClass) {
// Do not wrap if the return type is already ResultVo
return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);
}
@Override
public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
Class
> aClass,
ServerHttpRequest request, ServerHttpResponse response) {
// String type needs special handling
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());
}
}
// Wrap other types directly
return new ResultVo(data);
}
}Now a controller method can simply return the PO:
@PostMapping("/findByVo")
public ProductInfo findByVo(@Validated ProductInfoVo vo) {
ProductInfo productInfo = new ProductInfo();
BeanUtils.copyProperties(vo, productInfo);
return productInfoService.getOne(new QueryWrapper(productInfo));
}The response will automatically be formatted as:
{
"code": 1000,
"msg": "请求成功",
"data": {
"productId": 1,
"productName": "泡脚",
"productPrice": 100.00,
"productDescription": "中药泡脚加按摩",
"productStatus": 0
}
}2. Excluding Certain Endpoints
Some endpoints (e.g., health checks) should not be wrapped. Define an annotation:
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {}Modify the advice to skip methods annotated with it:
@Override
public boolean supports(MethodParameter methodParameter, Class
> aClass) {
return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
|| methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));
}Apply to health endpoint:
@RestController
public class HealthController {
@GetMapping("/health")
@NotControllerResponseAdvice
public String health() {
return "success";
}
}5. Unified Exception
Define business exception codes:
@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;
}
}Custom exception class:
@Getter
public class APIException extends RuntimeException {
private int code;
private String msg;
// manual setting
public APIException(StatusCode statusCode, String message) {
super(message);
this.code = statusCode.getCode();
this.msg = statusCode.getMsg();
}
// default uses APP_ERROR
public APIException(String message) {
super(message);
this.code = AppCode.APP_ERROR.getCode();
this.msg = AppCode.APP_ERROR.getMsg();
}
}Centralized exception handling:
@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) {
// log.error(e.getMessage(), e); // TODO: integrate logger
return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
}
}Usage in service or controller:
if (orderMaster == null) {
throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}Result:
{
"code": 2003,
"msg": "订单不存在",
"data": "订单号不存在:1998"
}With this setup, developers only need to throw APIException for business errors, while the framework takes care of consistent response formatting, validation error handling, and logging.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
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.