Backend Development 21 min read

Standardizing Controller Parameter Handling, Response Wrapping, Validation, and Exception Management in Spring Boot

This article explains how to structure Spring Boot controller layers by receiving request parameters, unifying status codes and response formats, applying @Validated for automatic input validation, using @RestControllerAdvice for centralized exception handling, and optionally disabling wrapping with a custom annotation, providing complete code examples for each step.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Standardizing Controller Parameter Handling, Response Wrapping, Validation, and Exception Management in Spring Boot

Introduction

This article introduces the handling of the controller layer in a Spring Boot backend, describing the four parts of a complete request (URL, method, request data, response data) and solving three main problems: elegant parameter validation, unified response formatting, and consistent exception handling.

1. Controller Layer Parameter Reception (basic)

Common requests are divided into GET and POST. The following code shows a typical controller definition:

@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 , making the class a controller and automatically serializing return values.

@RequestMapping: Sets the common URL prefix /product/product-info for all methods in this controller.

@GetMapping("/findById"): Marks a GET request accessible via /findById .

@PostMapping("/page"): Marks a POST request accessible via /page .

Parameters: When a JSON request is sent, Spring maps fields such as productId to the corresponding properties of ProductInfoQueryVo .

size : 1
current : 1
productId : 1
productName : 泡脚

2. Unified Status Code

1. Return Format

To cooperate smoothly with the frontend, the backend wraps response data with a status code and message. Without wrapping, a response looks like:

{
  "productId": 1,
  "productName": "泡脚",
  "productPrice": 100.00,
  "productDescription": "中药泡脚加按摩",
  "productStatus": 0,
}

After wrapping, it becomes:

{
  "code": 1000,
  "msg": "请求成功",
  "data": {
    "productId": 1,
    "productName": "泡脚",
    "productPrice": 100.00,
    "productDescription": "中药泡脚加按摩",
    "productStatus": 0
  }
}

2. Encapsulating ResultVo

Define a status‑code interface and an enum for standard 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 generic wrapper class:

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

Usage in a controller method:

@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. Manual Validation (original)

Before using @Validated, developers wrote explicit if‑checks and threw APIException for each rule, which is cumbersome.

@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. @Validated Parameter Validation

Adding Bean Validation annotations to the VO class and using @Validated on the controller method lets Spring perform the checks automatically.

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

When 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. Automatic Wrapping

Implement ResponseBodyAdvice to wrap any non‑ResultVo return value. String responses need special handling because they cannot be directly serialized.

@RestControllerAdvice(basePackages = {"com.bugpool.leilema"})
public class ControllerResponseAdvice implements ResponseBodyAdvice
{
    @Override
    public boolean supports(MethodParameter methodParameter, Class
> aClass) {
        return !methodParameter.getParameterType().isAssignableFrom(ResultVo.class);
    }

    @Override
    public Object beforeBodyWrite(Object data, MethodParameter returnType, MediaType mediaType,
                                  Class
> 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);
    }
}

2. Excluding Certain Endpoints

Define an annotation @NotControllerResponseAdvice and modify supports to skip wrapping for methods annotated with it.

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NotControllerResponseAdvice {}

// In the advice class
return !(methodParameter.getParameterType().isAssignableFrom(ResultVo.class)
         || methodParameter.hasMethodAnnotation(NotControllerResponseAdvice.class));

Example of a health‑check endpoint that should not be wrapped:

@RestController
public class HealthController {
    @GetMapping("/health")
    @NotControllerResponseAdvice
    public String health() {
        return "success";
    }
}

5. Unified Exception

Define business‑level error codes and a custom exception class.

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

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

Global exception handler to convert both validation and business exceptions into ResultVo responses:

@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) {
        // TODO: log.error(e.getMessage(), e);
        return new ResultVo(e.getCode(), e.getMsg(), e.getMessage());
    }
}

Usage in service or controller code:

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

Conclusion

By applying the patterns above—standard annotations for request mapping, a unified ResultVo wrapper, centralized validation and exception handling, and an opt‑out annotation for special endpoints—developers can produce consistent, maintainable backend APIs that communicate clearly with frontend teams.

backendJavaException HandlingValidationSpring BootControllerResponse Wrapper
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

0 followers
Reader feedback

How this landed with the community

login 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.