How to Refactor SpringBoot Controllers for Unified Responses and Robust Validation
This article shows how to refactor SpringBoot controller code by introducing a unified Result wrapper, ResponseBodyAdvice, proper HttpMessageConverter ordering, JSR‑303 validation (including custom validators), and centralized exception handling, resulting in concise, maintainable backend code with consistent API responses.
An Excellent Controller Layer Logic
Controller is an indispensable part of three‑tier or COLA architecture, responsible for receiving requests, invoking services, handling exceptions, and returning responses.
Identify Problems
Parameter validation is tightly coupled with business logic, violating single‑responsibility principle.
Same exception may be thrown by many services, causing code duplication.
Inconsistent error and success response formats make API integration unfriendly.
Refactor Controller Layer Logic
Unified Return Structure
Define a generic result interface and enum for common status codes, then implement a generic Result<T> class with static factory methods.
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
// fields and constructor omitted
}
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) { /* ... */ }
public static Result<?> failed() { /* ... */ }
// other factory methods omitted
}Use ResponseBodyAdvice to wrap every controller response automatically.
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> 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);
}
}Handling String Conversion Issue
When the selected converter is StringHttpMessageConverter, the wrapper must be converted to JSON manually, or adjust the converter order so that MappingJackson2HttpMessageConverter is evaluated first.
@Configuration
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
converters.add(0, new MappingJackson2HttpMessageConverter());
}
}Parameter Validation
Spring MVC supports JSR‑303 validation. Use @Validated on controller classes and validation annotations such as @NotBlank, @Min, @Max, @Email on method parameters or DTO fields.
@RestController
@RequestMapping("/pretty")
@Validated
public class TestController {
@GetMapping("/{num}")
public Integer detail(@PathVariable("num") @Min(1) @Max(20) Integer num) {
return num * num;
}
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO dto) {
testService.save(dto);
}
}Validation failures raise MethodArgumentNotValidException (for body) or ConstraintViolationException (for path/query parameters).
Custom Validation Rules
Create a custom annotation and a corresponding validator.
@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<? extends Payload>[] payload() default {};
}
public class MobileValidator implements ConstraintValidator<Mobile, CharSequence> {
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 Unified Exception Handling
Define business‑specific exceptions and handle them centrally with @RestControllerAdvice to return the same Result structure.
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();
String msg = br.getFieldErrors().stream()
.map(e -> e.getField() + ":" + e.getDefaultMessage())
.collect(Collectors.joining(", "));
return Result.failed(ResultEnum.VALIDATE_FAILED.getCode(), msg);
}
@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());
}
}Summary
By introducing a unified Result wrapper, a ResponseBodyAdvice implementation, proper converter ordering, JSR‑303 validation (including custom validators), and centralized exception handling, controller code becomes concise, responsibilities are clearly separated, and API responses are consistent, allowing developers to focus on business logic.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Architect's Guide
Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.
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.
