Refactor Spring Controllers for Unified Responses and Robust Validation

This article explains how to refactor Spring MVC controllers by introducing a unified response wrapper, handling String conversion issues with ResponseBodyAdvice, configuring message converters, applying JSR‑303 validation, creating custom validators and exceptions, and centralizing error handling for cleaner, more maintainable backend code.

Architecture Digest
Architecture Digest
Architecture Digest
Refactor Spring Controllers for Unified Responses and Robust Validation

Understanding the role of Controller

Controller is an indispensable companion in both three‑layer and COLA architectures, responsible for receiving requests, delegating to Service, handling exceptions, and returning responses.

Typical responsibilities

Receive request and parse parameters

Invoke Service for business logic (including validation)

Capture business exceptions and provide feedback

Return successful response

Problems with naive implementation

Parameter validation is tightly coupled with business logic, violating SRP.

Same exception may be thrown from multiple services, causing code duplication.

Inconsistent error and success response formats make client integration difficult.

Unified response structure

Define a generic IResult interface and a ResultEnum enum for common status codes, then implement a generic Result<T> class with static factory methods.

public interface IResult {
    Integer getCode();
    String getMessage();
}
public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    // fields, constructor, getters omitted
}
@Data @NoArgsConstructor @AllArgsConstructor
public class Result<T> {
    private Integer code;
    private String message;
    private T data;
    public static <T> Result<T> success(T data) { /* ... */ }
    public static Result<?> failed() { /* ... */ }
    // other factory methods omitted
}

Controllers can now return Result.success(...) or Result.failed(...) without repetitive boilerplate.

ResponseBodyAdvice for automatic wrapping

Implement ResponseBodyAdvice<Object> to intercept all controller responses. If the body is already a Result, return it unchanged; otherwise wrap it with Result.success(body). Special handling for String responses converts the wrapper to JSON.

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

When String responses cause a ClassCastException, the advice converts the result to a JSON string and the controller method should declare produces = "application/json; charset=UTF-8".

MessageConverter ordering issue

The exception occurs because StringHttpMessageConverter is placed before MappingJackson2HttpMessageConverter. Adjust the converter order in WebMvcConfigurer or insert the Jackson converter at index 0.

@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

Parameter validation with JSR‑303

Use standard annotations such as @NotBlank, @Email, @Min, @Max on DTO fields and enable @Validated on controller classes. Spring’s MethodValidationPostProcessor and MethodValidationInterceptor perform the actual validation.

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

Custom validation annotation

Define a custom annotation @Mobile and its validator MobileValidator to check Chinese mobile numbers.

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

Unified exception handling

Define custom exceptions such as BusinessException and ForbiddenException, then use @RestControllerAdvice to translate them into Result objects.

@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();
        String msg = br.getFieldErrors().stream()
                .map(e -> e.getField() + ":" + e.getDefaultMessage())
                .collect(Collectors.joining(", "));
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), "校验失败:" + msg);
    }
    @ExceptionHandler(ConstraintViolationException.class)
    public Result<?> handleConstraintViolationException(ConstraintViolationException ex) {
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), ex.getMessage());
    }
    @ExceptionHandler(Exception.class)
    public Result<?> handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

After applying these changes, controller code becomes concise, validation rules are clearly expressed on DTOs, and all responses—including errors—share a consistent structure.

StringHttpMessageConverter issue
StringHttpMessageConverter issue
MappingJackson2HttpMessageConverter works
MappingJackson2HttpMessageConverter works
Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

validationexception-handlingspring-bootresponse-body-advice
Architecture Digest
Written by

Architecture Digest

Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.

0 followers
Reader feedback

How this landed with the community

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.