Backend Development 18 min read

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.

Architect
Architect
Architect
Refactoring Spring Controller Layer: Unified Response Structure, Validation, and Exception Handling

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
{
    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);
    }
    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
{
    @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);
    }
}

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
[] 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 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.

JavaSpringValidationControllerExceptionHandlingResponseBodyAdvice
Architect
Written by

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.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.