【后端】使用 Easy Rules 构建灵活的业务规则引擎 — Spring Boot 集成实践

📖目录


引言

在现代软件开发过程中,业务规则往往复杂且多变。为了应对这些挑战,规则引擎成为了一种有效的解决方案。本文将介绍如何使用 Easy Rules 框架,在 Spring Boot 项目中实现一个可扩展、易维护的业务规则管理系统。


一、Easy Rules 基础知识

1.1 Easy Rules 是什么?

Easy Rules 是一个简单而强大的 Java 规则引擎,它允许你定义和执行一系列业务规则。其核心思想是将复杂的业务逻辑从代码中分离出来,使得规则可以独立于应用程序代码进行管理和修改。

1.2 核心概念介绍

  • Rule :表示一条具体的业务规则,通过 @Rule 注解标识。
  • Condition :定义了规则触发的条件,通过 @Condition 注解标识的方法返回 boolean 值。
  • Action :当条件满足时要执行的动作,通过 @Action 注解标识。
  • Priority :规则的优先级,通过 @Priority 注解标识,数值越小优先级越高。
  • RulesEngineParameters:用于配置规则引擎的行为参数,如是否跳过后续规则等。

1.3 解耦业务逻辑的优势

通过引入规则引擎,我们可以实现以下目标:

  • 将业务规则与程序代码解耦
  • 提高系统的灵活性和可维护性
  • 支持动态加载和更新规则
  • 易于测试和调试

二、源码结构与示例分析

本项目提供了两个示例来演示 Easy Rules 的不同应用场景:

示例一:基础用法展示

第一个示例是一个简单的规则应用案例,展示了如何使用 Easy Rules 定义基本的规则、设置条件和指定优先级。

核心代码实现

java 复制代码
// Priority100RequestService.java
@Rule(name = "执行STEP_1", description = "执行第一步处理")
public class Priority100RequestService {
    @Condition
    public boolean condition() {
        return true;
    }

    @Action
    public void action(Facts facts) {
        System.out.println("规则引擎执行STEP_1:开始处理");
        String step1Input = (String) facts.get("STEP_1_INPUT");
        System.out.println("STEP_1入参:" + step1Input);
        // 设置STEP_2的输入参数
        facts.put("STEP_2_INPUT", "来自STEP_1的数据");
        System.out.println("规则引擎执行STEP_1:结束处理");
    }

    @Priority
    public int priority() {
        return 100;
    }
}
java 复制代码
// RuleController.java
@RestController
@RequestMapping("/api/rules")
public class RuleController {
    @GetMapping("/execute")
    public String executeRules() {
        // 创建规则引擎参数
        RulesEngineParameters parameters = new RulesEngineParameters()
            .skipOnFirstAppliedRule(false);
        
        // 创建规则引擎
        RulesEngine rulesEngine = new DefaultRulesEngine(parameters);

        // 创建规则集
        Rules rules = new Rules();
        rules.register(new Priority100RequestService());
        rules.register(new Priority101RequestService());

        // 创建事实
        Facts facts = new Facts();
        facts.put("STEP_1_INPUT", "初始输入数据");

        // 执行规则
        rulesEngine.fire(rules, facts);

        // 返回最终结果
        return facts.get("FINAL_OUTPUT");
    }
}

示例二:高级集成模式

第二个示例更加贴近实际生产环境的需求,采用了工厂方法设计模式来创建规则实例,并结合 Spring 进行依赖注入管理。

关键组件说明

组件 描述
AppCenter 应用中心,负责协调整个规则执行流程,初始化和管理
AutoCreateFactory 工厂类,根据配置自动创建规则对象实例,支持注解扫描
RobotAdapter 机器人适配器,封装多个流程步骤,作为规则执行的入口点
ProcessAdapter 流程适配器,组织多个具体规则,管理规则引擎和规则集
AdapterRule 所有自定义规则需实现此接口
AutoCreateFactory 工厂类,基于@AutoCreate注解自动扫描和创建规则实例,支持按类型和标记分类进行实例化管理,实现规则的动态加载和配置
RefUtils 反射工具类,负责包扫描和类加载,支持从指定包路径下查找带有特定注解的类,为AutoCreateFactory提供底层类扫描支持

核心实现代码

规则接口:

java 复制代码
public interface AdapterRule {
}

规则1:

java 复制代码
// Priority100RequestService.java - 带注解的规则实现
@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)
@Rule(name = "执行STEP_1", description = "执行STEP_1")
public class Priority100RequestService implements AdapterRule {
    @Condition
    public boolean condition() {
        return true;
    }

