Refactor Spring Boot Controllers for Cleaner Code and Unified Responses
This article explains the role of Controllers in Spring Boot, identifies common pitfalls such as duplicated validation and inconsistent responses, and demonstrates how to refactor them using a unified response structure, ResponseBodyAdvice, proper message converter ordering, JSR‑303 validation, custom validators, and centralized exception handling.
Understanding the Controller Layer
Controllers act as an indispensable yet supporting role in both traditional three‑layer and modern COLA architectures, handling request reception, delegating to services, catching business exceptions, and returning responses.
Current Problems with Controllers
Parameter validation is tightly coupled with business logic, violating the Single Responsibility Principle.
Repeatedly throwing the same exceptions across multiple services leads to code duplication.
Inconsistent error and success response formats make API integration unfriendly.
Refactoring the Controller Logic
The mall project (SpringBoot3 + JDK 17 + Vue) provides a real‑world example of these improvements.
Unified Return Structure
A common response type with a status code and message helps clients quickly determine success or failure, regardless of whether the front‑end and back‑end are separated.
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
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> 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
}After defining this structure, each Controller can simply return Result.success(...) or Result.failed(...), eliminating repetitive wrapping code.
Unified Wrapper with ResponseBodyAdvice
Spring provides ResponseBodyAdvice to intercept the response before the HttpMessageConverter processes it.
public interface ResponseBodyAdvice<T> {
boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);
@Nullable T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);
}Implementation example:
@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
return true; // apply to all responses
}
@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 new ObjectMapper().writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}Handling String Conversion Issues
When the response type is String, Spring uses StringHttpMessageConverter, which cannot convert a Result object, causing a ClassCastException. Two solutions are presented:
Detect String in beforeBodyWrite and manually serialize the Result to JSON.
Reorder the message converters so that MappingJackson2HttpMessageConverter precedes StringHttpMessageConverter, allowing automatic conversion.
Parameter Validation with JSR‑303
Spring’s validation framework builds on the JSR‑303 validation-api and Hibernate Validator. By annotating DTO fields with constraints such as @NotBlank, @Length, @Email, and using @Validated on controllers, validation is performed automatically without mixing business logic.
@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")
public class TestController {
@PostMapping("/test-validation")
public void testValidation(@RequestBody @Validated TestDTO testDTO) {
testService.save(testDTO);
}
}Custom Validation Rules
When built‑in constraints are insufficient, developers can create custom annotations and validators.
@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<? 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 && !StringUtils.hasText(value)) return false;
return !StringUtils.hasText(value) || pattern.matcher(value).matches();
}
}Custom Exceptions and Global Exception Handling
Define specific runtime exceptions for business errors and permission issues, then handle them centrally with @RestControllerAdvice to produce the unified Result format.
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());
}
}Conclusion
By applying a unified response wrapper, leveraging ResponseBodyAdvice, ordering message converters correctly, and using JSR‑303 validation with custom rules and centralized exception handling, Controller code becomes concise, maintainable, and 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.
macrozheng
Dedicated to Java tech sharing and dissecting top open-source projects. Topics include Spring Boot, Spring Cloud, Docker, Kubernetes and more. Author’s GitHub project “mall” has 50K+ stars.
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.
