Backend Development 17 min read

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

This article explains how to refactor Spring MVC controllers by introducing a unified response wrapper, using ResponseBodyAdvice for automatic packaging, handling String conversion issues, applying JSR‑303 validation for request parameters, creating custom validators, and defining centralized exception handling to keep controller code clean and maintainable.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture Tech Stack
Improving Spring Controller Design: Unified Response Structure, Validation, and Exception Handling

Background

In a typical three‑layer or COLA architecture, the Controller layer acts as an indispensable façade that receives HTTP requests, delegates business logic to Services, handles exceptions, and returns responses, but it should not contain core business logic.

Problems with the Current Implementation

Parameter validation is tightly coupled with business code, violating the Single Responsibility Principle.

Repeated exception throwing across different services leads to duplicated code.

Inconsistent success and error response formats make client integration cumbersome.

Unified Response Structure

Define a generic result interface and an enum of common status codes, then implement a Result<T> class that provides static factory methods for success and failure.

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, 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);
    }
    public static Result
failed() { return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null); }
    // other factory methods omitted
}

ResponseBodyAdvice for Automatic Wrapping

Implement ResponseBodyAdvice to intercept the response before it is written and wrap it into Result.success(...) . The advice also handles the special case where the controller returns a String by converting the Result object to JSON.

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

Fixing the String Conversion Issue

When the response type is String , Spring uses StringHttpMessageConverter , which cannot handle the Result object. The advice converts the wrapper to a JSON string, or alternatively the converter order can be adjusted so that MappingJackson2HttpMessageConverter is evaluated first.

Parameter Validation (JSR‑303)

Use standard validation annotations such as @NotBlank , @Email , @Min , and @Max on DTO fields and method parameters. Annotate controllers with @Validated to trigger automatic validation. Validation failures raise MethodArgumentNotValidException or ConstraintViolationException .

@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 dto) {
        testService.save(dto);
    }
}

Custom Validation Annotation

Define a custom annotation (e.g., @Mobile ) and a corresponding validator that checks the phone number format.

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = MobileValidator.class)
public @interface Mobile {
    String message() default "不是一个手机号码格式";
    boolean required() default true;
    Class
[] groups() default {};
    Class
[] payload() default {};
}

public class MobileValidator implements ConstraintValidator
{
    private final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
    private boolean required;
    @Override
    public void initialize(Mobile annotation) { this.required = annotation.required(); }
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
        if (!required && !StringUtils.hasText(value)) return true;
        return pattern.matcher(value).matches();
    }
}

Centralized Exception Handling

Create specific runtime exceptions (e.g., BusinessException , ForbiddenException ) and a @RestControllerAdvice that maps each exception to a unified Result response, ensuring HTTP status 200 for all business errors.

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
handleBusiness(BusinessException ex) { return Result.failed(ex.getMessage()); }
    @ExceptionHandler(ForbiddenException.class)
    public Result
handleForbidden(ForbiddenException ex) { return Result.failed(ResultEnum.FORBIDDEN); }
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result
handleValidation(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
handleOther(Exception ex) { return Result.failed(ex.getMessage()); }
}

Conclusion

After applying the unified response wrapper, automatic validation, custom validators, and centralized exception handling, controller code becomes concise, each endpoint’s contract is explicit, and error handling is consistent, allowing developers to focus on business logic rather than boilerplate concerns.

JavaSpringValidationControllerExceptionHandlingResponseBodyAdvice
Top Architecture Tech Stack
Written by

Top Architecture Tech Stack

Sharing Java and Python tech insights, with occasional practical development tool tips.

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.