    @Action
    public void action(Facts facts) {
        System.out.println("执行STEP_1:开始");
        String step1Input = (String) facts.get("STEP_1_INPUT");
        System.out.println("STEP_1入参:" + step1Input);
        facts.put("STEP_2_INPUT", "STEP_1出参=STEP_2入参");
        System.out.println("执行STEP_1:结束");
    }

    @Priority
    public int priority() {
        return 100;
    }
}

规则2:

java 复制代码
@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)
@Rule(name = "执行STEP_1_1", description = "执行STEP_1_1")
public class Priority101RequestService implements AdapterRule {
    @Condition
    public boolean condition() {
        return true;
    }

    @Action
    public void action(Facts facts) {
        System.out.println("执行STEP_1_1:开始");

        String step2Input = (String) facts.get("STEP_2_INPUT");

        System.out.println("STEP_2入参:" + step2Input);

        facts.put("FINAL_OUTPUT", "STEP_2出参");

        System.out.println("执行STEP_1_1:结束");
    }

    @Priority
    public int priority() {
        return 101;
    }
}

规则应用中心:

java 复制代码
// AppCenter.java - 应用中心
@Service
public class AppCenter {
    private static final Map<String, RobotAdapter> APP_MAP = new ConcurrentHashMap<>();

    @PostConstruct
    public void init() {
        APP_MAP.put("MyAppCode", refresh());
    }

    private static final RulesEngineParameters rulesEngineParameters = new RulesEngineParameters()
            .skipOnFirstAppliedRule(false)
            .skipOnFirstFailedRule(true)
            .skipOnFirstNonTriggeredRule(true);

    private static final RulesEngine rulesEngine = new DefaultRulesEngine(rulesEngineParameters);

    public RobotAdapter refresh() {
        RobotAdapter robotAdapter = new RobotAdapter();
        for(ProcessAdapterEnum adapterEnum : ProcessAdapterEnum.values()) {
            Rules rules = new Rules();
            String robotAdapterTemplateSign = adapterEnum.getCode();
            String methodSign = "DEFAULT";
            
            List<AdapterRule> adapterRules = AutoCreateFactory.getInstanceList(
                AdapterRule.class, "MyAppCode", methodSign, robotAdapterTemplateSign);

            rules.register(adapterRules.toArray());
            ProcessAdapter processAdapter = ProcessAdapter.builder()
                    .appCode("appCode")
                    .rulesEngine(rulesEngine)
                    .rules(rules)
                    .build();
            robotAdapter.getAdapters().put(adapterEnum, processAdapter);
        }
        return robotAdapter;
    }
}

规则适配器:

JAVA 复制代码
@Service
@Getter
public class RobotAdapter {

    @Autowired
    private AppCenter appCenter;

    private final Map<ProcessAdapterEnum, ProcessAdapter> adapters = new EnumMap<>(ProcessAdapterEnum.class);

    public <T, U> U adapter(ProcessAdapterEnum processAdapterService, T param) {
        ProcessAdapter processAdapter = this.getAdapters().get(ProcessAdapterEnum.STEP_1);
        if(processAdapter == null) {
            throw new IllegalArgumentException("未注册的ProcessAdapterEnum:" + processAdapterService);
        }

        return processAdapter.adapter(param);
    }

    public <T, U> U adapter(ProcessAdapterEnum processAdapterService, T param, U returnType) {

        U processAdapter = adapter(processAdapterService, param);
        return processAdapter == null ?  returnType : processAdapter;
    }

    @Override
    public String toString() {
        return JSON.toJSONString(this);
    }
}

流程适配器:

JAVA 复制代码
@Builder
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ProcessAdapter {
    private String appCode;
    private String businessLineId;
    private String businessLineName;
    private String robotId;
    private RulesEngine rulesEngine;
    private Rules rules;

    public <T, U> U adapter(T param) {
        Facts facts = new Facts();

        facts.put("STEP_1_INPUT", "STEP_1入参");

        rulesEngine.fire(rules, facts);

        System.out.println("执行结果:" + facts.get("FINAL_OUTPUT"));

        return facts.get("FINAL_OUTPUT");
    }
}

控制器:

java 复制代码
// AdapterController.java - 控制器调用
@RestController
@RequestMapping("/demos/adapter")
public class AdapterController {
    @Autowired
    private AppCenter appCenter;

    @RequestMapping("/execute")
    public String execute() {
        RobotAdapter robotAdapter = appCenter.getRobotAdapter("MyAppCode");
        robotAdapter.adapter(ProcessAdapterEnum.STEP_1, "入参");
        return "";
    }
}

