Designing an Excellent Controller Layer in Spring MVC
This article explains how to improve the Controller layer in Spring applications by separating responsibilities, implementing unified response structures with Result wrappers, handling String response issues via ResponseBodyAdvice, applying parameter validation with JSR‑303, customizing validators, and centralizing exception handling for cleaner, more maintainable backend code.
The article begins by describing the typical responsibilities of a Controller in a three‑tier or COLA architecture: receiving requests, delegating to services, handling exceptions, and forming responses. It points out common problems such as excessive coupling of validation logic, duplicated exception handling, and inconsistent response formats.
Refactoring the Controller Logic
To address these issues, the author proposes a unified return structure using a generic Result class and an IResult interface that defines a status code and message. An enum ResultEnum provides common result codes like SUCCESS, VALIDATE_FAILED, COMMON_FAILED, and FORBIDDEN.
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
{
private Integer code;
private String message;
private T data;
public static
Result
success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
// other factory methods omitted
}With this wrapper, Controllers can simply return Result.success(body) and let a global advice handle the final JSON conversion.
Unified Packaging with ResponseBodyAdvice
The ResponseBodyAdvice interface intercepts the response before the HttpMessageConverter processes it. By implementing supports to always return true and checking the response type in beforeBodyWrite , the advice wraps any non‑Result object into a Result.success instance. Special handling for String responses converts the wrapped object to JSON manually.
@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);
}
}When the StringHttpMessageConverter appears before the JSON converter, the manual conversion prevents ClassCastException errors.
Parameter Validation (JSR‑303)
The article explains using standard validation annotations such as @NotBlank , @Email , @Min , and @Max on DTO fields, combined with @Validated on controller methods. Spring’s RequestResponseBodyMethodProcessor automatically validates the bound objects and throws MethodArgumentNotValidException on failure.
@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 testDTO) {
testService.save(testDTO);
}Custom validators are also demonstrated, such as a @Mobile annotation backed by a MobileValidator that checks Chinese mobile number patterns.
@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
[] 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 context) {
if (required) return isMobile(value);
return !StringUtils.hasText(value) || isMobile(value);
}
private boolean isMobile(CharSequence str) { return pattern.matcher(str).matches(); }
}Custom Exceptions and Global Exception Handling
Two custom runtime exceptions, BusinessException and ForbiddenException , are defined to represent business‑level errors. A @RestControllerAdvice class catches these exceptions as well as validation exceptions, converting them into the unified Result format so that every API response, even error cases, returns HTTP 200 with a structured payload.
@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(Exception.class)
public Result
handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}Conclusion
By applying these refactorings—separating validation, centralizing response wrapping, and handling exceptions globally—the Controller code becomes concise, its responsibilities clear, and the API contract consistent, allowing developers to focus on core business logic.
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.