Refactor Spring Controllers for Unified Responses and Robust Validation
This article explains why Controllers are essential in Spring MVC, identifies common problems such as tangled validation and inconsistent responses, and demonstrates how to introduce a unified return structure, use ResponseBodyAdvice for automatic wrapping, handle String conversion issues, apply JSR‑303 validation, create custom validators, and centralize exception handling to produce cleaner, more maintainable backend code.
Why Controllers Matter
In Spring MVC, a Controller provides external data interfaces and acts as an indispensable supporting role: it receives requests, delegates to services, handles exceptions, and returns responses without containing business logic.
Typical Responsibilities
Receive request and parse parameters
Invoke Service to execute business code (including validation)
Capture business exceptions and give feedback
Return successful response data
Problems with Conventional Controllers
Parameter validation is tightly coupled with business code, violating the Single Responsibility Principle
Repeated exception handling across many controllers
Inconsistent response formats make client integration difficult
Unified Return Structure
Define a common result interface and enum, then wrap all responses in a generic Result<T> object containing a status code, message, and data.
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private Integer code;
private String message;
// getters, setters, constructors 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) {
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 before they are written, wrapping them in Result.success(...). Special handling is added for String return types to avoid type‑casting errors.
@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);
}
}When the converter order places StringHttpMessageConverter before MappingJackson2HttpMessageConverter, the above logic prevents ClassCastException for string responses.
Adjusting Converter Order
Two approaches are shown: manually convert String results to JSON, or reorder the converters so that MappingJackson2HttpMessageConverter is evaluated first.
@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
for (int i = 0; i < converters.size(); i++) {
if (converters.get(i) instanceof MappingJackson2HttpMessageConverter) {
HttpMessageConverter<?> jackson = converters.get(i);
converters.set(i, converters.get(0));
converters.set(0, jackson);
break;
}
}
}
}Parameter Validation with JSR‑303
Spring MVC leverages JSR‑303 (Hibernate Validator) for automatic validation of @RequestBody, @PathVariable, and @RequestParam parameters. Validation failures throw MethodArgumentNotValidException or ConstraintViolationException.
@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
return num * num;
}
@GetMapping("/getByEmail")
public TestDTO getByAccount(@RequestParam @NotBlank @Email String email) {
TestDTO dto = new TestDTO();
dto.setEmail(email);
return dto;
}
}Custom Validation Rules
When built‑in constraints are insufficient, developers can create custom annotations and validators.
@Target({ElementType.METHOD, 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 !StringUtils.hasText(value) || pattern.matcher(value).matches();
}
}Unified Exception Handling
Define custom exceptions and a global @RestControllerAdvice to translate them into the unified Result format, ensuring HTTP status 200 while conveying error details in the response body.
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()); }
}Conclusion
By introducing a unified response wrapper, leveraging ResponseBodyAdvice, applying JSR‑303 validation, creating custom validators, and centralizing exception handling, Controller code becomes concise, responsibilities are clearly separated, and both success and error responses share a consistent structure, greatly improving maintainability and developer experience.
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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