三、关键代码解析

3.1 AutoCreateFactory 工厂类详解

AutoCreateFactory 是一个通用的自动创建工厂类,它提供了一种灵活的机制来动态创建和管理带有 @AutoCreate 注解的类实例。

3.2 核心功能

  • 动态类扫描 : 利用 RefUtils 工具类扫描指定包路径下所有带有 @AutoCreate 注解的类
  • 实例创建管理 : 根据类型 value 来创建和管理类实例
  • 灵活的分类机制 : 支持通过 sign 关键词对实例进行分类和筛选

3.2.1 Sign 标签分类机制

@AutoCreate 属性提供了强大的分类功能,可以理解为给类实例"打标签":

java 复制代码
@AutoCreate(value = "MyAppCode", sign = {"STEP_1", "DEFAULT"}, isSingleton = true)

在上面的例子中,** Priority100RequestService** 类被打上了两个标签:"STEP_1""DEFAULT"

3.2.2 工作原理

  1. 注册阶段 : AutoCreateFactory 扫描所有带有 @AutoCreate 注解的类,并根据 valuesign 属性建立映射关系
  2. 查询阶段 : 当调用 getInstanceList 方法时,可以通过指定 typesigns 参数来筛选符合条件的实例
JAVA 复制代码
   List<AdapterRule> adapterRules = AutoCreateFactory.getInstanceList(
       AdapterRule.class, 
       "MyAppCode",           // 对应 @AutoCreate 的 value
       "DEFAULT",             // 对应 @AutoCreate 的 sign
       robotAdapterTemplateSign  // 另一个维度的 sign 筛选
   );
  1. 匹配逻辑 : 工厂会查找同时满足以下条件的类:
    • value 匹配指定的类型
    • sign 包含所有指定的标签

3.3 使用工厂类创建规则的优势

  • 解耦: 业务逻辑与具体实现类解耦,通过配置即可改变行为
  • 可扩展: 新增规则只需添加带注解的类,无需修改现有代码
  • 灵活筛选: 多维度的标签系统支持复杂的实例筛选需求
  • 性能优化: 支持单例模式,避免重复创建实例

3.4 关键代码

注解,加到每个规则类的上方

JAVA 复制代码
// 自动注册到工厂
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface AutoCreate {

    /**
     * 标记类型名称 支持多个
     * 简单工厂[必须]指定创建类型名称自动创建实例
     */
    String[] value();

    /**
     * 标记分类名称 支持多个
     * 简单工厂[可以]指定标记分类名称自动创建实例
     */
    String[] sign() default {};

    boolean isSingleton() default false;
}

工厂类,自动扫描和创建规则服务的实例

JAVA 复制代码
/**
 * 简单的自动创建工厂
 * 线程安全
 * 支持自动扫描@AutoCreate注解的实现类生成通用工具
 * 不需要在new对象时写繁琐的if else 判断语句
 */
@Slf4j
public class AutoCreateFactory {
    private AutoCreateFactory() {

    }


    /**
     * Map<Class名称,Map<type, Map<二级type,List<实例提供方法>>>>
     */
    private static final Map<String, Map<String, Map<String, Set<Supplier<?>>>>> classzzCostructorMap = new ConcurrentHashMap<>();

    /**
     * 创建单个实例
     * 默认扫描实例父类class所在的包
     */
    public static <T> T getInstance(Class<?> clazz, String type, String... signs) {
        List<T> list = getInstanceList(clazz, type, signs);
        if(CollectionUtils.isEmpty(list)) {
            return null;
        }
        return list.get(0);
    }

    public static <T> T getInstanceInPackage(Package scanPackage, Class<?> clazz, String type, String... signs) {
        List<T> list = getInstanceListInPackage(scanPackage, clazz, type, signs);
        if(CollectionUtils.isEmpty(list)) {
            return null;
        }
        return list.get(0);
    }

    public static <T> List<T> getInstanceList(Class<?> clazz, String type, String... signs) {
        return getInstanceListInPackage(clazz.getPackage(), clazz, type, signs);
    }

