Backend Development 18 min read

Improving Spring MVC Controller Logic: Unified Response, Validation, and Exception Handling

This article explains how to refactor Spring MVC controller code by introducing a unified response structure, handling String response issues with ResponseBodyAdvice, applying JSR‑303 validation, creating custom validation annotations, and implementing custom exceptions with global exception handling to produce cleaner, more maintainable backend services.

IT Architects Alliance
IT Architects Alliance
IT Architects Alliance
Improving Spring MVC Controller Logic: Unified Response, Validation, and Exception Handling

In Spring MVC, the Controller layer acts as an essential façade that receives requests, delegates to services, handles exceptions, and returns responses, but tightly coupling validation and response formatting leads to duplicated and error‑prone code.

Key problems identified:

Parameter validation logic is mixed with business code, violating the Single Responsibility Principle.

Repeated exception handling and inconsistent response formats across endpoints.

When using ResponseBodyAdvice , String return types cause a ClassCastException because StringHttpMessageConverter processes them before the wrapper can be applied.

Solution 1 – Unified response structure:

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

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

// Result wrapper
@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
}

All controller methods now return Result objects, and the wrapper is applied automatically.

Solution 2 – ResponseBodyAdvice for automatic wrapping:

@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 new ObjectMapper().writeValueAsString(Result.success(body)); } catch (JsonProcessingException e) { throw new RuntimeException(e); }
        }
        return Result.success(body);
    }
}

To fix the String conversion issue, two approaches are offered: manually converting the Result to JSON when the body is a String , or reordering the HttpMessageConverter list so that MappingJackson2HttpMessageConverter precedes StringHttpMessageConverter :

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

Parameter validation with JSR‑303:

@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 testDTO) { /* ... */ }
}

The validation process is performed by RequestResponseBodyMethodProcessor , which invokes validateIfApplicable and throws MethodArgumentNotValidException on failure.

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
[] 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) { return value != null && pattern.matcher(value).matches(); }
        return value == null || pattern.matcher(value).matches();
    }
}

Custom exceptions and global handling:

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) {
        StringBuilder sb = new StringBuilder("校验失败:");
        for (FieldError fe : ex.getBindingResult().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, controller code becomes concise, validation rules are clearly expressed, responses are consistently wrapped, and all exceptions are centrally managed, allowing developers to focus on business logic.

backendJavaSpringvalidationControllerExceptionHandling
IT Architects Alliance
Written by

IT Architects Alliance

Discussion and exchange on system, internet, large‑scale distributed, high‑availability, and high‑performance architectures, as well as big data, machine learning, AI, and architecture adjustments with internet technologies. Includes real‑world large‑scale architecture case studies. Open to architects who have ideas and enjoy sharing.

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.