Java集成各种脚本语言

liminjun

过年放假终于有空写博客了,今年给自己定的目标是要多写博客,先从一些平时开发中遇到的问题总结开始,也方便后面查询

应用场景

​ 当在开发一些需要频繁对接第三方接口或是需要频繁修改业务逻辑的功能时,为了保证业务代码的扩展性,可以选择集成一种脚本语言实现。

​ 以下探索了Java语言集成Groovy脚本,Shell脚本,Javascript脚本,Lua脚本和Python脚本的方法,并且进行了一些简单的性能对比。

Groovy脚本

Groovy脚本的一大优势是编译后的字节码文件可以直接运行在java虚拟机上,性能和跨平台方面都比较友好;另外Gradle编译工具也是基于Groovy语言,也是应用比较广发的一种脚本语言

Maven依赖

1
2
3
4
5
6
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>${groovy.version}</version>
<type>pom</type>
</dependency>

Java端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import groovy.lang.Binding;
import groovy.lang.GroovyShell;

public class Test{

public static void main(String[] args) throws IOException {
String groovyScriptFile = "dist/resource/online.groovy";

// 实现加载公共脚本,目前没想到比较好的方法,仅仅是将脚本直接拼接,这种方式在调试时会出现行号对应不上的问题 todo: 后面来填坑
Path groovyScriptPath = Paths.get(groovyScriptFile);
Path commonGroovyScriptPath = Paths.get(StrUtil.format("{}/common.groovy", groovyScriptPath.getParent()));
StringBuilder groovyScriptContent = new StringBuilder();
if(commonGroovyScriptPath.toFile().exists()){
groovyScriptContent.append(new String(Files.readAllBytes(commonGroovyScriptPath)));
groovyScriptContent.append("\n");
}
groovyScriptContent.append(new String(Files.readAllBytes(groovyScriptPath)));

// 参数传递
Binding binding = new Binding();
binding.setVariable("EnginePort", "9998");
binding.setVariable("GroovyContext", binding);
GroovyShell groovyShell = new GroovyShell(binding);
Object evaluate = groovyShell.evaluate(groovyScriptContent.toString(), groovyScriptPath.getFileName().toString());
System.out.println(evaluate);
}

}

Groovy代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// 可以直接引入java中的一些工具类
import java.nio.file.Files
import java.nio.file.Paths
import java.lang.*
// 同时包含groovy语言特有的一些工具类
import groovy.json.JsonSlurper

// 执行成功
def EXEC_SUCCESS = 0
// 执行失败
def EXEC_FAILED = 100

// 读取参数示例
def enginePort = GroovyContext.hasVariable("EnginePort") ? EnginePort : "9998"
println("引擎端口(enginePort)为:" + enginePort)

// 示例函数,请求第三方接口
static def postForm(engineIp, enginePort, data, method) {
def boundary = UUID.randomUUID().toString();
def connection = new URL("""http://$engineIp:$enginePort/api/v1/test?method=$method""").openConnection()
connection.setRequestMethod('POST')
connection.useCaches = false
connection.doOutput = true
connection.doInput = true
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary)
def writer = new OutputStreamWriter(connection.outputStream)
def LINE = "\r\n"
writer.append("--" + boundary).append(LINE)
writer.append("Content-Disposition: form-data; name=data").append(LINE)
writer.append("Content-Type: text/plain; charset=utf-8").append(LINE)
writer.append(LINE)
writer.append(data).append(LINE)
writer.append("--" + boundary + "--").append(LINE)
writer.flush()
writer.close()
connection.connect()
return connection.content.text
}

return EXEC_SUCCESS

Shell脚本

这里执行shell脚本主要通过外部程序调用,在执行业务逻辑时需要启动一个新的进程,仅推荐在linux系统上使用,此方法对系统预装软件包有一定的需求,比如某些linux发行版没有预装curl命令,此时在编写一些请求第三方接口的脚本时就会比较麻烦

Maven依赖:无需引入,这一点对减少jar包体积还是比较友好的

