前言
近日有人对我说想要在页面上写一些groovy脚本,跑任务时调用,目前他的groovy都是一些简单的单行脚本,而他想实现对稍微复杂的groovy脚本的支持(20行以内),最好是能支持在groovy中调用系统提供的某些服务,问我有没有好的办法。
听到他有这个想法,顿时想起了,在页面上撸代码可是老本行了,任职于前公司时,接手负责过一个核心链路上的应用,该应用日常秒级高峰调用量在10w+,其中每次调用都会执行一些groovy脚本,而这些脚本都是由用户(业务开发的同学)写的包含一定数据转换逻辑的代码,这里不说这种使用方式的好坏,只谈谈如何处理groovy脚本。
在该系统中,处理groovy脚本的代码非常冗长,主要涉及到其中的两个类,这两个类都有2000多行代码,每次需要对这两个类做修改都非常难受,代码逻辑复杂、类的职责太多、没有领域建模、测试困难都是这两个类难以维护的原因。
虽然是groovy脚本,但是在编写groovy代码片断时依旧使用的是java的语法,抛开具体的业务场景,只谈谈groovy在该系统中的应用,该系统中的groovy脚本分为以下三类:
- 生产者模型:表示一个DTO类,生产者模型是通过从数据库中查询出类名以及类所包含的字段信息,最后通过velocity模板转换成完整的groovy class代码,并通过GroovyClassLoader做编译,模型之间可以有依赖关系,即A模型可依赖B模型,生产者模型是由用户在后台管理系统的页面上通过输入框输入模型名以及每个字段的信息配置的(也可通过java类生成配置),管理系统会将其存入不同的表中
- 消费者模型:与生产者模型几乎一样,只是用途不同
- 转换脚本:用于将多个生产者模型转换成消费者模型,用户通过groovy代码片断的方式编写模型之间的转换逻辑
当时的那两个类中,做了所有的这三类groovy类的生成相关的代码,其中包括:
- 从数据库的多个表中查出生产者模型、消费者模型以及转换脚本的配置
- 处理脚本的文本内容,并得到velocity的context
- 通过velocity对每种类型的groovy脚本模板做渲染,得到完整的groovy类文件
- 通过GroovyClassLoader对groovy类文件做编译加载,得到Class对象并存储在ConcurrentHashMap中
究其原因,该系统中没有实现一个简单可扩展的groovy脚本片断处理框架。
如何设计
基于以上的分析,我们是可以通过先实现基本的脚本处理框架,再基于该脚本处理框架来处理groovy代码片断以及生产者模型和消费者模型,但在实现此框架之前,我们需要先对问题做抽象和建模。
接下来我们看看想要达到什么样的效果,以转换脚本为例,简单起见,使用一下简单的模型表示,假设有如下velocity模板(每种类型的脚本对应的velocity模板不一样):
package $packageName;
import cn.yxffcode.script.core.Executable;
import java.util.*;
$import
public class $className implements Executable {
public Object execute(Map<String, Object> context) {
$expression
}
}
有了此模板后,我们再看一个简单的脚本片断:
import com.google.common.collect.Lists;
List<String> list = Lists.newArrayList();
list.add(context.get("i"));
return list;
对于上面的groovy代码片断,我们需要支持它能正确运行,初看之下,很简单,将代码片断中的import与代码逻辑按行分离开,用velocity做渲染即可,但是我们忽略了前面说的复杂逻辑,这只是个为了说明本文的设计思路而写的简单的示例,真实的groovy处理所使用的代码模板都要比这个要复杂得多。
有了模板和代码片断,我们就有了脚本处理中的核心实体,对于脚本处理成groovy类的过程,可抽象如下:

- Script:表示脚本代码片断的实体
- CodeTemplate:表示代码模板的实体,包括模板内容
- SourceBuilder:用于将多个代码片断结合代码模板,组装成一个完整的groovy类
这样似乎就能够用于处理前面的groovy片断了,只需要在SourceBuilder类中将import语句和其它语句按行分离开,并作为模板引擎的变更做模板渲染即可,但是模板可能会非常复杂:
package $packageName;
import cn.yxffcode.script.core.Executable;
import java.util.*;
$imports
${outputClassPackage}.${outputClass}; //导入输入类
public class $className implements Executable {
public $outputClass execute(Map<String, Object> context) {
$outputClass result = new $outputClass();
#foreach ($field in $methods.keySet())
result.$field = convert_$field(context);
#end
return result;
}
#foreach($en in $methods.entrySet())
private Object contert_${en.key}(Map<String, Object> context)) {
${en.value}
}
#end
}
对于这样的复杂模板,按照上面的建模,SourceBuilder类会非常大,如果模板中的变量非常多,且变量的配置来源于DB或者其它地方,则SourceBuilder接口的实现类又将非常巨大,我们需要将些类设计成可扩展的方式。
SourceBuilder在处理模板时,实际上是处理模板中所使用到的变量,即得到模板中变量的值。为了将此过程做拆解,可通过职责链的方式完成模板中的变量的处理:

