Streamlining Spring Controllers with Unified Responses and Validation
This article explains how to simplify Spring MVC controller code by introducing a unified response format, leveraging ResponseBodyAdvice for automatic wrapping, applying JSR‑303 validation for request parameters, creating custom validators, and handling exceptions globally to keep business logic clean and maintainable.
What Makes a Good Controller Layer
In a typical three‑tier or COLA architecture, the Controller is essential for exposing data interfaces, handling request parsing, delegating to services, catching exceptions, and returning responses.
Current Problems
Parameter validation is tightly coupled with business logic, violating the single‑responsibility principle.
Repeatedly throwing the same exception across services leads to duplicated code.
Inconsistent success and error response formats make client integration difficult.
Refactoring the Controller Layer
Unified Response Structure
Define a common result interface and an enum of standard codes, then implement a generic Result<T> class with static factory methods for success and failure.
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private final Integer code;
private final String message;
// constructor, getters omitted
}
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static Result<?> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
// other factory methods omitted
}Automatic Wrapping with ResponseBodyAdvice
Implement ResponseBodyAdvice to intercept controller return values and wrap them in Result.success(...) unless they are already wrapped.
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}
return Result.success(body);
}
}Parameter Validation
Use JSR‑303 (Hibernate Validator) annotations such as @NotBlank, @Min, @Max, @Email on DTO fields and enable automatic validation with @Validated on controller methods.
@Data
public class TestDTO {
@NotBlank
private String userName;
@NotBlank @Length(min = 6, max = 20)
private String password;
@NotNull @Email
private String email;
}
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO dto) {
testService.save(dto);
}Custom Validation Rules
Create a custom annotation and validator, for example a @Mobile annotation that checks Chinese mobile phone formats.
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
boolean required() default true;
String message() default "不是一个手机号码格式";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
}
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
private boolean required;
private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
@Override
public void initialize(Mobile annotation) { this.required = annotation.required(); }
@Override
public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
if (!required && !StringUtils.hasText(value)) return true;
return pattern.matcher(value).matches();
}
}Custom Exceptions and Global Exception Handling
Define business‑specific runtime exceptions and handle them uniformly with @RestControllerAdvice, mapping each exception to a Result.failed(...) response.
public class BusinessException extends RuntimeException {
public BusinessException(String msg) { super(msg); }
}
@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
@ExceptionHandler(BusinessException.class)
public Result<?> handleBusinessException(BusinessException ex) {
return Result.failed(ex.getMessage());
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleValidationException(MethodArgumentNotValidException ex) {
StringBuilder sb = new StringBuilder("校验失败:");
ex.getBindingResult().getFieldErrors().forEach(e ->
sb.append(e.getField()).append(":").append(e.getDefaultMessage()).append(", "));
return Result.failed(sb.toString());
}
@ExceptionHandler(Exception.class)
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}Conclusion
After applying unified response wrapping, JSR‑303 validation, custom validators, and global exception handling, controller code becomes concise, each endpoint clearly declares its input contracts, and all success and error responses share the same structure, allowing developers to focus on business logic.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.