Java端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
/**
* 执行shell脚本
*
* @param scriptPath 脚本路径
* @param workDir 脚本运行路径
* @param env 执行脚本注入的环境变量
* @return 脚本执行返回码
* @throws Exception
*/
private ScriptExecuteResult execute(String scriptPath, String workDir, Map<String, String> env) throws Exception {
File scriptFile = new File(scriptPath);
ScriptExecuteResult scriptExecuteResult = new ScriptExecuteResult();
if (!scriptFile.exists() || scriptFile.isDirectory()) {
scriptExecuteResult.setScriptExecuteCodeEnum(ScriptExecuteCodeEnum.EXEC_SCRIPT_NOT_EXIST);
return scriptExecuteResult;
}
File workDirFile = new File(workDir);
if (!workDirFile.exists() || !workDirFile.isDirectory()) {
scriptExecuteResult.setScriptExecuteCodeEnum(ScriptExecuteCodeEnum.EXEC_WORK_DIR_NOT_EXIST);
return scriptExecuteResult;
}
if (!scriptFile.setExecutable(true)) {
scriptExecuteResult.setScriptExecuteCodeEnum(ScriptExecuteCodeEnum.EXEC_CAN_NOT_SET_EXECUTABLE);
return scriptExecuteResult;
}
Integer idx = 0;
Map<String, String> envMap = environmentProperties.getMap();
for (String k : envMap.keySet()) {
if(!env.containsKey(k)){
env.put(k, envMap.get(k));
}
}
String[] envArray = new String[env.size()];
for (String k : env.keySet()) {
String v = env.get(k);
envArray[idx] = StrUtil.format("{}={}", k, v);
idx++;
}

Process process = Runtime.getRuntime().exec(scriptPath, envArray, workDirFile);
InputStream inputStream = process.getInputStream();
InputStream errorStream = process.getErrorStream();
Integer scriptResCode = process.waitFor();
byte[] stdOut = new byte[inputStream.available()];
byte[] stdErr = new byte[errorStream.available()];
inputStream.read(stdOut);
errorStream.read(stdErr);
log.info("script execute result: {}", new String(stdOut));
String errorString = new String(stdErr);
if(!StrUtil.isEmpty(errorString)){
log.error("script execute error: {}", errorString);
log.error("scriptPath:{} , envArray:{}, workDirFile:{}", scriptPath, envArray, workDirFile);
}
scriptExecuteResult.setCode(scriptResCode);
ScriptExecuteCodeEnum scriptExecuteCodeEnum = ScriptExecuteCodeEnum.codeEnumMap.get(scriptResCode);
scriptExecuteResult.setScriptExecuteCodeEnum(scriptExecuteCodeEnum);
if (ObjectUtil.isNull(scriptExecuteCodeEnum)) {
scriptExecuteResult.setSuccess(false);
} else {
scriptExecuteResult.setSuccess(scriptExecuteCodeEnum.success());
}
return scriptExecuteResult;
}

Shell脚本

common.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#!/bin/bash

#执行成功
EXEC_SUCCESS=0
#执行失败
EXEC_FAILED=100

#该文件从环境变量解析配置,如环境变量为空使用默认值
function defaultIfEmpty() {
if [ -z $1 ]; then
echo $2
return
fi
echo $1
}

env.sh

1
2
3
4
5
6
7
8
9
10
11
#!/bin/bash

source common.sh

echo "start parse config params from env"
#读取环境变量示例
EnginePort=`defaultIfEmpty $EnginePort "10017"`
echo "WallePort=$EnginePort"

cp config_template/online.properties config/online.properties
sed -i "s%\${EnginePort}%${EnginePort}%g" config/isc.properties

online.sh

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#!/bin/bash

source env.sh

java -jar online.jar > run.log
cat run.log

if [ `cat run.log | grep "online success" | wc -l` == "1" ]; then
echo "online success"
exit $EXEC_SUCCESS
else
echo "online failed"
exit $EXEC_FAILED
fi

这里公共脚本的提取无需特殊处理,在Shell脚本中直接source命令加载即可,还是比较方便的

Javascript脚本

Java虚拟机内置Javascript脚本解析引擎,但是后续版本貌似要被移除了,所以不推荐使用

Maven依赖:无需引入,Java虚拟机内置了

Java端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.TypeReference;
import lombok.extern.slf4j.Slf4j;

import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import java.util.HashMap;
import java.util.Map;

@Slf4j
public class JSEngineUtils {
/**
* js引擎
*/
private static ScriptEngine engine;

static {
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
engine = scriptEngineManager.getEngineByName("js");
}

/**
* 执行js函数,执行异常时返回传入参数params
* @param params 参数
* @param funcName 函数名
* @param js 函数
* @return
*/
public static Map<String,String> doJSFunction(Map<String,String> params, String funcName, String js){
if(StrUtil.isEmpty(funcName) || StrUtil.isEmpty(js))
return params;
try {
engine.eval(js);
Invocable invocable = (Invocable) engine;
String result = (String) invocable.invokeFunction(funcName, JSONObject.parseObject(JSONObject.toJSONString(params)));
if(StrUtil.isEmpty(result)){
return new HashMap<>();
}
return JSONObject.parseObject(result, new TypeReference<Map<String, String>>(){});
}catch (Exception ex){
log.error("js执行异常", ex);
return params;
}
}

public static void main(String[] args) {
String js = "function mapping(data){ var result = JSON.parse(data);if(result.hasOwnProperty('cc')){result.ddbb = result.cc;delete result.cc;}\n" +
"if(result.hasOwnProperty('ss')){result.cd = result.ss;delete result.ss;}\n" +
"if(result.hasOwnProperty('vv')){if(result.vv == 'cw'){result.vv = 's';}}\n" +
"return JSON.stringify(result);}";
Map<String,String> params = new HashMap<>();
params.put("cc","c");
Map<String, String> result = doJSFunction(params, "mapping", js);
System.out.println(result);
}
}

