Backend Development 17 min read

Refactoring Spring MVC Controller Layer: Unified Response Structure, Validation, and Exception Handling

This article explains how to refactor the Spring MVC Controller layer by unifying response structures, using ResponseBodyAdvice for automatic wrapping, handling parameter validation with JSR‑303, and implementing custom exceptions with centralized exception handling to simplify and standardize API responses.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture Tech Stack
Refactoring Spring MVC Controller Layer: Unified Response Structure, Validation, and Exception Handling

The article introduces a four‑step approach to refactor the Controller layer in a Spring Boot application, aiming to make the code more maintainable and consistent.

Unified Return Structure

A generic IResult interface and ResultEnum enum are defined to standardize status codes and messages. A generic Result<T> class provides static factory methods for success and failure responses.

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

public enum ResultEnum implements IResult {
    SUCCESS(2001, "接口调用成功"),
    VALIDATE_FAILED(2002, "参数校验失败"),
    COMMON_FAILED(2003, "接口调用失败"),
    FORBIDDEN(2004, "没有权限访问资源");
    // getters omitted
}

@Data @NoArgsConstructor @AllArgsConstructor
public class Result
{
    private Integer code;
    private String message;
    private T data;
    public static
Result
success(T data) { ... }
    public static Result
failed() { ... }
    // other factory methods omitted
}

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

Automatic Wrapping with ResponseBodyAdvice

Spring's ResponseBodyAdvice is used to intercept all controller responses and wrap them into the unified Result structure. The advice checks if the body is already a Result and skips wrapping; otherwise it wraps the body.

@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;
        }
        return Result.success(body);
    }
}

Special handling for String return types is added to avoid ClassCastException by converting the Result to JSON when necessary.

Resolving String Conversion Issue

The problem arises because StringHttpMessageConverter is placed before MappingJackson2HttpMessageConverter in the converter list. Two solutions are presented:

In beforeBodyWrite , detect String bodies and manually serialize the Result to JSON using ObjectMapper .

Reorder the converters 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 (JSR‑303)

The article explains how to use Hibernate Validator (via validation-api ) and Spring's @Validated to automatically validate request parameters and DTOs. Validation failures throw MethodArgumentNotValidException or ConstraintViolationException , which are later handled by a global exception handler.

@Data
public class TestDTO {
    @NotBlank private String userName;
    @NotBlank @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 dto) { ... }
}

Custom Validation Annotation

A custom @Mobile annotation and its validator are shown as an example of extending the validation framework for business‑specific rules.

@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 final Pattern pattern = Pattern.compile("^1[34578][0-9]{9}$");
    @Override
    public boolean isValid(CharSequence value, ConstraintValidatorContext ctx) {
        return value != null && pattern.matcher(value).matches();
    }
}

Custom Exceptions and Global Exception Handling

Two custom runtime exceptions ( BusinessException and ForbiddenException ) are defined. A @RestControllerAdvice class catches these exceptions as well as validation exceptions and returns a unified Result payload.

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) { ... }
    @ExceptionHandler(ConstraintViolationException.class)
    public Result
handleConstraintViolationException(ConstraintViolationException ex) { ... }
    @ExceptionHandler(Exception.class)
    public Result
handle(Exception ex) { return Result.failed(ex.getMessage()); }
}

Conclusion

After applying these refactorings, controller code becomes concise, validation rules are declarative, and all responses—including errors—share a consistent format, allowing developers to focus on business logic rather than repetitive boilerplate.

BackendJavaException HandlingValidationSpring MVCResponseBodyAdvice
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.