基于JAVA+GROOVY敏捷开发(一)

因为之前在项目中使用了Groovy对业务能力进行一些扩展,效果比较好,所以简单记录分享一下,这里你可以了解:


什么是 Groovy ?

Groovy 是用于Java虚拟机的一种敏捷的动态语言,能够与 Java 代码很好地结合,也能用于扩展现有代码, 相对于Java, 它在编写代码的灵活性上有非常明显的提升,Groovy 可以使用其他 Java 语言编写的库。

  • 为什么选用Groovy作为脚本引擎
  • 了解Groovy的基本原理和Java如何集成Groovy
  • 在项目中使用脚本引擎时做的安全和性能优化
  • 实际使用的一些建议

一、为什么使用Groovy

1.1、技术优势

互联网时代随着业务的飞速发展,不仅产品迭代、更新的速度越来越快,个性化需求也是越来越多,如:多维度(条件)的查询、业务流转规则等。办法通常有如下几个方面:

  • 最常见的方式是用代码枚举所有情况,即所有查询维度、所有可能的规则组合,根据运行时参数遍历查找;
  • 使用开源方案,例如drools规则引擎,此类引擎适用于业务基于规则流转,且比较复杂的系统;
  • 使用动态脚本引擎,例如Groovy,JSR223。注:JSR即 Java规范请求,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式请求。任何人都可以提交JST,以向Java平台增添新的API和服务。JSR是Java界的一个重要标准。JSR223提供了一种从Java内部执行脚本编写语言的方便、标准的方式,并提供从脚本内部访问Java资源和类的功能,即为各脚本引擎提供了统一的接口、统一的访问模式。JSR223不仅内置支持Groovy、Javascript、Aviator,而且提供SPI扩展,笔者曾通过SPI扩展实现过Java脚本引擎,将Java代码“脚本化”运行。

1.2、技术选型

对于脚本语言来说,最常见的就是Groovy,JSR233也内置了Groovy。对于不同的脚本语言,选型时需要考虑性能、稳定性、灵活性,综合考虑后选择Groovy,有如下几点原因:

  • 学习曲线平缓,有丰富的语法糖,对于Java开发者非常友好;
  • 技术成熟,功能强大,易于使用维护,性能稳定,被业界看好;
  • 和Java兼容性强,可以无缝衔接Java代码,可以调用Java所有的库

二、Groovy集成

2.1、Groovy基本原理

Groovy的语法很简洁,即使不想学习其语法,也可以在Groovy脚本中使用Java代码,兼容率高达90%,除了lambda、数组语法,其他Java语法基本都能兼容。这里对语法不多做介绍,有兴趣可以自行阅读 https://www.w3cschool.cn/groovy 进行学习。

2.2、引入Groovy依赖

 <dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>2.4.16</version>
</dependency>

<dependency>
    <groupId>org.kohsuke</groupId>
    <artifactId>groovy-sandbox</artifactId>
    <version>1.7</version>
</dependency>

2.3、Groovy脚本引擎

GroovyScriptEngine可以从url(文件夹,远程协议地址,jar包)等位置动态加装resource(script或则Class),同时对
编译后的class字节码进行了缓存,当文件内容更新或者文件依赖的类更新时,会自动更新缓存。

@Component
public class MyGroovyEngine{

    private final GroovyScriptEngine engine;

    private final Map<String, Class<?>> scriptMap = new ConcurrentHashMap(); //文件脚本缓存

    private MyGroovyEngine() throws IOException {
        //初始化
        engine = new GroovyScriptEngine(ResourceUtils.getFile(CommonConstant.COMPONENT_LOCATION).getAbsolutePath()
                , new GroovyClassLoader());
    }

    /**
     * 执行groovy脚本文件
     */
    public Object runScript(final String resourceLocation, final Map<String, Object> bindings) {
        try {
            //优先从缓存中获取编译后的Class
            Class<?> cls = this.scriptMap.get(resourceLocation);
            if (cls == null) {
                //如果缓存没有则加载groovy脚本并编译
                URL url = ResourceUtils.getFile(resourceLocation).toURL();
                GroovyCodeSource source = new GroovyCodeSource(url);
                cls = engine.getGroovyClassLoader().parseClass(source);
                this.scriptMap.putIfAbsent(resourceLocation, cls);
            }
            Script s = (Script) cls.newInstance();
            Binding binding = new Binding();
            if (bindings != null) {
                binding = new Binding(bindings);
            } else {
                binding = new Binding();
            }
            s.setBinding(binding);
            return s.run();
        } catch (Throwable e) {
            throw new ProviderException("Script execution error,script file:" + resourceLocation + "\n", e);
        }
    }