    private static <T> List<T> getInstanceListInPackage(Package scanPackage, Class<?> clazz, String type, String... signs) {
        Assert.notNull(clazz, "auto create instance class can not be null");
        Assert.notNull(type, "auto create instance type can not be null");
        String scanPackageName = scanPackage.getName();
        String key = scanPackageName + "-" + clazz.getSimpleName();
        Map<String, Map<String, Set<Supplier<?>>>> constructorMap = classzzCostructorMap.computeIfAbsent(key, k -> getConstructorMap(scanPackageName, clazz));
        Map<String, Set<Supplier<?>>> signConstructorMap = constructorMap.get(type);
        if(signConstructorMap == null) {
            return new ArrayList<>();
        }
        List<String> signList;
        if(ArrayUtils.isEmpty(signs)) {
            signList = new ArrayList<>();
            signList.add(null);
        } else {
            signList = Arrays.asList(signs);
        }
        Set<Supplier<?>> signFitSet = Collections.emptySet();
        for(int i = 0; i < signList.size(); i++) {
            String sign = signList.get(i);
            Set<Supplier<?>> supplierSet = signConstructorMap.computeIfAbsent(sign, k -> new HashSet<>());
            if(i == 0) {
                signFitSet = supplierSet;
            } else {
                signFitSet = Sets.intersection(signFitSet, supplierSet);
            }
        }
        return signFitSet.stream().map(supplier ->  (T) supplier.get()).collect(Collectors.toList());
    }

    /**
     * 扫描所有标记了@AutoCreate注解的实践类并自动注册到工厂
     */
    private static <T> Map<String, Map<String, Set<Supplier<?>>>> getConstructorMap(String scanPackage, Class<T> parentClazz) {
        Map<String, Map<String, Set<Supplier<?>>>> constructorMap = new HashMap<>();
        // 获取制定包路径下的所有实现类@AutoCreate注解的class子类
        Set<Class<T>> allClazzSet = RefUtils.loadClasses(scanPackage, parentClazz, AutoCreate.class);
        for(Class<? extends T> clazz : allClazzSet) {
            try {
                initConstructorMap(constructorMap, clazz);
            } catch (Exception e) {
                log.error("", e);
            }
        }

        return Collections.unmodifiableMap(constructorMap);
    }

    private static <T> void initConstructorMap(Map<String, Map<String, Set<Supplier<?>>>> constructorMap, Class<? extends T> clazz) throws Exception {
        AutoCreate autoCreate = clazz.getAnnotation(AutoCreate.class);
        Constructor<? extends T> constructor = clazz.getDeclaredConstructor () ;
        if(!constructor.isAccessible()) {
            constructor.setAccessible(true);
        }
        Supplier<?> consumer = getSupplier(autoCreate, constructor);
        String[] types = autoCreate.value();
        String[] signs = autoCreate.sign();

        for(String type: types) {
            boolean isInitNulSign = true;
            for(String sign : signs) {
                Map<String, Set<Supplier<?>>> signMap = constructorMap.computeIfAbsent(type, k -> new HashMap<>());
                signMap.computeIfAbsent(sign, k -> new HashSet<>()).add(consumer);
                if (sign == null) {
                    isInitNulSign = false;
                }
            }
            if(isInitNulSign) {
                Map<String, Set<Supplier<?>>> signMap = constructorMap.computeIfAbsent(type, k -> new HashMap<>());
                signMap.computeIfAbsent(null, key -> new HashSet<>()).add(consumer);
            }
        }
    }

    // 获取父类
    private static <T> Supplier<?> getSupplier(AutoCreate autoCreate, Constructor<? extends T> constructor) throws  InstantiationException, IllegalAccessException, InvocationTargetException {
        Supplier<?> consumer;
        if(autoCreate.isSingleton()) {
            final T singletonInstance = constructor.newInstance();
            // 单例
            consumer = () -> singletonInstance;
        } else {
            // 原型
            consumer = () -> {
                try {
                    //T instance = constructor.newInstance();
                    //autowiredProcess(instance, instance.getClass());
                    //return instance;
                    return constructor.newInstance();
                } catch (Exception e) {
                    log.warn(e.getMessage());
                }
                return null;
            };
        }
        return consumer;
    }
}

反射工具类,负责包扫描和类加载

JAVA 复制代码
public class RefUtils {

