Refactoring Controller Layer Logic: Unified Response Structure, Parameter Validation, and Global Exception Handling in Spring
This article outlines a four‑step approach to refactor Spring MVC controllers by unifying response structures, centralizing response wrapping via ResponseBodyAdvice, implementing robust parameter validation with JSR‑303, and handling custom exceptions globally, resulting in cleaner, more maintainable backend code.
Controller layers are essential entry points in both traditional three‑tier and modern COLA architectures, but they should delegate business logic to services while handling request parsing, validation, exception capture, and response generation.
Current Issues
Typical controller implementations suffer from excessive parameter validation coupling, duplicated exception handling, and inconsistent response formats, making client integration cumbersome.
Unified Return Structure
Define a generic IResult interface and a ResultEnum enumeration for common status codes, then implement a generic Result<T> class with static factory methods for success and failure responses.
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
}
public class Result
{
private Integer code;
private String message;
private T data;
// static factory methods omitted for brevity
}Unified Response Wrapping
Leverage ResponseBodyAdvice to intercept controller return values before they are written by HttpMessageConverter . The supports method decides whether to apply wrapping, and beforeBodyWrite wraps non‑Result objects into Result.success(...) . Special handling for String responses converts the wrapped object to JSON to avoid cannot be cast to java.lang.String errors.
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
@Override
public boolean supports(MethodParameter returnType, Class
> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
Class
> 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);
}
}String Conversion Issue
When the selected converter is StringHttpMessageConverter , wrapping a Result object leads to a class‑cast exception. Two solutions are presented: (1) detect String return types in beforeBodyWrite and manually serialize to JSON; (2) reorder the HttpMessageConverter list so that MappingJackson2HttpMessageConverter has higher priority.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List
> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}Parameter Validation
Use JSR‑303 ( validation-api ) with Hibernate Validator implementation. Annotate DTO fields with constraints such as @NotBlank , @Length , @Email , and apply @Validated on controller methods. Validation failures throw MethodArgumentNotValidException (for @RequestBody ) or ConstraintViolationException (for @PathVariable / @RequestParam ).
@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 testDTO) {
testService.save(testDTO);
}
}Custom Validation Annotation
Define a custom annotation (e.g., @Mobile ) with a corresponding ConstraintValidator that checks phone number format using a regular expression. The validator reads annotation attributes (e.g., required ) in initialize and implements the validation logic in isValid .
@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
[] payload() default {};
}
public class MobileValidator implements ConstraintValidator
{
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) return pattern.matcher(value).matches();
return !StringUtils.hasText(value) || pattern.matcher(value).matches();
}
}Custom Exceptions and Global Exception Handling
Create domain‑specific exceptions such as BusinessException and ForbiddenException . Use a @RestControllerAdvice to map these exceptions (and validation exceptions) to the unified Result format, ensuring HTTP status 200 while conveying error codes and messages 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) {
StringBuilder sb = new StringBuilder("校验失败:");
for (FieldError fe : ex.getBindingResult().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
After applying these refactorings, controller code becomes concise, validation rules are declaratively expressed, and all responses—including errors—share a consistent structure, allowing developers to focus on business logic while maintaining clean, robust backend services.
Java Captain
Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java 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.