    /**
     * 清除缓存,脚本修改后需要清除缓存
     */
    public void clearCache() {
        this.scriptMap.clear();
    }

}

2.4、编码安全

Groovy会自动引入java.util,java.lang包,方便用户调用,但同时也增加了系统的风险。为了防止用户调用System.exit或Runtime等方法导致系统宕机,以及自定义的Groovy片段代码执行死循环或调用资源超时等问题,Groovy提供了SecureASTCustomizer安全管理者和SandboxTransformer沙盒环境。

@Test
public void testAST() {
    final String script = "import com.alibaba.fastjson.JSONObject;JSONObject object = new JSONObject()";
 
    // 创建SecureASTCustomizer
    final SecureASTCustomizer secure = new SecureASTCustomizer();
    // 禁止使用闭包
    secure.setClosuresAllowed(true);
    List<Integer> tokensBlacklist = new ArrayList<>();
    // 添加关键字黑名单 while和goto
    tokensBlacklist.add(Types.KEYWORD_WHILE);
    tokensBlacklist.add(Types.KEYWORD_GOTO);
    secure.setTokensBlacklist(tokensBlacklist);
    // 设置直接导入检查
    secure.setIndirectImportCheckEnabled(true);
    // 添加导入黑名单,用户不能导入JSONObject
    List<String> list = new ArrayList<>();
    list.add("com.alibaba.fastjson.JSONObject");
    secure.setImportsBlacklist(list);
 
    // statement 黑名单,不能使用while循环块
    List<Class<? extends Statement>> statementBlacklist = new ArrayList<>();
    statementBlacklist.add(WhileStatement.class);
    secure.setStatementsBlacklist(statementBlacklist);
 
    // 自定义CompilerConfiguration,设置AST
    final CompilerConfiguration config = new CompilerConfiguration();
    config.addCompilationCustomizers(secure);
    Binding intBinding = new Binding();
    GroovyShell shell = new GroovyShell(intBinding, config);
 
    final Object eval = shell.evaluate(script);
    System.out.println(eval);
}

2.5、DSL(Json转换)

Groovy支持元数据编程,会将所有groovy代码编译成java对象,如果是groovy代码片段则会动态编译成groovy.lang.Script对象,如果是class类,则会动态编译类实现groovy.lang.GroovyObject接口。编译后对象方法的调用,都会调用GroovyObject接口的invokeMethod方法,下面是我重写invokeMethod方法实现json自动转换的dsl代码。

def invokeMethod(String name, Object args) {
    Object[] objArgs = (Object[]) args
    JSONObject currentJson = (JSONObject) this.getProperty("current_json")
    if (objArgs.size() == 1) {
        Object arg = objArgs[0]
        if (arg instanceof Closure) {
            JSONObject newJson = new JSONObject()
            currentJson.put(name, newJson)
            this.setProperty("current_json", newJson)
            arg.delegate = this
            arg.resolveStrategy = Closure.DELEGATE_ONLY
            arg.run()
            this.setProperty("current_json", currentJson)
        } else {
            currentJson.put(name, arg)
        }
    } else if (objArgs.size() == 2) {
        Object it = objArgs[0]
        Closure single = (Closure) objArgs[1]
        JSONObject newJson = new JSONObject()
        currentJson.put(name, newJson)
        this.setProperty("current_json", newJson)
        single.setProperty("it", it)
        single.call(it)
        this.setProperty("current_json", currentJson)
    } else if (objArgs.size() == 3) {
        Closure eachCl = (Closure) objArgs[2]
        int rule = (int) objArgs[1]
        if (rule == 2) {
            JSONArray array = new JSONArray()
            currentJson.put(name, array)
            objArgs[0].each {
                JSONObject newJson = new JSONObject()
                array.add(newJson)
                eachCl.setProperty("it", it)
                this.setProperty("current_json", newJson)
                eachCl.call(it)
            }
        } else if (rule == 1) {
            JSONObject newJson = new JSONObject()
            currentJson.put(name, newJson)
            objArgs[0].each {
                eachCl.setProperty("it", it)
                this.setProperty("current_json", newJson)
                eachCl.call(it)
            }
        } else if (rule == 3) {
            objArgs[0].each {
                eachCl.setProperty("it", it)
                eachCl.call(it)
            }
        }
        this.setProperty("current_json", currentJson)
    }
}

三、总结

Groovy是一种动态脚本语言,适用于业务变化多又快以及配置化的需求实现。Groovy极易上手,其本质也是运行在JVM的Java代码。Java程序员可以使用Groovy在提高开发效率,加快响应需求变化,提高系统稳定性等方面更进一步。


版权声明:本文为qq_36988277原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。