Java 如何实现动态脚本?

作者: 赋苏 阿里技术

Java 如何实现动态脚本?

文章插图
 
阿里妹导读:在平台级的 JAVA 系统中,动态脚本技术是不可或缺的一环 。本文分享了一种 Java 动态脚本实现方案,给出了其中的关键技术点,并就类重名问题、生命周期、安全问题等做出进一步讨论,欢迎同学们共同交流 。
文末福利:Java 学习路线 。
前言
繁星是一个数据服务平台,其核心功能是:用户配置一段 SQL,繁星产出对应的 HSF/TR/SOA/Http 取数接口 。
繁星引擎流程图如下:
Java 如何实现动态脚本?

文章插图
 
一次查询请求经过引擎的管道,被各个阀门处理后就得到了相应的结果数据 。图中高亮的两个阀门就是本文讨论的重点:前置脚本与后置脚本 。
温馨提示:动态脚本就意味着代码发布跳过了公司内部发布平台,做不到监控、灰度、回滚三板斧,容易引发线上故障,因此业务系统中强烈不推荐使用该技术 。
当然 Java 动态脚本技术一般使用场景也比较少,主要在平台性质的系统中可能用到,比如 leetcode 平台,D2 平台,繁星数据服务平台等 。本文权当技术探索和交流 。
功能描述
对 JavaScript 熟悉的同学知道,eval() 函数,例如:
eval('console.log(2+3)')就会在控制台中打出 5 。
这里我们要做的和 eval 类似,就是希望输入一段 Java 代码,服务器按照代码中的逻辑执行 。在繁星中前置脚本的功能就是可以对用户的输入参数进行自定义的处理,后置脚本的功能就是可以对数据库中查询到的结果做进一步加工 。
为什么是 Java 脚本?
Groovy
要实现动态脚本的需求,首先可能会想到 Groovy,但是使用 Groovy 有几大缺点:
  • Groovy 虽然也是运行在 JVM,但是语法和 Java 有一些差异,对于只会 Java 的同学来说有一定学习成本 。
  • 动态类型,缺乏约束 。有时候太过于灵活自由也是缺点,尤其是对于平台说来 。
  • 需要额外引入 Groovy 的引擎 jar 包,大小 6.2M,属实不小,对于有代码强迫症的我来说这会是一个重要考虑因素 。
Java
采用 Java 来实现动态脚本的功能有以下优点:
  • 学习成本低,在阿里最主要的语言就是 Java,会 Java 几乎是每个工程师必备的技能,因此上手难度几乎为零 。
  • Java 可以规定接口约束,从而使得用户写的前后置脚本整齐划一,方便管理和治理 。
  • 可以实时编译和错误提示,方便用户及时订正问题 。
实现方式
代码工程说明
本文的代码工程:
https://kbtdatacenter-read.oss-cn-zhangjiakou.aliyuncs.com/fusu-share/dynamic-script.zip
??????--dynamic-script------advance-discuss //深度讨论脚本动态化技术中的一些细节------code-javac //使用代码执行编译加载运行任务------command-javac //演示用命令行的方式动态编译和加载java类------facade //提供单独的接口包,方便整个演示过程流畅进行实现方案设计
我们首先定义好一个接口,例如 Animal,然后用户在自己的代码中实现 Animal 接口 。相当于用户提供的是 Animal 的实现类 Cat,这样系统加载了用户的 Java 代码后,可以很方便的利用 Java 多态特性,访问到对应的方法 。这样既方便了用户书写规范,同时平台使用起来也简单 。
使用控制台命令行
首先回顾如何使用命令行来编译 Java 类,并且运行 。
首先对 facade 模块打一个 jar 包,方便后续依赖:
??????cd 项目根目录mvn install进入到模块 command-javac 的 resources 文件夹下(绝对路径因人而异):
??????# 进入到Cat.java所在的目录cd /Users/fusu/d/group/fusu-share/dynamic-script/command-javac/src/main/resources# 使用命令行工具javac编译,linux/mac 上cp分隔符使用 : windown使用 ;javac -cp .:/Users/fusu/d/group/fusu-share/dynamic-script/facade/target/facade-1.0.jar Cat.java# 运行java -cp .:/Users/fusu/d/group/fusu-share/dynamic-script/facade/target/facade-1.0.jar Cat# 得到结果# > I'm Cat Main使用 Process 调用 javac 编译
有了上面的控制台命令行操作,很容易想到用 Java 的 Process 类调用命令行工具执行 javac 命令,然后使用 URLClassLoader 来加载生成的 class 文件 。代码位于模块 command-javac 下的 ProcessJavac.java 文件中,核心代码如下:
//项目所在路径String projectPath = PathUtil.getAppHomePath();Process process = null;String cmd = String.format("javac -cp .:%s/facade/target/facade-1.0.jar -d %s/command-javac/src/main/resources %s/command-javac/src/main/resources/Cat.java", projectPath, projectPath, projectPath);System.out.println(cmd);process = Runtime.getRuntime().exec(cmd);// 打印程序输出readProcessOutput(process);int exitVal = process.waitFor();if (exitVal == 0) { System.out.println("javac执行成功!" + exitVal);} else { System.out.println("javac执行失败" + exitVal); return;}String classFilePath = String.format("%s/command-javac/src/main/resources/Cat.class", projectPath);String urlFilePath = String.format("file:%s", classFilePath);URL url = new URL(urlFilePath);URLClassLoader classLoader = new URLClassLoader(new URL[]{url});Class<?> catClass = classLoader.loadClass("Cat");Object obj = catClass.newInstance();if (obj instanceof Animal) { Animal animal = (Animal) obj; animal.hello("Kitty");}//会得到结果: Hello,Kitty! 我是Cat 。用编程方式编译和加载


推荐阅读