Backend Development 18 min read

Refactoring Spring MVC Controllers: Unified Return Structure, Response Advice, Parameter Validation, and Global Exception Handling

This article explains how to refactor the Spring MVC controller layer by introducing a unified response format, using ResponseBodyAdvice for automatic wrapping, fixing String conversion issues, applying JSR‑303 validation, creating custom validators, and handling exceptions globally to keep controller code clean and maintainable.

Architecture Digest
Architecture Digest
Architecture Digest
Refactoring Spring MVC Controllers: Unified Return Structure, Response Advice, Parameter Validation, and Global Exception Handling

Spring MVC controllers are essential for exposing data APIs, but naïve implementations often suffer from duplicated logic, scattered validation, and inconsistent error handling.

Four‑step Refactor of the Controller Layer

Standardize the return structure.

Encapsulate response packaging.

Centralize parameter validation.

Define custom exceptions and a unified exception interceptor.

By following these steps the controller code becomes concise, while business logic stays in the service layer.

Unified Return Structure

Define a generic result interface and an enum of 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, "没有权限访问资源");
    private Integer code;
    private String message;
    // getters, setters, 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 controller responses before they are written, wrapping non‑Result objects into Result.success(...) . Special handling for String responses converts the wrapped 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);
    }
}

When the default StringHttpMessageConverter precedes the JSON converter, the above logic prevents ClassCastException by converting the Result to a JSON string.

Fixing Converter Order

Adjust the HttpMessageConverter list so that MappingJackson2HttpMessageConverter is placed before StringHttpMessageConverter , either by inserting it at index 0 or swapping positions in a custom WebMvcConfigurer implementation.

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

Parameter Validation (JSR‑303)

Use @Validated on controller classes and validation annotations such as @NotBlank , @Min , @Max , @Email on DTO fields. Spring automatically validates request bodies and method arguments, throwing MethodArgumentNotValidException or ConstraintViolationException on failure.

@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 constraint annotation (e.g., @Mobile ) and its validator to handle complex rules not covered by built‑in constraints.

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

Custom Exceptions and Global Exception Handler

Create domain‑specific runtime exceptions and a @RestControllerAdvice that maps them to the unified Result format, 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 err : ex.getBindingResult().getFieldErrors()) {
            sb.append(err.getField()).append(":").append(err.getDefaultMessage()).append(", ");
        }
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), sb.toString());
    }
    @ExceptionHandler(Exception.class)
    public Result
handleOther(Exception ex){ return Result.failed(ex.getMessage()); }
}

After applying these refactorings, controller methods contain only business intent, validation rules are declarative, and all responses—including errors—share a consistent JSON structure.

JavaSpringValidationControllerExceptionHandling
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

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.