    public static <T, A extends Annotation> Set<Class<T>> loadClasses(String pack, Class<T> parentClazz, Class<A> annotationClass) {
        Set<Class<T>> clazzSet = new LinkedHashSet<>();
        Set<Class<T>> allClazzSet = loadClasses(pack);
        for (Class<T> clazz : allClazzSet) {
            try {
                if(parentClazz != null && !parentClazz.isAssignableFrom(clazz)) {
                    continue;
                }

                if(annotationClass != null && clazz.getAnnotation(annotationClass) == null) {
                    continue;
                }
                clazzSet.add(clazz);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return clazzSet;
    }

    public static <T> Set<Class<T>> loadClasses(String pack) {
        Set<Class<T>> classes = new LinkedHashSet<>();
        boolean recursive = true;
        String packageDirName = pack.replace('.', '/');
        Enumeration<URL> dirs;
        try {
            dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
            while (dirs.hasMoreElements()) {
                URL url = dirs.nextElement();
                String protocol = url.getProtocol();
                if ("file".equals(protocol)) {
                    String filePath = URLDecoder.decode(url.getFile(), "UTF-8");
                    findClassesInPackageByFile(pack, filePath, recursive, classes);
                }
                if("jar".equals(protocol)) {
                    findClassesInPackageByJar(url, pack, packageDirName, recursive, classes);
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }

        return classes;
    }


    private static <T> void findClassesInPackageByFile(String packageName, final String packagePath, boolean recursive, Set<Class<T>> classes) {
        java.io.File dir = new java.io.File(packagePath);
        if (!dir.exists() || !dir.isDirectory()) {
            return;
        }
        java.io.File[] dirfiles = dir.listFiles(file -> (recursive && file.isDirectory()) || (file.getName().endsWith(".class")));

        for(File file : dirfiles) {
            if (file.isDirectory()) {
                findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), recursive, classes);
            } else {
                String className = file.getName().substring(0, file.getName().length() - 6);
                try {
                    String classPath = packageName + '.' + className;
                    classes.add(loadClass(classPath));
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private static <T> void findClassesInPackageByJar(URL url, String packageName, String packageDirName, final boolean recursive, Set<Class<T>> classes) {
        try {
            java.util.jar.JarFile jar = ((java.net.JarURLConnection) url.openConnection()).getJarFile();
            java.util.Enumeration<java.util.jar.JarEntry> entries = jar.entries();
            while(entries.hasMoreElements()) {
                packageDirName = findClassesByOneJar(packageName, packageDirName, recursive, classes, entries.nextElement());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private static <T> String findClassesByOneJar(String packageName, String packageDirName, boolean recursive, Set<Class<T>> classes, JarEntry jarEntry) {
        String className = jarEntry.getName();
        if (className.charAt(0) == '/') {
            className = className.substring(1);
        }
        if(className.startsWith(packageDirName)) {
            int idx = className.lastIndexOf('/');
            if (idx != -1) {
                packageName = className.substring(0, idx).replace('/', '.');
            }
            if(((idx != -1) && recursive) && className.endsWith(".class") && !jarEntry.isDirectory()) {
                String classNameNew = className.substring(packageName.length() + 1, className.length() - 6);
                try {
                    String classPath = packageName + '.' + classNameNew;
                    classes.add(loadClass(classPath));
                } catch (Exception e) {
                    e.printStackTrace();
                }

            }
        }
        return packageName;
    }

    public static <T> Class<T> loadClass(String classPath) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
        Method forNameMethod = Class.class.getDeclaredMethod("forName", String.class, boolean.class, ClassLoader.class);
        Class<T> clazz = (Class<T>) forNameMethod.invoke(null, classPath, false, classLoader);
        return clazz;
    }
}

四、运行效果

通过访问对应的接口:

  • 示例一:http://localhost:8080/api/rules/execute
  • 示例二:http://localhost:8080/demos/adapter/execute

可以观察到规则按照预设的优先级顺序执行,并且能够正确传递参数和处理业务逻辑。


五、总结

本文通过对 Easy Rules 在 Spring Boot 中的应用进行了深入探讨,不仅介绍了其基本概念及工作原理,还给出了两种典型的应用场景及其对应的实现方案。希望读者能够借此掌握规则引擎的设计思路和技术要点,在今后的实际工作中加以运用。

六、福利资源

完整的代码已上传为附件,可在Intellij IDEA中导入并直接运行,欢迎交流讨论。如果你对该项目感兴趣或者有任何疑问,或者有更好的实现方案,欢迎留言交流!

相关推荐
多喝开水少熬夜2 小时前
算法-哈希表和相关练习-java
java·算法·散列表
昵称为空C2 小时前
SpringBoot基于注解的数据库字段回填方案
spring boot·spring
古城小栈2 小时前
Go 1.25 发布:性能、工具与生态的全面进化
开发语言·后端·golang
Data_Adventure3 小时前
从 TypeScript 到 Java(2):从脚本执行到 main 方法 —— 理解 Java 的程序入口
前端·后端
shayudiandian3 小时前
【Java】面向对象编程
java
Data_Adventure3 小时前
从 TypeScript 到 Java(1):理解类与包结构
前端·后端
FreeCode3 小时前
LangChain1.0智能体开发:上下文工程
后端·langchain·agent
500佰3 小时前
京东后端架构技术,Pipline 设计 解决复杂查询逻辑
后端·架构