Lua脚本

Lua语言语法简单,在公司的很多遗留项目中都还在使用,所以在重构一些组件时,为了可以兼容老服务,所以需要支持Lua脚本的解析

Java集成lua脚本有两种方式:

一种是通过jni或者jna调用c版本的lua解析器,这种方式涉及复杂的数据类型转换,对跨平台也不友好,比如适配国产化非x86架构的服务器时

另一种是有国外大佬用纯java语言重构了lua解释器,不过这个开源项目现在也不咋维护了,这里主要使用第二种方法

Maven依赖:

1
2
3
4
5
<dependency>
<groupId>org.luaj</groupId>
<artifactId>luaj-jse</artifactId>
<version>${luaj.version}</version>
</dependency>

Java端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.luaj.vm2.Globals;
import org.luaj.vm2.LuaValue;
import org.luaj.vm2.Varargs;
import com.fasterxml.jackson.databind.JsonNode;

@Slf4j
@Service
public class LuaScriptExecutorServiceImpl implements ScriptExecutorService {

@Value("${main.method:main}")
private String mainMethod;

/**
* 运行Lua脚本,这里还是通过脚本拼接的方式解决公共逻辑提取的问题
*
* @param scriptContent 脚本内容
* @param inputParams 输入参数
* @return {@link JsonNode}
* @throws Exception 异常
*/
@Override
public JsonNode runScript(String scriptContent, String inputParams) throws Exception {
String injectContent = readResourceFile("classpath:lua/inject.lua");
Globals globals = ThreadLocalConstant.luaGlobalsThreadLocal.get();
globals.load(injectContent + "\n" + scriptContent).call();
LuaValue mainFunc = globals.get(LuaValue.valueOf(mainMethod));
Varargs outputVarargs = mainFunc.invoke(LuaValue.valueOf(""), LuaValue.valueOf(inputParams));
return JACKSON.readTree(outputVarargs.arg(2).toString());
}

/**
* 读取资源文件
*
* @param resourceFilePath 资源文件路径
* @return {@link String}
* @throws IOException ioexception
*/
private static String readResourceFile(String resourceFilePath) throws IOException {
ClassPathResource classPathResource = new ClassPathResource(resourceFilePath);
try (InputStream inputStream = classPathResource.getStream()) {
byte[] buff = new byte[inputStream.available()];
inputStream.read(buff);
return new String(buff);
}
}

}

Lua脚本,这里由于公司内的遗留脚本需要兼容cjson的解析,这个正好用来演示lua脚本与Java代码的相互调用

inject.lua

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
local jacksonLuaBind = luajava.bindClass("com.demo.luaBind.JacksonLuaBind")

-- 打印lua的table
function printTable ( t )
local print_r_cache={}
local function sub_print_r(t,indent)
if (print_r_cache[tostring(t)]) then
print(indent.."*"..tostring(t))
else
print_r_cache[tostring(t)]=true
if (type(t)=="table") then
for pos,val in pairs(t) do
if (type(val)=="table") then
print(indent.."["..pos.."] => "..tostring(t).." {")
sub_print_r(val,indent..string.rep(" ",string.len(pos)+8))
print(indent..string.rep(" ",string.len(pos)+6).."}")
elseif (type(val)=="string") then
print(indent.."["..pos..'] => "'..val..'"')
else
print(indent.."["..pos.."] => "..tostring(val))
end
end
else
print(indent..tostring(t))
end
end
end
if (type(t)=="table") then
print(tostring(t).." {")
sub_print_r(t," ")
print("}")
else
sub_print_r(t," ")
end
print()
end

-- 在纯java实现的lua解释器中添加cjson的命名实现,实际调用的java的jackson解析
local cjson = {
decode = function(str)
local table = jacksonLuaBind:decode(str)
return table
end,
encode = function(table)
printTable(table)
return jacksonLuaBind:encode(table)
end
}

公司遗留的删除业务逻辑的main.lua脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- 定制代码
local function do_func(inst_handle, inparams)
print ('inparams : '.. inparams)
local outparams
local req_table = {};
local status, req_table = pcall(cjson.decode,inparams)
if not status then
outparams = '{"code": "1", "reason":"查询失败"}'
return outparams
end
outparams = '{"code": "0", "reason":"查询成功", "inparams": ' .. cjson.encode(req_table) .. '}'
return outparams
end

