深圳幻海软件技术有限公司 欢迎您!

从零搭建开发脚手架Spring Boot 集成Groovy实现动态加载业务规则

2023-02-27

本文转载自微信公众号「Java大厂面试官」,作者laker。转载本文请联系Java大厂面试官公众号。背景前段时间体验了Zuul的groovyFilter,其实现了动态热加载Filter,可以在不重启应用的情况下新增、修改自己的业务规则,现在我也来仿照Zuul来山寨一个,用于我们日常多变的业务规则中。

本文转载自微信公众号「Java大厂面试官」,作者laker。转载本文请联系Java大厂面试官公众号。

背景

前段时间体验了Zuul的groovy Filter,其实现了动态热加载Filter,可以在不重启应用的情况下新增、修改自己的业务规则,现在我也来仿照Zuul来山寨一个,用于我们日常多变的业务规则中。

需要依赖groovy-all

<dependency> 
     <groupId>org.codehaus.groovy</groupId> 
     <artifactId>groovy-all</artifactId> 
     <version>2.4.12</version> 版本自己去适配哈 
 </dependency> 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

什么是 Groovy?

类似于Python,perl等灵活动态语言,不过它是运行在java平台上,也就是Groovy最后代码会被编译成class字节码文件,集成到web应用或者java应用中,groovy编写的程序其实就是特殊点的Java程序,而且java类库可以在groovy中直接使用。

Groovy 的另一个好处是,它的语法与 Java 语言的语法很相似。

使用体验

先来体验下实现后的成果

1、利用Spring Boot的CommandLineRunner注册SpringBean、GroovyBean

  • 初始化加载项目中RuleFilter的Spring Bean
    • 直接使用@Autowired注解配合List即可获取所有RuleFilter的子类
  • 初始化Groovy动态扫描的监控间隔,目录配置
    • 这里配置的是每5秒检查D:\\laker\\lakernote\\groovy目录下,新增或者修改的文件用于编译加载
    • 初始化也会加载D:\\laker\\lakernote\\groovy目录下文件。
