Improving Spring MVC Controllers: Unified Response Structure, Validation, and Exception Handling
The article explains how to simplify Spring MVC controller code by introducing a unified response wrapper, handling String return types with ResponseBodyAdvice, adjusting HttpMessageConverter order, applying JSR‑303 validation for @RequestParam, @PathVariable and @RequestBody, creating custom validation annotations, and implementing global exception handling for consistent API responses.
In Spring MVC, the Controller layer is essential for receiving requests, delegating to Service, handling exceptions, and returning responses, but naïve implementations often suffer from duplicated validation logic, inconsistent error handling, and mismatched response formats.
Unified response structure is introduced via an IResult interface, a ResultEnum enumeration, and a generic Result<T> class that encapsulates code , message , and data fields.
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
}To avoid writing the wrapper in every controller, a ResponseBodyAdvice implementation intercepts the response before the HttpMessageConverter processes it.
@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 controller returns a plain String , the default StringHttpMessageConverter would cause a cast exception; the advice converts the Result to JSON manually, and the endpoint should declare produces = "application/json; charset=UTF-8" .
Another root‑cause of the problem is the order of converters. By moving MappingJackson2HttpMessageConverter to the first position, JSON conversion takes precedence over the string converter.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List
> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}Parameter validation leverages JSR‑303 (Hibernate Validator) and Spring’s @Validated . An 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 methods can then use @Validated on the DTO or on individual parameters such as @PathVariable and @RequestParam :
@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable @Min(1) @Max(20) Integer num) {
return num * num;
}
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
testService.save(testDTO);
}
}For more complex rules, a custom annotation and validator can be created:
@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 pattern.matcher(value).matches();
return !StringUtils.hasText(value) || pattern.matcher(value).matches();
}
}Custom exceptions and global handling provide fine‑grained error responses while keeping HTTP status 200.
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 patterns, controller code becomes concise, validation rules are explicit, responses are uniform, and all exceptions are transformed into the same Result format, allowing developers to focus on business logic.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.