--以下为固定写法-----------------------------------------------------------------------------
function main(inst_handle, inparams)
local ret = 0
return ret, do_func(inst_handle, inparams)
end

另外需要在java代码中编写结构映射相关代码JacksonLuaBind

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
// 以下仅仅示例,不完善的代码
public class JacksonLuaBind {

/**
* 将json字符串转成lua中的table结构
* 使用广度优先算法处理多层json结构
*
* @param jsonStr json str
* @return {@link LuaValue}
* @throws JsonProcessingException json处理异常
*/
public static LuaValue decode(String jsonStr) throws JsonProcessingException {
LuaTable resultTable = new LuaTable();

Queue<JsonCopyNode> nodeQueue = new LinkedBlockingQueue<>();
nodeQueue.offer(new JsonCopyNode(null, JACKSON.readTree(jsonStr), resultTable));
while(!nodeQueue.isEmpty()) {
JsonCopyNode node = nodeQueue.poll();
String key = node.getKey();
JsonNode value = node.getValue();

if(JsonNodeType.OBJECT.equals(value.getNodeType())){
Iterator<String> keyIterator = value.fieldNames();
LuaTable parentTable = new LuaTable();
if(null == key){
parentTable = resultTable;
}
while (keyIterator.hasNext()) {
String subKey = keyIterator.next();
nodeQueue.offer(new JsonCopyNode(subKey, value.get(subKey), parentTable));
}
if(null != key){
node.getParentTable().set(key, parentTable);
}
}else if(JsonNodeType.STRING.equals(value.getNodeType())){
node.getParentTable().set(key, value.asText());
}else if(JsonNodeType.NUMBER.equals(value.getNodeType())){
node.getParentTable().set(key, value.asInt());
}else if(JsonNodeType.BOOLEAN.equals(value.getNodeType())){
node.getParentTable().set(key, LuaValue.valueOf(value.asBoolean()));
}
}

return resultTable;
}

/**
* 将lua中的table结构转成json字符串
*
* @param inputValue 输入值
* @return {@link LuaValue}
* @throws JsonProcessingException json处理异常
*/
public static LuaValue encode(LuaValue inputValue) throws JsonProcessingException {
if(inputValue instanceof LuaTable){
Map<String, Object> resultMap = new HashMap<>();
Queue<TableCopyNode> nodeQueue = new LinkedBlockingQueue<>();
nodeQueue.offer(new TableCopyNode(null, inputValue, resultMap));
while(!nodeQueue.isEmpty()){
TableCopyNode node = nodeQueue.poll();
String key = node.getKey();
LuaValue value = node.getValue();
if(value instanceof LuaTable){
LuaTable table = ((LuaTable) value);
Map<String, Object> parentMap = new HashMap<>();
if(null == key){
parentMap = resultMap;
}
for (LuaValue subKey : table.keys()) {
nodeQueue.offer(new TableCopyNode(subKey.toString(), table.get(subKey), parentMap));
}
if(null != key){
node.getParentMap().put(key, parentMap);
}
}else if(value instanceof LuaBoolean){
node.getParentMap().put(key, value.toboolean());
}else if(value instanceof LuaInteger){
node.getParentMap().put(key, value.toint());
}else if(value instanceof LuaDouble){
node.getParentMap().put(key, value.tofloat());
}else if(value instanceof LuaString){
node.getParentMap().put(key, value.tojstring());
}
}

return LuaValue.valueOf(JACKSON.writeValueAsString(resultMap));
}
return inputValue;
}

@Data
@AllArgsConstructor
public static class JsonCopyNode{
private String key;
private JsonNode value;
private LuaTable parentTable;
}

@Data
@AllArgsConstructor
public static class TableCopyNode{
private String key;
private LuaValue value;
private Map<String, Object> parentMap;
}

}

基于上述的映射及相互调用,可以逐步完善相关模块,以达到完全兼容C实现lua脚本的所有功能

Python脚本

Java对python的支持并不好,纯java实现的只能支持到python2.7,而且对第三方模块的支持很差,感觉python语言脱离第三方模块就没啥好用的了,所以这里还是直接rpc调用吧,python端直接起个grpc服务,不香么。

性能对比

(后续填坑)

  • Title: Java集成各种脚本语言
  • Author: liminjun
  • Created at: 2023-01-24 17:36:46
  • Updated at: 2023-05-15 10:38:37
  • Link: https://olldbg.github.io/2023/01/24/Java集成各种脚本语言/
  • License: This work is licensed under CC BY-NC-SA 4.0.