Dynamic Business Rule Validation in Spring Boot 3 Using AOP
This article demonstrates how to decouple business logic from validation in Spring Boot 3 by creating a custom @BusinessValidation annotation, defining rule interfaces, implementing concrete validators, and using an AOP aspect to execute them with configurable fast‑fail and exception handling, complete with code examples and testing.
Introduction
In complex business systems, validation logic often gets tangled with core code, leading to bulky if‑else statements, poor readability, and frequent changes that violate the Open/Closed principle. This is especially problematic in e‑commerce and finance where orders, transactions, and permissions require multi‑dimensional checks.
The article proposes a solution based on Spring AOP: extract validation into independent rule components, annotate methods to combine rules, and support fast‑fail or full‑validation modes. This decouples business logic from validation, improves maintainability, and adapts dynamically to rule changes.
Custom Annotation
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface BusinessValidation {
/** Rules to execute */
Class<? extends ValidationRule>[] rules();
/** Fail fast flag */
boolean failFast() default true;
/** Throw exception on failure */
boolean throwException() default true;
/** Default error message */
String message() default "规则校验失败";
}Rule Interface and Result
/** Business validation rule interface */
public interface ValidationRule {
ValidationResult validate(Object target, Method method, Object[] args);
}
/** Validation result */
public class ValidationResult {
private final boolean valid;
private final String message;
private ValidationResult(boolean valid, String message) {
this.valid = valid;
this.message = message;
}
public static ValidationResult valid() { return new ValidationResult(true, null); }
public static ValidationResult invalid(String message) { return new ValidationResult(false, message); }
public boolean isValid() { return valid; }
public String getMessage() { return message; }
}Example Rule Implementations
@Component
public class OrderAmountValidation implements ValidationRule {
private static final BigDecimal MAX_AMOUNT = new BigDecimal("100000");
@Override
public ValidationResult validate(Object target, Method method, Object[] args) {
BigDecimal amount = ((Order) args[0]).getAmount();
if (amount.compareTo(BigDecimal.ZERO) <= 0) {
return ValidationResult.invalid("订单金额必须大于0");
}
if (amount.compareTo(MAX_AMOUNT) > 0) {
return ValidationResult.invalid("订单金额超过最大限制: " + MAX_AMOUNT);
}
return ValidationResult.valid();
}
}
@Component
public class OrderAddressValidation implements ValidationRule {
private static final Set<String> NO_SUPPORT = Set.of("新疆", "西藏");
@Override
public ValidationResult validate(Object target, Method method, Object[] args) {
String address = ((Order) args[0]).getAddress();
if (NO_SUPPORT.contains(address)) {
return ValidationResult.invalid(String.format("【%s】不支持的邮寄地址", address));
}
return ValidationResult.valid();
}
}Validation Aspect
@Aspect
@Component
public class BusinessValidationAspect implements ApplicationContextAware {
private ApplicationContext context;
@Around("@annotation(validation)")
public Object validateBusinessRules(ProceedingJoinPoint pjp, BusinessValidation validation) throws Throwable {
Object target = pjp.getTarget();
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
Object[] args = pjp.getArgs();
Class<? extends ValidationRule>[] ruleClasses = validation.rules();
List<ValidationRule> rules = new ArrayList<>();
for (Class<? extends ValidationRule> clazz : ruleClasses) {
try {
rules.add(context.getBean(clazz));
} catch (Exception e) {
Objenesis obj = new ObjenesisStd();
rules.add(obj.newInstance(clazz));
}
}
List<String> errors = new ArrayList<>();
for (ValidationRule rule : rules) {
ValidationResult result = rule.validate(target, method, args);
if (!result.isValid()) {
errors.add(result.getMessage());
if (validation.failFast()) {
break;
}
}
}
if (!errors.isEmpty() && validation.throwException()) {
throw new BusinessValidationException(errors);
} else {
System.err.println(errors);
}
return pjp.proceed();
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context;
}
}Exception Handling
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler
public ResponseEntity<?> businessValidationException(BusinessValidationException e) {
return ResponseEntity.ok(e.getErrors());
}
}
public class BusinessValidationException extends RuntimeException {
private List<String> errors = new ArrayList<>();
public BusinessValidationException(List<String> errors) {
super("");
this.errors = errors;
}
public List<String> getErrors() { return errors; }
}Testing the Solution
@Service
public class OrderService {
@BusinessValidation(rules = { OrderAmountValidation.class, OrderAddressValidation.class }, failFast = false)
public void createOrder(Order order) {
System.err.println("订单创建成功");
}
} private final OrderService orderService;
public OrderController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping("/create")
public ResponseEntity<?> create(@RequestBody Order order) {
this.orderService.createOrder(order);
return ResponseEntity.ok(order);
}Result Example
When the method is invoked, the aspect runs the configured validators. If any rule fails, errors are collected and either thrown as BusinessValidationException or logged, depending on the annotation settings.
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.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.