@Component 
public class GroovyRunner implements CommandLineRunner { 
    @Autowired 
    List<RuleFilter> ruleFilterList; 
    @Override 
    public void run(String... args) throws Exception { 
        // 初始化加载项目中RuleFilter的Springbean 
        RuleFilterLoader.getInstance().initSpringRuleFilter(ruleFilterList); 
        try { 
            // 每隔多少秒,扫描目录下的groovy文件 
            RuleFilterFileManager.init(5, "D:\\laker\\lakernote\\groovy"); 
        } catch (Exception e) { 
            e.printStackTrace(); 
            throw new RuntimeException(); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

2、项目内不变的规则以Java实现继承RuleFilter

这个就是普通的Java类,我们把不变的规则以这种方式实现。

@Component 
public class JavaRule extends RuleFilter { 
    /** 
     * 具体规则执行 
     * @param msg 
     */ 
    @Override 
    public void run(String msg) { 
        System.out.println(" === Java 实现的业务规则 order = 1 , msg = " + msg + " === "); 
    } 
    /** 
     * 该规则是否被执行 
     * @return 
     */ 
    @Override 
    public boolean shouldRun() { 
        return true
    } 
 
    /** 
     * 该规则执行的顺序 
     * @return 
     */ 
    @Override 
    public int runOrder() { 
        return 1; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.

3、项目内经常变动的以Groovy来实现

groovy兼容Java语法,可以直接用java语法来写。

public class GroovyRule extends RuleFilter { 
    @Override 
    public void run(String msg) { 
 
        System.out.println(" === Groovy 实现的业务规则 order = " + runOrder() + ", msg = " + msg + " === "); 
    } 
    @Override 
    public boolean shouldRun() { 
        return true
    } 
    @Override 
    public int runOrder() { 
        return 2; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

“然后把这个xxx.java文件丢到我们监控的文件夹即可

4、在合适的位置使用RuleFilterProcessor

这里我写了个Controller用来测试动态加载规则。

@RestController 
@RequestMapping("/groovy"
public class GroovyController { 
    @Autowired 
    private RuleFilterProcessor ruleFilterProcessor; 
 
    @GetMapping() 
    @ApiOperation("测试groovy的动态加载"
    public void transaction(@RequestParam String msg) { 
        ruleFilterProcessor.runRuleFilters(msg); 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

5、启动并验证

我分了几个场景验证如下:

1). 启动程序

浏览器访问:http://localhost:8080/groovy?msg=laker%20666

结果如下:

=== Java 实现的业务规则 order = 1 , msg = laker 666 ===  
=== Groovy 实现的业务规则 order = 2 , msg = laker 666 ===  
  • 1.
  • 2.

2.) 我修改GroovyRule中的runOrder(),把它改为0

“不用重启服务

浏览器访问:http://localhost:8080/groovy?msg=laker%20666

结果如下:

=== Groovy 实现的业务规则 order = 0 , msg = laker 666 ===  
=== Java 实现的业务规则 order = 1 , msg = laker 666 ===  
  • 1.
  • 2.

3). 我新增一个Groovy2Rule然后丢进上面指定的监控文件夹

public class Groovy2Rule extends RuleFilter { 
    @Override 
    public void run(String msg) { 
        System.out.println(" === Groovy 实现的业务规则 order = " + runOrder() + ", msg = " + msg + " === "); 
        List<RuleFilter> ruleFilters = RuleFilterLoader.getInstance().getFilters(); 
        for (RuleFilter ruleFilter : ruleFilters) { 
            System.out.println(ruleFilter.getClass().getName()); 
        } 
    } 
    @Override 
    public boolean shouldRun() { 
        return true
    } 
 
    @Override 
    public int runOrder() { 
        return 3; 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.

不用重启服务

浏览器访问:http://localhost:8080/groovy?msg=laker%20666

结果如下:

 === Groovy 实现的业务规则 order = 0 , msg = laker 666 ===  
 === Java 实现的业务规则 order = 1 , msg = laker 666 ===  
 === Groovy 实现的业务规则 order = 3, msg = laker 666 ===  
com.laker.map.moudle.groovy.javarule.GroovyRule 
com.laker.map.moudle.groovy.javarule.JavaRule 
com.laker.map.moudle.groovy.Groovy2Rule 
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

“这里如果想调用Spring环境中的bean可以借助SpringContextUtil

实现

核心的模块如下

  • RuleFilter :规则过滤器抽象类,用于扩展实现业务规则,供Java和Groovy继承。
  • RuleFilterLoader :规则过滤器加载器,用于加载基于Spring的RuleFilter实现类和动态编译指定文件基于Groovy的RuleFilter实现类。

存储所有的规则过滤器并能动态加载改变的和新增的规则。

  • RuleFilterFileManager : 一个独立线程轮询监听指定目录文件的变化配合RuleFilterLoader ( 规则过滤器加载器)使用。
  • RuleFilterProcessor: 业务规则处理器核心入口

“这四个核心模块都是盗版Zuul的实现。

贴上部分核心代码如下:

RuleFilter.java

public abstract class RuleFilter implements IRule, Comparable<RuleFilter> { 
 
    abstract public int runOrder(); 
 
    @Override 
    public int compareTo(RuleFilter ruleFilter) { 
        return Integer.compare(this.runOrder(), ruleFilter.runOrder()); 
    } 
    ... 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

RuleFilterLoader.java

public class RuleFilterLoader { 
    public boolean putFilter(File file) throws Exception { 
        String sName = file.getAbsolutePath() + file.getName(); 
        if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) { 
            LOG.debug("reloading filter " + sName); 
            filterRegistry.remove(sName); 
        } 
        RuleFilter filter = filterRegistry.get(sName); 
        if (filter == null) { 
            Class clazz = compile(file); 
            if (!Modifier.isAbstract(clazz.getModifiers())) { 
                filter = (RuleFilter) clazz.newInstance(); 
                filterRegistry.put(file.getAbsolutePath() + file.getName(), filter); 
                ruleFilters.clear(); 
                filterClassLastModified.put(sName, file.lastModified()); 
                return true
            } 
        } 
        return false
    } 
    public List<RuleFilter> getFilters() { 
        if (CollUtil.isNotEmpty(ruleFilters)) { 
            return ruleFilters; 
        } 
        ruleFilters.addAll(springRuleFilterList); 
        ruleFilters.addAll(this.filterRegistry.values()); 
        Collections.sort(ruleFilters); 
        return ruleFilters; 
    } 
    private Class compile(File file) throws IOException { 
        GroovyClassLoader loader = getGroovyClassLoader(); 
        Class groovyClass = loader.parseClass(file); 
        return groovyClass; 
    } 
    GroovyClassLoader getGroovyClassLoader() { 
        return new GroovyClassLoader(); 
    } 
    ... 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.

RuleFilterFileManager.java

public class RuleFilterFileManager { 
    public static void init(int pollingIntervalSeconds, String... directories)  { 
        if (INSTANCE == null) INSTANCE = new RuleFilterFileManager(); 
 
        INSTANCE.aDirectories = directories; 
        INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds; 
        INSTANCE.manageFiles(); 
        INSTANCE.startPoller(); 
    } 
    void startPoller() { 
        poller = new Thread("GroovyRuleFilterFileManagerPoller") { 
            @Override 
            public void run() { 
                while (bRunning) { 
                    try { 
                        sleep(pollingIntervalSeconds * 1000); 
                        manageFiles(); 
                    } catch (Exception e) { 
                        e.printStackTrace(); 
                    } 
                } 
            } 
        }; 
        poller.setDaemon(true); 
        poller.start(); 
    } 
   void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException { 
 
        for (File file : aFiles) { 
            RuleFilterLoader.getInstance().putFilter(file); 
        } 
    } 
    void manageFiles() throws Exception, IllegalAccessException, InstantiationException { 
        List<File> aFiles = getFiles(); 
        processGroovyFiles(aFiles); 
    } 
    ... 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.

RuleFilterProcessor.java

@Component 
public class RuleFilterProcessor { 
    public void runRuleFilters(String msg) { 
        List<RuleFilter> list = RuleFilterLoader.getInstance().getFilters(); 
        if (list != null) { 
            list.forEach(ruleFilter -> { 
                if (ruleFilter.shouldRun()) { 
                    ruleFilter.run(msg); 
                } 
            }); 
        } 
    } 

  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.

总结

可以看到使用起来是相当的方便,仅依赖groovy-all,整体代码结构简单。

性能和稳定性未测试,但是这基本就是翻版的Zuul,Zuul都在使用了,应该没什么问题。

参考:

参考了Zuul源码,比较简单,建议大家都去看看。