技术能力对比
一、表达式引擎
应用场景总结:针对于需要进行脚本求值场景,性能上aviator比SpringEL要好一些,可能是1-2倍or10倍or更多,但是本身计算就是ns级别,所以在非高性能场景可以忽略不计。
标准接口定义:
import java.util.Map;
public interface ExpressionParser {
<T> T getValue(String expressionString, Map<String, Object> rootObject);
<T> T getValue(String expressionString);
}
① aviator
官方文档:https://github.com/killme2008/aviatorscript
引入:
<dependency>
<groupId>com.googlecode.aviator</groupId>
<artifactId>aviator</artifactId>
<version>5.2.1</version>
</dependency>示例代码:
import com.boommapro.gaia.expression.ExpressionParser;
import com.googlecode.aviator.AviatorEvaluator;
import com.googlecode.aviator.AviatorEvaluatorInstance;
import com.googlecode.aviator.Expression;
import java.util.Map;
public class AviatorExpressionParser implements ExpressionParser {
private static AviatorEvaluatorInstance aviatorEvaluator = AviatorEvaluator.getInstance();
public <T> T getValue(String expression, Map<String, Object> context) {
//开启缓存
Expression expr = aviatorEvaluator.compile(expression, expression, true);
return (T) expr.execute(context);
}
@Override
public <T> T getValue(String expressionString) {
return getValue(expressionString, null);
}
}
② SpringEL
代码块参考:/post/springel-util/
官方文档:https://docs.spring.io/spring-framework/reference/core/expressions.html
import com.boommapro.gaia.expression.ExpressionParser;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextException;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.expression.*;
import org.springframework.context.support.GenericApplicationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.expression.spel.support.StandardTypeLocator;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class SpringExpressionParser implements ExpressionParser {
private static final long serialVersionUID = 1L;
private StandardEvaluationContext context;
private org.springframework.expression.ExpressionParser expressionParser;
private final ConcurrentHashMap<String, Expression> EXPRESSION_MAP = new ConcurrentHashMap<>();
@Override
public <T> T getValue(String expressionString) {
return getValue(expressionString, null);
}
private static final SpringExpressionParser PARSER = new SpringExpressionParser();
public static SpringExpressionParser getInstance() {
return PARSER;
}
private SpringExpressionParser() {
setApplicationContext(new GenericApplicationContext());
}
@Override
@SuppressWarnings("all")
public <T> T getValue(String expressionString, Map<String, Object> rootObject) {
Expression expression = getExpression(expressionString);
return ((T) expression.getValue(context, rootObject));
}
private Expression getExpression(String expressionString) {
return EXPRESSION_MAP.computeIfAbsent(expressionString, s -> expressionParser.parseExpression(s));
}
@SuppressWarnings("all")
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
if (applicationContext instanceof ConfigurableApplicationContext) {
ConfigurableApplicationContext ac = (ConfigurableApplicationContext) applicationContext;
context = new StandardEvaluationContext();
context.addPropertyAccessor(new BeanExpressionContextAccessor());
context.addPropertyAccessor(new BeanFactoryAccessor());
context.addPropertyAccessor(new MapAccessor());
context.addPropertyAccessor(new EnvironmentAccessor());
context.setBeanResolver(new BeanFactoryResolver(ac.getBeanFactory()));
context.setTypeLocator(new StandardTypeLocator(ac.getBeanFactory().getBeanClassLoader()));
//初始化上下文
expressionParser = new SpelExpressionParser();
//注册默认工具方法
// registerFunc(context,xxx.class);
} else {
throw new ApplicationContextException("can't cast ConfigurableApplicationContext");
}
}
public void registerFunc(StandardEvaluationContext context, Class clazz) {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getModifiers() == 9) {
String name = method.getName();
context.registerFunction(name, method);
}
}
}
public void registerFunc(StandardEvaluationContext context, String className) throws ClassNotFoundException {
Class<?> clazz = Class.forName(className);
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
if (method.getModifiers() == 9) {
String name = method.getName();
context.registerFunction(name, method);
}
}
}
}
③ Groovy简化求值
Gaia 里的表达式统一用上面的 SpringEL,没有把 Groovy 当表达式引擎用;Groovy 用在代码节点里,实现见下文「二、① Groovy」。
如果确实要用 Groovy 做简单求值,要注意:GroovyShell 每次 evaluate/parse 都会编译出一个新类,高频调用会不断占用 Metaspace。正确做法是按表达式缓存编译结果,每次求值新建 Script 实例(Script 持有 Binding,不是线程安全的)。下面是按这个思路写的示例:
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class GroovyExpressionParser implements ExpressionParser {
private static final GroovyShell SHELL = new GroovyShell();
// 同一个表达式只编译一次
private static final Map<String, Class<? extends Script>> CACHE = new ConcurrentHashMap<>();
@Override
@SuppressWarnings("unchecked")
public <T> T getValue(String expressionString, Map<String, Object> rootObject) {
Class<? extends Script> clazz = CACHE.computeIfAbsent(expressionString, e -> SHELL.parse(e).getClass());
Binding binding = new Binding(rootObject == null ? new HashMap<>() : new HashMap<>(rootObject));
// 每次求值创建新的 Script 实例
return (T) InvokerHelper.createScript(clazz, binding).run();
}
@Override
public <T> T getValue(String expressionString) {
return getValue(expressionString, null);
}
}二、动态脚本
应用场景总结:groovy适合轻量级引入,但是存在不兼容java lambda语法和部分语法不兼容等问题,在特殊情况下可以使用 Java Dynamic Class Loader
① Groovy
引入:
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>2.4.17</version>
</dependency>示例代码:
public class GroovyShellTest {
public static void main(String[] args) {
GroovyShell shell = new GroovyShell();
Binding binding = new Binding();
binding.setVariable("param", "param");
String script = "";
try {
shell.evaluate(script);
} catch (Exception e) {
throw new GroovyRuntimeException("groovy脚本执行异常", e);
} finally {
shell.resetLoadedClasses();
}
}
}Gaia 中的实现:代码节点用 GroovyClassLoader.parseClass 把脚本编译成类,再反射调用 execute(Map)。源码:GroovyCompilerEngine.java:
package cn.boommanpro.gaia.workflow.infra.extend.node.code.compiler.groovy;
import cn.boommanpro.gaia.workflow.infra.extend.node.code.compiler.CompilerEngine;
import cn.boommanpro.gaia.workflow.infra.extend.node.code.compiler.pojo.CompileResult;
import groovy.lang.GroovyClassLoader;
public class GroovyCompilerEngine implements CompilerEngine {
private static final GroovyClassLoader loader = new GroovyClassLoader();
@Override
public CompileResult loadClass(String script) {
try {
Class<?> result = loader.parseClass(script);
//这里比较简单,没有对代码进行reformat
return CompileResult.success(result, script);
} catch (Exception e) {
return CompileResult.otherException(e);
}
}
}脚本写法(示例 ExampleCode.groovy):
/**
* Groovy 代码节点示例。
* <p>
* 约束:
* <ul>
* <li>GroovyClassLoader.parseClass 解析脚本,类定义会被识别为主类</li>
* <li>需提供 execute(Map) 方法,返回 Map 作为节点输出</li>
* <li>可使用 Groovy 语法糖(字符串插值、def 等)</li>
* </ul>
* 输入参数 name(String),输出 greeting / upper。
*/
class ExampleCode {
Map<String, Object> execute(Map<String, Object> inputs) {
def result = new HashMap<String, Object>()
def name = inputs.get("name") ?: ""
result.put("greeting", "Hello, ${name}!".toString())
result.put("upper", name.toString().toUpperCase())
return result
}
}注意:这里用的是一个全局的 GroovyClassLoader,每次 parseClass 都会生成新类且不会被卸载。脚本固定、只编译一次没问题;如果同一段脚本会被反复编译,应该像上面的示例一样按脚本内容缓存。
② Java Dynamic Class Loader
核心逻辑:通过 javax.tools.JavaCompiler 在内存中编译 .java 源码(注意类名需要加随机后缀),再用自定义 ClassLoader 加载字节码并实例化。
Gaia 中的完整实现在 compiler/java 目录下,分 4 步:
① 类名加随机后缀(AbstractCompiler.java):同一个 ClassLoader 里不能重复定义同名类,所以每次编译前把类名改成“原类名 + 时间戳 + 随机数”:
public JavaCompilerResult compile(String script, ClassLoader classLoader) {
script = script.trim();
Matcher matcher = PACKAGE_PATTERN.matcher(script);
String pkg;
if (matcher.find()) {
pkg = matcher.group(1);
} else {
pkg = "";
}
matcher = CLASS_PATTERN.matcher(script);
String cls;
if (matcher.find()) {
String match = matcher.group();
String className = matcher.group(1);
cls = className + System.currentTimeMillis() + random.nextInt(1000);
match = match.replaceFirst(className, cls);
script = matcher.replaceFirst(match).replace(String.format("%s.class", className), String.format("%s.class", cls));
} else {
throw new ScriptIllegalException("No such class name in " + script);
}
String fullClassName = pkg != null && !pkg.isEmpty() ? pkg + "." + cls : cls;
return doCompile(fullClassName, script, classLoader);
}② 在内存中编译(JavaCompiler.java):用 JDK 自带的编译器,源码和产物都放在内存里,不落盘:
@Override
protected JavaCompilerResult doCompile(String name, String script, ClassLoader classLoader) {
// 创建新的DiagnosticCollector和MemoryJavaFileManager实例避免状态污染
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
MemoryJavaFileManager javaFileManager = new MemoryJavaFileManager(compiler.getStandardFileManager(diagnostics, null, null));
// 将 extractedPath 添加到类路径
SpringBootJarExtract.initJarCompilerEnvironment();
//① 将source code -> MemoryJavaFileObject
MemoryJavaFileObject javaSource = new MemoryJavaFileObject(name, script);
try {
// ③ 填充项目依赖到MemoryJavaFileManager
fillThirdDependencyToFileManager(javaFileManager);
// ④借助jdk本身的compiler 直接进行编译
compiler.getTask(null, javaFileManager, diagnostics, OPTIONS, null, Arrays.asList(javaSource)).call();
//编译错误处理
validJavaCompilerSourceError(name, diagnostics, javaSource);
return loadAndReturnCompilerResult(script,javaFileManager, name, classLoader);
} catch (Exception e) {
throw new ScriptIllegalException("script compile ERROR!error message:" + e.getMessage(), e);
} finally {
resourceClose(name, javaSource, javaFileManager);
}
}编译产物通过自定义的 MemoryJavaFileManager 拦截到内存:
@Override
public JavaFileObject getJavaFileForOutput(Location location, String className, JavaFileObject.Kind kind,
FileObject sibling) throws IOException {
MemoryJavaFileObject javaFileObject = new MemoryJavaFileObject(className, kind);
fileObjectMap.put(javaFileObject.toUri(), javaFileObject);
return javaFileObject;
}③ 加载字节码(MemoryClassLoader.java):
package cn.boommanpro.gaia.workflow.infra.extend.node.code.compiler.java.dependency;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.HashMap;
import java.util.Map;
public class MemoryClassLoader extends URLClassLoader {
private final Map<String, byte[]> classCache = new HashMap<>();
public MemoryClassLoader() {
super(new URL[0], Thread.currentThread().getContextClassLoader());
}
public Class<?> loadClass(String fullName, byte[] classData) {
return this.defineClass(fullName, classData, 0, classData.length);
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
// 2. 缓存中没有,委托给父类加载器(双亲委派)
try {
// 1. 先尝试从缓存中加载
byte[] classBytes = classCache.get(name);
if (classBytes != null) {
return defineClass(name, classBytes, 0, classBytes.length);
}
return super.findClass(name);
} catch (ClassNotFoundException e) {
// 父类加载器也找不到,抛出异常
throw new ClassNotFoundException("Class " + name + " not found in memory or parent classloader", e);
}
}
public void addClass(String name, byte[] classBytes) {
classCache.put(name, classBytes);
}
}④ 缓存编译结果并调用(JavaCompilerEngine.java、DynamicCompileNode.java):同一段脚本 1 小时内只编译一次,执行时反射调用 execute(Map):
// 添加缓存,使用LoadingCache自动处理缓存加载和过期
private static final LoadingCache<String, CompileResult> scriptCache = CacheBuilder.newBuilder()
.maximumSize(1000) // 最多缓存1000个脚本
.expireAfterWrite(1, TimeUnit.HOURS) // 缓存1小时后过期
.build(new CacheLoader<String, CompileResult>() {
@Override
public CompileResult load(String script) throws Exception {
try {
JavaCompilerResult compile = javaCompiler.compile(script, new MemoryClassLoader());
return CompileResult.success(compile.getMainClass(),compile.getClazzCode(), compile.getClassList());
} catch (Exception e) {
return CompileResult.otherException(e);
}finally {
}
}
}private Map<String, Object> executeJava(Chain chain) {
try {
CompileResult compileResult = CompilerEngineEnum.JAVA.loadClass(code);
if (compileResult.getCode().getCode() != 0) {
throw new RuntimeException("Java compilation failed: " + compileResult.getMsg());
}
Class<?> clazz = compileResult.getClazz();
Object instance = clazz.newInstance();
// 检查是否实现了CodeExecute接口
if (instance instanceof CodeExecute) {
CodeExecute codeExecute = (CodeExecute) instance;
Map<String, Object> parameterValues = chain.getParametersData(this);
return codeExecute.execute(parameterValues != null ? parameterValues : new HashMap<>());
}
// 尝试调用execute方法
Method executeMethod = clazz.getMethod("execute", Map.class);
Map<String, Object> parameterValues = chain.getParametersData(this);
return (Map<String, Object>) executeMethod.invoke(instance, parameterValues != null ? parameterValues : new HashMap<>());
} catch (Exception e) {
throw new RuntimeException("Java execution failed", e);
}
}脚本写法(示例 ExampleCode.java):
import java.util.HashMap;
import java.util.Map;
/**
* Java 代码节点示例。
* <p>
* 约束:
* <ul>
* <li>需提供 public 无参构造(默认即满足)</li>
* <li>需提供 {@code execute(Map<String,Object>)} 方法,返回 Map 作为节点输出</li>
* <li>无需实现 CodeExecute 接口,DynamicCompileNode 会通过反射调用 execute</li>
* </ul>
* 输入参数 name(String),输出 greeting / length。
*/
public class ExampleCode {
public Map<String, Object> execute(Map<String, Object> inputs) {
Map<String, Object> result = new HashMap<>();
Object name = inputs.get("name");
String nameStr = name == null ? "" : name.toString();
result.put("greeting", "Hello, " + nameStr + "!");
result.put("length", nameStr.length());
return result;
}
}踩坑点:
ToolProvider.getSystemJavaCompiler()只在 JDK 下可用,运行在 JRE 上会返回 null,部署镜像必须带 JDK。Spring Boot 打成 fat jar 后,依赖是嵌套在 jar 里的,javac 读不到。Gaia 用 SpringBootJarExtract.java 先把依赖解压到 extracted 目录,再设置为编译的 CLASS_PATH。
编译参数
-source 21 -target 21要和运行时的 JDK 版本匹配。
三、插件化技术
① PF4J
官方文档:https://github.com/pf4j/pf4j
示例demo见:https://github.com/pf4j/pf4j/tree/master/demo
halo项目的最佳实践:https://github.com/halo-dev/halo
依赖引入:
<dependency>
<groupId>org.pf4j</groupId>
<artifactId>pf4j</artifactId>
<version>3.10.0</version>
</dependency>源码索引
文中 Groovy 和 Java 动态编译的实现均来自 Gaia 工作流引擎:https://github.com/boommanpro/gaia-workflow-engine
GroovyCompilerEngine.java:Groovy 编译
AbstractCompiler.java、JavaCompiler.java:Java 动态编译
DynamicCompileNode.java:代码节点调用入口