Backend Development 17 min read

Refactoring Spring MVC Controllers: Unified Response Structure, Parameter Validation, and Centralized Exception Handling

This article explains how to refactor the Spring MVC Controller layer by standardizing response structures, implementing unified response wrapping via ResponseBodyAdvice, handling String conversion issues, applying JSR‑303 parameter validation, creating custom validation rules, and defining custom exceptions with centralized exception handling to simplify controller code.

Java Captain
Java Captain
Java Captain
Refactoring Spring MVC Controllers: Unified Response Structure, Parameter Validation, and Centralized Exception Handling

In Spring MVC the Controller layer provides data interfaces but often mixes business logic, parameter validation, and exception handling, leading to duplicated code and poor separation of concerns.

Problems with the current implementation

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

Repeated exception throwing across services makes code redundant.

Inconsistent success and error response formats hinder client integration.

Unified response structure

Define a generic IResult interface and a ResultEnum enumeration, then implement a Result<T> class with static factory methods such as success and failed .

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
{
    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);
    }
    // other factory methods omitted
}

Unified packaging with ResponseBodyAdvice

Implement ResponseAdvice that intercepts all controller responses, wraps non‑Result objects into Result.success , and handles String return types by converting 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);
    }
}

String conversion issue

When the selected converter is StringHttpMessageConverter , the generic wrapper cannot be written directly, causing a cannot be cast to java.lang.String error. Two solutions are provided: (1) detect String return type in beforeBodyWrite and manually serialize the Result object; (2) reorder the HttpMessageConverter list so that MappingJackson2HttpMessageConverter has higher priority.

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

Parameter validation

Spring’s JSR‑303 support (Hibernate Validator) is used to validate request bodies, path variables and request parameters. Annotations such as @NotBlank , @Min , @Max , @Email are placed on DTO fields, and controllers are annotated with @Validated to trigger automatic validation.

@Data
public class TestDTO {
    @NotBlank private String userName;
    @Length(min = 6, max = 20) private String password;
    @NotNull @Email private String email;
}
@RestController
@Validated
public class TestController {
    @PostMapping("/test-validation")
    public void testValidation(@RequestBody @Validated TestDTO testDTO) {
        testService.save(testDTO);
    }
}

Custom validation rule

Define a custom annotation @Mobile and a corresponding MobileValidator to validate Chinese mobile numbers.

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

Custom exceptions and centralized handling

Define business‑specific exceptions and a ExceptionAdvice that maps them to the unified Result format, ensuring HTTP status 200 for all responses.

public class BusinessException extends RuntimeException {
    public BusinessException(String message) { super(message); }
}
public class ForbiddenException extends RuntimeException {
    public ForbiddenException(String message) { super(message); }
}
@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(Exception.class)
    public Result
handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

Conclusion

After applying these refactorings the controller code becomes concise, validation rules are declarative, responses are consistently wrapped, and exception handling is centralized, allowing developers to focus on business logic.

JavaParameter ValidationControllerSpring MVCcustom-exceptionResponseBodyAdviceUnified Response
Java Captain
Written by

Java Captain

Focused on Java technologies: SSM, the Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading; occasionally covers DevOps tools like Jenkins, Nexus, Docker, ELK; shares practical tech insights and is dedicated to full‑stack Java development.

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.