How to Refactor Spring Controllers for Unified Responses and Robust Validation

This article explains how to redesign Spring MVC controllers by introducing a unified response wrapper, handling String conversion issues, applying JSR‑303 validation, creating custom validators, and centralizing exception handling to produce clean, maintainable backend code.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
How to Refactor Spring Controllers for Unified Responses and Robust Validation

Understanding the role of Controller

In traditional three‑layer or COLA architectures the Controller layer is an indispensable supporting role: it receives requests, delegates to Service for business logic, handles parameter validation, catches exceptions and returns responses.

Current problems

Parameter validation is tightly coupled with business code, violating the single‑responsibility principle.

Same exception may be thrown in many services, causing code duplication.

Response formats for success and error are inconsistent, making API integration unfriendly.

Refactoring the Controller layer

Unified return structure

Define a generic IResult interface and a ResultEnum enumeration, then implement a Result<T> class with static factory methods success and failed. Controllers can return Result objects instead of raw data.

public interface IResult {
    Integer getCode();
    String getMessage();
}

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    // getters and constructor 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
}

Unified packaging with ResponseBodyAdvice

Implement ResponseBodyAdvice to intercept the body before it is written by the HttpMessageConverter. If the body is already a Result, return it directly; otherwise wrap it with Result.success. Special handling for String responses converts the Result 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);
    }
}

Fixing the String conversion issue

The problem occurs because StringHttpMessageConverter is placed before MappingJackson2HttpMessageConverter. Adjust the converter order either by inserting the Jackson converter at index 0 or by swapping the two converters in WebMvcConfigurer.configureMessageConverters.

Converter order diagram
Converter order diagram
String vs Jackson converter
String vs Jackson converter
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
        converters.add(0, new MappingJackson2HttpMessageConverter());
    }
}

Parameter validation with JSR‑303

Use @Validated on controllers and constraint annotations such as @NotBlank, @Min, @Email on DTO fields. Spring’s RequestResponseBodyMethodProcessor validates the arguments and throws MethodArgumentNotValidException or ConstraintViolationException when validation fails.

@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 example

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

Unified exception handling

Define custom exceptions such as BusinessException and ForbiddenException, then create a @RestControllerAdvice that maps each exception to a Result response. All uncaught exceptions are also wrapped into a failed Result, ensuring the HTTP status is always 200 while business codes indicate the error.

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<?> 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(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 the controller code becomes concise, validation rules are clearly expressed, responses are uniformly packaged, and all exceptions are handled in a consistent way, allowing developers to focus on business logic.

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.

JavaException HandlingvalidationcontrollerSpring MVCUnified response
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.