Backend Development 17 min read

Improving Spring MVC Controllers: Unified Response Structure, Validation, and Exception Handling

This article explains how to refactor Spring MVC controller layers by defining a unified response format, using ResponseBodyAdvice for automatic wrapping, handling String conversion issues, applying parameter validation with JSR‑303, creating custom validators, and implementing centralized exception handling to produce consistent API responses.

Selected Java Interview Questions
Selected Java Interview Questions
Selected Java Interview Questions
Improving Spring MVC Controllers: Unified Response Structure, Validation, and Exception Handling

In Spring MVC, the Controller layer is essential for exposing data interfaces, but naïve implementations often mix validation logic, duplicate exception handling, and return inconsistent response formats.

1. Problems with the current controller implementation

Parameter validation is tightly coupled with business logic, violating the Single Responsibility Principle.

Repeated exception throwing across multiple services leads to duplicated code.

Response structures are inconsistent, making client integration cumbersome.

2. Unified response structure

A generic IResult interface and a Result class are introduced to encapsulate a status code, message, and data payload.

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 for brevity
}

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result
{
    private Integer code;
    private String message;
    private T data;
    // static factory methods for success/failure omitted for brevity
}

3. Automatic response wrapping with ResponseBodyAdvice

The ResponseAdvice class intercepts controller return values before they are written to the HTTP response. It wraps non‑Result objects into Result.success(...) and handles special cases for String return types.

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice
{
    @Override
    public boolean supports(MethodParameter returnType, Class
> converterType) {
        return true; // apply to all controllers
    }
    @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);
    }
}

4. Dealing with String conversion errors

When a controller method returns String , Spring may use StringHttpMessageConverter , causing a ClassCastException if the wrapper object is not a string. The beforeBodyWrite method converts the Result to JSON manually, or the converter order can be adjusted so that MappingJackson2HttpMessageConverter processes the response first.

5. Parameter validation with JSR‑303

Spring Validation (a wrapper around Hibernate Validator) enables declarative validation on @RequestBody , @PathVariable , and @RequestParam parameters. Example DTO with validation annotations:

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

Controller method using the DTO:

@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
    testService.save(testDTO);
}

6. Custom validation rules

For business‑specific checks, a custom annotation and validator can be created. Example of a @Mobile annotation and its validator:

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

7. Centralized exception handling

Custom exceptions such as BusinessException and ForbiddenException are defined, and a ExceptionAdvice class maps them to the unified Result format, ensuring that even error responses follow the same structure and HTTP status remains 200.

@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) {
        // build detailed validation message
        return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), "校验失败: " + ex.getMessage());
    }
    @ExceptionHandler(Exception.class)
    public Result
handle(Exception ex) {
        return Result.failed(ex.getMessage());
    }
}

8. Conclusion

By applying a unified response wrapper, leveraging ResponseBodyAdvice , enforcing declarative validation, adding custom validators, and centralizing exception handling, controller code becomes concise, maintainable, and consistently communicates success or failure to clients.

JavaException HandlingValidationControllerSpring MVCResponse Wrapping
Selected Java Interview Questions
Written by

Selected Java Interview Questions

A professional Java tech channel sharing common knowledge to help developers fill gaps. Follow us!

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.