How to Refactor Spring Controllers for Clean, Unified Responses
This article explains how to improve Spring MVC controller logic by separating responsibilities, introducing a unified response structure, handling String return types with ResponseBodyAdvice, applying JSR‑303 validation, creating custom validators, and using custom exceptions with global exception handling to produce concise and maintainable backend code.
What Makes a Good Controller Layer
A controller should receive requests, parse parameters, delegate to a service for business logic, handle exceptions, and return a response without containing core business code.
Problems with Naïve Implementation
Parameter validation is tightly coupled with business logic, violating the single‑responsibility principle.
Repeatedly throwing the same exception across different services leads to duplicated code.
Inconsistent success and error response formats make client integration difficult.
Unified Response Structure
Define a common result interface and enum, then a generic Result<T> class to wrap all responses.
public interface IResult {
Integer getCode();
String getMessage();
}
public enum ResultEnum implements IResult {
SUCCESS(2001, "接口调用成功"),
VALIDATE_FAILED(2002, "参数校验失败"),
COMMON_FAILED(2003, "接口调用失败"),
FORBIDDEN(2004, "没有权限访问资源");
private final Integer code;
private final String message;
// constructor, getters omitted for brevity
}
@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
}ResponseBodyAdvice for Automatic Wrapping
Implement ResponseBodyAdvice to intercept controller return values and wrap them with Result.success(...). The supports method always returns true, and beforeBodyWrite checks if the body is already a Result before wrapping.
@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 new ObjectMapper().writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
return Result.success(body);
}
}Handling String Return Types
When a controller returns String, the default StringHttpMessageConverter prevents JSON conversion, causing a ClassCastException. Two solutions are provided:
Detect String in beforeBodyWrite and manually convert the Result to JSON.
Reorder the message converters so that MappingJackson2HttpMessageConverter is evaluated before StringHttpMessageConverter.
Parameter Validation with JSR‑303
Use Spring’s @Validated together with annotations such as @Min, @Max, @NotBlank, @Email, etc., to automatically validate @PathVariable, @RequestParam, and @RequestBody parameters. Validation failures throw MethodArgumentNotValidException or ConstraintViolationException.
Custom Validation Annotation
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, 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 context) {
if (required && !StringUtils.hasText(value)) return false;
return !StringUtils.hasText(value) || pattern.matcher(value).matches();
}
}Custom Exceptions and Global Exception Handling
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) {
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 separating concerns, using a unified Result wrapper, leveraging ResponseBodyAdvice, applying JSR‑303 validation (including custom validators), and handling exceptions globally, controller code becomes concise, maintainable, and consistent, allowing developers to focus on core 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.
Java Backend Technology
Focus on Java-related technologies: SSM, Spring ecosystem, microservices, MySQL, MyCat, clustering, distributed systems, middleware, Linux, networking, multithreading. Occasionally cover DevOps tools like Jenkins, Nexus, Docker, and ELK. Also share technical insights from time to time, committed to Java full-stack development!
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.
