How to Unify Controller Parameters, Responses, Validation, and Exceptions in Spring Boot

This article explains how to standardize Spring Boot controller handling by receiving parameters, defining unified status codes, applying global validation, wrapping responses consistently, and managing exceptions with custom advice, using annotations, enums, and AOP techniques to improve code readability and front‑end integration.

Java High-Performance Architecture
Java High-Performance Architecture
Java High-Performance Architecture
How to Unify Controller Parameters, Responses, Validation, and Exceptions in Spring Boot

Preface

This article introduces a complete approach to handling the controller layer in Spring Boot. A typical request consists of four parts: URL, HTTP method (GET, POST, PUT, DELETE), request data (header and body), and response data.

1. Controller Parameter Reception (basic, can be skipped)

@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, tells Spring Boot to treat the class as a controller and serialize returned objects.

@RequestMapping : defines the common URL prefix for all endpoints in this controller (e.g., /product/product-info).

@GetMapping("/findById") : marks a GET endpoint accessible via /findById.

@PostMapping("/page") : marks a POST endpoint.

Parameters : declaring ProductInfoQueryVo allows Spring to map incoming JSON fields (e.g., productId) to the object automatically.

size : 1
current : 1

productId : 1
productName : 泡脚

2. Unified Status Codes

1. Return Format

To cooperate smoothly with the front end, the back end wraps responses with a status code and message. For example, code 1000 indicates success.

{
  "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. ResultVo Wrapper

Define a StatusCode interface and an enum ResultCode to hold 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;
    }
}

The ResultVo class provides several constructors for different scenarios.

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

    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, 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

1. Original Manual Checks

@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("商品价格不能为负数");
    }
    ...
}

2. Using @Validated

@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 productInfoService.getOne(new QueryWrapper(productInfo));
}

If validation fails, Spring throws BindException. The following advice converts it to a unified response.

@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. Global Wrapper via ResponseBodyAdvice

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

Endpoints that should not be wrapped can be marked with a custom annotation.

@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));
    }
    ...
}
@RestController
public class HealthController {
    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
}

5. Unified Exception Handling

Define business‑level 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;
    }
}

Create a custom runtime exception that carries both the code and a user‑friendly 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();
    }
}

Finally, a global advice handles both validation and business exceptions.

@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 logging
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}

Usage example:

if (orderMaster == null) {
    throw new APIException(AppCode.ORDER_NOT_EXIST, "订单号不存在:" + orderId);
}

The response will be:

{
  "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 BootControllerResponse wrapper
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.