Refactoring Spring Controller Layer: Unified Response Structure, Validation, and Exception Handling
The article presents a step‑by‑step guide to refactor Spring MVC controller code by unifying response formats, applying a global response wrapper, enforcing parameter validation with JSR‑303, and handling custom exceptions through a centralized advice, all illustrated with complete Java examples.
In Spring MVC the Controller layer is a necessary but auxiliary component that handles request parsing, service invocation, exception capture and response generation.
The article proposes a four‑step refactor: unify the return structure, apply a unified wrapper, perform parameter validation, and define custom exceptions with a global interceptor.
It introduces a generic IResult interface and Result class with static factory methods for success and failure, then shows how to use ResponseBodyAdvice to automatically wrap any controller return value into a Result object.
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, constructors omitted for brevity
}
@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
}The advice checks the response type; if it is already a Result it returns it unchanged, otherwise it wraps the value. For String return types it converts the Result to JSON to avoid the "cannot be cast to java.lang.String" error.
@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);
}
}Parameter validation leverages JSR‑303 annotations such as @NotBlank, @Email, @Min, and @Max on DTO fields, combined with @Validated on controller methods. Spring’s RequestResponseBodyMethodProcessor and MethodValidationPostProcessor perform the actual validation and throw MethodArgumentNotValidException or ConstraintViolationException when constraints fail.
@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);
}
}A custom validation annotation example ( @Mobile) demonstrates how to create a reusable constraint with its own validator class.
@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 context) {
if (required) {
return value != null && pattern.matcher(value).matches();
}
return value == null || pattern.matcher(value).matches();
}
}Custom business exceptions ( BusinessException, ForbiddenException) are defined and captured by a @RestControllerAdvice that returns a unified Result payload for all error scenarios.
public class BusinessException extends RuntimeException {
public BusinessException(String message) { super(message); }
}
public class ForbiddenException extends RuntimeException {
public ForbiddenException(String message) { super(message); }
}
@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(Exception.class)
public Result<?> handle(Exception ex) {
return Result.failed(ex.getMessage());
}
}After applying these refactorings the controller code becomes concise, the API contract is consistent, and error handling is centralized, allowing developers to focus on core 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.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
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.