- TemplateRenderChain用于做模板渲染处理,是一个职责链,它由多个TemplateRenderInterceptor组成,在调用TemplateRenderInterceptor前,SourceBuilder会将groovy脚本片断按行分列成List,便于TemplateRenderInterceptor按行处理脚本
- TemplateRenderInterceptor是TemplateRenderChain中的拦截器,每个TemplateRenderInterceptor用于生成模板渲染前所需要的变量,比如ImportExtractInterceptor用于识别import语句,MethodExtractInterceptor用于生成脚本中的$methods
- ScriptCompiler:用于编译由SourceBuilder创建出的groovy类的代码,并创建出对象
有了这一步的过程拆解成对象后,groovy代码片断处理成完整的groovy类这个过程已经可以得到可扩展的处理框架,但这还不够,因为我们需要让用户可以在groovy脚本片断中调用一些内置的服务,那么脚本模板可能如下:
package $packageName;
import cn.yxffcode.script.core.Executable;
import cn.yxffcode.script.Inject;
import java.util.*;
$imports
${outputClassPackage}.${outputClass}; //导入输入类
public class $className implements Executable {
@Inject
private ServiceA serviceA;
@Inject
private ServiceB serviceB;
@Inject
private ServiceC serviceC;
public $outputClass execute(Map<String, Object> context) {
$outputClass result = new $outputClass();
#foreach ($field in $methods.keySet())
result.$field = convert_$field(context);
#end
return result;
}
#foreach($en in $ methods.entrySet())
private Object contert_${en.key}(Map<String, Object> context)) {
${en.value}
}
#end
}
显然,这个模板需要在groovy片断处理成groovy类,并创建出实例后,给实例注入ServiceA, ServiceB, ServiceC这三个服务,同样,我们新增一个对象处理类,用于脚本编译、创建出对象并对创建出的对象做服务的注入处理:

同样,对创建出的对象,可能有很多种处理需求,比如设置配置值,创建动态代理收集执行数据等,显然,一个ServiceInjector不够,需要创建多个同样的类,并修改ScriptCompiler,使其调用调用新的类处理groovy对象,这样不符合开闭原则,我们可以参考Spring的BeanPostProcessor的设计,新增一个ObjectPostProcessor接口,在ScriptCompiler创建出groovy对象后依次调用每一个GroovyObjectPostProcessor处理该对象:

当需要新增groovy对象处理的特定功能时,写一个ObjectPostProcessor,在创建ScriptCompiler时传入即可。
实现
代码地址:https://github.com/gaohanghbut/groovytp
使用方式
先看看框架使用方式:

其中,入口就是ScriptCompiler类
test_template.vm模板内容如下:
package cn.yxffcode.groovytp.gen;
$!imports
public class TestGroovy {
public Object test() {
$expression
}
}
主要实现代码
入口代码如下:

入口实现非常简单,主要分为四步:
- 处理脚本,做模板渲染
- 编译成class
- 创建对象
- 执行ObjectPostProcessor
再看看脚本渲染处理的主体代码:

同样非常简单:
- 调用TemplateChain处理上下文
- 执行模板引擎渲染
最后几个接口和渲染处理的chain的定义如下:
public interface ObjectPostProcessor {
Object postProcessObject(Object instance);
}
public interface TemplateRendInterceptor {
void intercept(TemplateRenderChain.RenderContext renderContext);
}
public class TemplateRenderChain {
private static final Splitter SPLITTER = Splitter.on('\n').trimResults();
private final TemplateRendInterceptor[] renderInterceptors;
public TemplateRenderChain(TemplateRendInterceptor[] renderInterceptors) {
this.renderInterceptors = renderInterceptors;
}
public RenderContext proceed(Script script) {
final RenderContext renderContext = new RenderContext(script);
renderContext.doProceed();
return renderContext;
}
职责链的实现:
public final class RenderContext {
private final Map<String, List<String>> context = Maps.newHashMap();
private final List<String> scriptLines;
private int interceptorIndex;
public RenderContext(Script script) {
this.scriptLines = Collections.unmodifiableList(SPLITTER.splitToList(script.getContentFragment()));
}
Map<String, List<String>> getContext() {
return context;
}
public RenderContext appendTo(String variable, String value) {
List<String> values = context.get(variable);
if (values == null) {
values = new ArrayList<String>();
context.put(variable, values);
}
values.add(value);
return this;
}
public List<String> getScriptLines() {
return scriptLines;
}
public void doProceed() {
if (interceptorIndex >= renderInterceptors.length) {
return;
}
//调用TemplateRenderInterceptor前先执行interceptorIndex++,调用结束后执行interceptorIndex--
//因为这里是嵌套调用,RenderContext.doProceed()方法会在TemplateRenderInterceptor.interceptor()中被调用,而
//RenderContext.doProceed()中又会调用TemplateRenderInterceptor.intercept()
//当RenderContext.doProceed()被调用时,需要执行下一个TemplateRenderInterceptor
final TemplateRendInterceptor currentInterceptor = renderInterceptors[interceptorIndex++];
currentInterceptor.intercept(this);
interceptorIndex--;
}
}
}
总体代码非常简单,一遍过。