How to Refactor Spring Controllers for Unified Responses and Robust Validation
This article explains how to redesign Spring MVC controllers by introducing a unified response wrapper, handling String conversion issues, applying JSR‑303 validation, creating custom validators, and centralizing exception handling to produce clean, maintainable backend code.
Understanding the role of Controller
In traditional three‑layer or COLA architectures the Controller layer is an indispensable supporting role: it receives requests, delegates to Service for business logic, handles parameter validation, catches exceptions and returns responses.
Current problems
Parameter validation is tightly coupled with business code, violating the single‑responsibility principle.
Same exception may be thrown in many services, causing code duplication.
Response formats for success and error are inconsistent, making API integration unfriendly.
Refactoring the Controller layer
Unified return structure
Define a generic IResult interface and a ResultEnum enumeration, then implement a Result<T> class with static factory methods success and failed. Controllers can return Result objects instead of raw data.
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
// getters and constructor omitted
}
@Data @NoArgsConstructor @AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) { /* … */ }
public static Result<?> failed() { /* … */ }
// other factory methods omitted
}Unified packaging with ResponseBodyAdvice
Implement ResponseBodyAdvice to intercept the body before it is written by the HttpMessageConverter. If the body is already a Result, return it directly; otherwise wrap it with Result.success. Special handling for String responses converts the Result to JSON.
@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;
}
if (body instanceof String) {
try {
return objectMapper.writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}Fixing the String conversion issue
The problem occurs because StringHttpMessageConverter is placed before MappingJackson2HttpMessageConverter. Adjust the converter order either by inserting the Jackson converter at index 0 or by swapping the two converters in WebMvcConfigurer.configureMessageConverters.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}Parameter validation with JSR‑303
Use @Validated on controllers and constraint annotations such as @NotBlank, @Min, @Email on DTO fields. Spring’s RequestResponseBodyMethodProcessor validates the arguments and throws MethodArgumentNotValidException or ConstraintViolationException when validation fails.
@Data
public class TestDTO {
@NotBlank private String userName;
@NotBlank @Length(min = 6, max = 20) private String password;
@NotNull @Email private String email;
}
@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO dto) {
testService.save(dto);
}
}Custom validation annotation example
@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 false;
return pattern.matcher(value).matches();
}
}Unified exception handling
Define custom exceptions such as BusinessException and ForbiddenException, then create a @RestControllerAdvice that maps each exception to a Result response. All uncaught exceptions are also wrapped into a failed Result, ensuring the HTTP status is always 200 while business codes indicate the error.
public class BusinessException extends RuntimeException {
public BusinessException(String msg) { super(msg); }
}
public class ForbiddenException extends RuntimeException {
public ForbiddenException(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(ForbiddenException.class)
public Result<?> handleForbiddenException(ForbiddenException ex) {
return Result.failed(ResultEnum.FORBIDDEN);
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public Result<?> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult br = ex.getBindingResult();
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fe : br.getFieldErrors()) {
sb.append(fe.getField()).append(":").append(fe.getDefaultMessage()).append(", ");
}
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
}
@ExceptionHandler(ConstraintViolationException.class)
public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
}
@ExceptionHandler(Exception.class)
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}After applying these changes the controller code becomes concise, validation rules are clearly expressed, responses are uniformly packaged, and all exceptions are handled in a consistent way, allowing developers to focus on business logic.
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.
Code Ape Tech Column
Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn
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.
