百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

可扩展groovy脚本片断处理框架设计

ccwgpt 2024-11-18 09:33 18 浏览 0 评论

本文中代码地址代码地址:https://github.com/gaohanghbut/

起因

近日有用事想要在页面上写一些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
import ${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时传入即可。

实现

代码地址:

使用方式

先看看框架使用方式:

其中,入口就是ScriptCompiler类

主要实现代码

入口代码如下:

入口实现非常简单,主要分为四步:

· 处理脚本,做模板渲染

· 编译成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--;
 }
 }
}

总体代码非常简单,一遍过。

相关推荐

如何使用PIL生成验证码?(pi验证教程)

web项目中遇到使用验证码的情况有很多,进行介绍下使用PIL生成验证码的方法。安装开始安装PIL的过程确实麻烦各种问题层出不绝,不过不断深入后就没有这方面的困扰了:windows安装:直接安装Pil...

Python必学!3步解锁asyncio异步编程 性能直接狂飙10倍!

还在用传统同步代码被IO阻塞卡到崩溃?别当“代码苦行僧”了!Python的asyncio模块堪称异步编程的“开挂神器”,处理高并发任务就像开了涡轮增压!不管是网络爬虫、API接口开发还是文件批量处理,...

Tornado6+APScheduler/Celery打造并发异步动态定时任务轮询服务

定时任务的典型落地场景在各行业中都很普遍,比如支付系统中,支付过程中因为网络或者其他因素导致出现掉单、卡单的情况,账单变成了“单边账”,这种情况对于支付用户来说,毫无疑问是灾难级别的体验,明明自己付了...

Python学习怎么入门?附真实学习方法

Python技术在企业中应用的越来越广泛,因此企业对于Python方面专业人才的需求也越来越大,那对于之前对Python没有任何了解和接触的人而言,想要从零开始学习并不是一件容易的事情,接下来小U就为...

PySpider框架的使用(pyspider 教程)

PysiderPysider是一个国人用Python编写的、带有强大的WebUI的网络爬虫系统,它支持多种数据库、任务监控、项目管理、结果查看、URL去重等强大的功能。安装pip3inst...

大学计算机专业 学习Python学习路线图(最新版)

这是我刚开始学习python时的一套学习路线,从入门到上手。(不敢说精通,哈哈~)希望对大家有帮助哈~大家需要高清得完整python学习路线可以【文末有获取方式】【文末有获取方式】一、Python入门...

阿里巴巴打造的400集Python视频合集免费学起来,学完万物皆可爬

第一阶段Python入门章节1:Python入门章节2:编程基本概念章节3:序列章节4:控制语句章节5:函数章节6:面向对象编程第二阶段Python深入与提高章节1:异常处理章节2:游戏开发-坦克大...

Nginx Gunicorn在服务器中分别起什么作用

大部分人在gunicorn前面部署一层nginx的时候也的确没有想过为什么,他们只是觉得这样显得他们比较专业,而且幻想着加了一层nginx反向代理之后性能会有提升,恕我直言,请你们带上脑子,一个单纯的...

Python培训怎么学?Python基础技术总结!值得一看

Python培训如今越来越被更多人所接受,相比自学参加Python培训的好处也是显而易见,但Python毕竟属于后端编程开发的主流语言,其知识机构还是比较庞大的,那Python培训怎么学?以及Pyth...

使用Tornado部署Flask项目(tornado async)

Tornado不仅仅是一个WEB框架,也可以是一个WEB服务器。在Tornado中我们可以使用wsgi模块下的WSGIContainer类运行其他WSGI应用如:Fask,Bottle,Djang...

Python Web框架哪个好用?(python3 web框架)

  问:PythonWeb框架哪个好用?  答:  1.Django  Django是Python世界中最出名、最成熟的Web框架。Django功能全面,各模块之间结合紧密,(不讲其他的)Djang...

Vue3.0+Tornado6.1发布订阅模式打造异步非阻塞实时=通信聊天系统

“表达欲”是人类成长史上的强大“源动力”,恩格斯早就直截了当地指出,处在蒙昧时代即低级阶段的人类,“以果实、坚果、根作为食物;音节清晰的语言的产生是这一时期的主要成就”。而在网络时代人们的表达欲往往更...

Python开源项目合集(第三方平台)(python第三方开发工具)

wechat-python-sdk-wechat-python-sdk微信公众平台Python开发包http://wechat-python-sdk.readthedocs.org/,非官方...

IT界10倍高效学习法!用这种方式,一年学完清华大学四年的课程

有没有在某一个瞬间,让你放弃学编程刚开始学python时,我找了几十本国内外的python编程书籍学习后,我还是似懂非懂,那些书里面到处都是抽象的概念,复杂的逻辑,这样的书,对于专业开发者来说,在平常...

如何将Python算法模型注册成Spark UDF函数实现全景模型部署

背景Background对于算法业务团队来说,将训练好的模型部署成服务的业务场景是非常常见的。通常会应用于三个场景:部署到流式程序里,比如风控需要通过流式处理来实时监控。部署到批任务中部署成API服...

取消回复欢迎 发表评论: