过年放假终于有空写博客了,今年给自己定的目标是要多写博客,先从一些平时开发中遇到的问题总结开始,也方便后面查询
应用场景 当在开发一些需要频繁对接第三方接口或是需要频繁修改业务逻辑的功能时,为了保证业务代码的扩展性,可以选择集成一种脚本语言实现。
以下探索了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" ; 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 import java.nio.file.Filesimport java.nio.file.Pathsimport java.lang.*import groovy.json.JsonSlurperdef 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 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 { private static ScriptEngine engine; static { ScriptEngineManager scriptEngineManager = new ScriptEngineManager (); engine = scriptEngineManager.getEngineByName("js" ); } 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; @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()); } 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" )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 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 { 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; } 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服务,不香么。
性能对比 (后续填坑)