Backend Development 15 min read

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

This article explains how to refactor a Spring MVC Controller by introducing a unified response wrapper, leveraging ResponseBodyAdvice for automatic packaging, applying JSR‑303 validation for @RequestBody, @PathVariable and @RequestParam parameters, creating custom validation annotations, and handling business and system exceptions consistently.

IT Xianyu
IT Xianyu
IT Xianyu
Improving Spring Controller Layer: Unified Response Structure, Validation, and Exception Handling

The article begins by describing the typical responsibilities of a Controller in a three‑layer or COLA architecture and points out common problems such as excessive parameter validation logic, duplicated exception handling, and inconsistent response formats.

To address these issues, a unified response structure is introduced via an IResult interface, a ResultEnum enumeration, and a generic Result<T> class that provides 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, constructor omitted
}

@Data @NoArgsConstructor @AllArgsConstructor
public class Result
{
    private Integer code;
    private String message;
    private T data;
    public static
Result
success(T data) { ... }
    public static Result
failed() { ... }
    // other factory methods omitted
}

Using ResponseBodyAdvice , the article shows how to intercept all controller responses before they are written, automatically wrapping them in the unified Result object without modifying existing controller code.

@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;
        }
        return Result.success(body);
    }
}

The article then covers parameter validation using JSR‑303 annotations on DTOs, @Validated on controller methods, and automatic handling of validation failures via MethodArgumentNotValidException and ConstraintViolationException . Sample DTO and controller code illustrate @NotBlank, @Email, @Min/@Max, and custom validation annotations.

@Data
public class TestDTO {
    @NotBlank private String userName;
    @NotBlank @Length(min = 6, max = 20) private String password;
    @NotNull @Email private String email;
}

@RestController("prettyTestController")
@RequestMapping("/pretty")
@Validated
public class TestController {
    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        testService.save(testDTO);
    }
}

For more complex rules, a custom annotation @Mobile and its validator MobileValidator are defined, demonstrating how to plug in bespoke validation logic.

@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 ctx) {
        if (required) return value != null && pattern.matcher(value).matches();
        return value == null || pattern.matcher(value).matches();
    }
}

Finally, custom exceptions ( BusinessException , ForbiddenException ) and a global @RestControllerAdvice are introduced to translate business errors and validation failures into the unified Result format, ensuring that HTTP status codes remain 200 while the response body conveys success or error details.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ExceptionAdvice {
    @ExceptionHandler(BusinessException.class)
    public Result
handleBusinessException(BusinessException ex) {
        return Result.failed(ex.getMessage());
    }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result
handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
        // build error message from BindingResult
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
    }
    @ExceptionHandler(Exception.class)
    public Result
handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

In summary, after applying these refactorings the Controller code becomes concise, each endpoint’s input validation is declarative, responses are consistently wrapped, and all exceptions are centrally handled, allowing developers to focus on business logic.

BackendJavaexception handlingSpringvalidationController
IT Xianyu
Written by

IT Xianyu

We share common IT technologies (Java, Web, SQL, etc.) and practical applications of emerging software development techniques. New articles are posted daily. Follow IT Xianyu to stay ahead in tech. The IT Xianyu series is being regularly updated.

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.