JDK6.0之后提供了脚本引擎功能,让我们可以执行某些脚本语言,特别是javascript(javascript是一门解释性语言,动态性非常好),让JAVA的动态性得到更充分的体现,某些时候可以更加灵活的应对需求的变化。在 JDK1.7 之前,无论是 Java 语言还是 JVM 走得都是坚定的静态类型道路。其对动态类型支持的缺失主要是体现在方法调用上。JDK1.7 之前共有 4 条方法调用指令:
- invokevirtual: 调用实例方法。会根据对象的实际类型进行动态单分派 (虚方法分派)
- invokespecial: 以操作数栈栈顶 reference 类型的数据所指向的对象为方法的接收者,调用此对象的实例构造器方法,私有方法或超类构造方法。该指令的操作码之后会紧跟一个 u2 的操作数说明具体调用的是哪个方法,该参数指向常量池集合中的一个 CONSTANT_UTF8_info 类型的索引项,也就是该方法的方法符号引用
- invokestatic: 调用类方法 (static 修饰的方法)
- invokeinterface: 调用接口方法。运行期解释器会搜索一个实现了该接口方法的对象,并调用对应实现的接口方法
JDK1.7 中,这个真物体现在 JVM 层面就是新增的 invokedynamic 指令,该指令为动态类型而生,直接支持动态类型。而在 Java 语言层面的体现则是新增的 java.lang.invoke 包。
1 脚本语言
Rhino 是一种使用 Java 语言编写的 JavaScript 的开源实现,原先由Mozilla开发,现在被集成进入JDK 6.0。基于脚本的动态编译可以有效的增加代码的扩展性和动态发布。
使用java执行一段JS脚本,具体的结果如下:
String script = "print(123)"; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); //开始对提交的脚本进行编译 CompiledScript compiled = ((Compilable) engine).compile(script); //执行脚本 compiled.eval();
测试执行结果如下:
上面是执行一个print方法,如果仅仅支持这么简单的JS语法上的东西,对我们来说没有任何意义,是否可以和JAVA交互呢,使用Java执行JS这个肯定是可以支持和Java交互的,具体的逻辑如下:
@Test public void testJS() throws Exception{ String script = "out.println('asdfb')"; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); //开始对提交的脚本进行编译 CompiledScript compiled = ((Compilable) engine).compile(script); Bindings bindings = new SimpleBindings(); bindings.put("out",System.out); compiled.eval(bindings); }
其中执行JS脚本时,可以在脚本中加入自定义对象实例,在脚本中可以直接引用。脚本中可以直接引用相关对象的方法。具体的执行结果如下:
上面的两个过程中使用了ScriptEngineManager、ScriptEngine、CompiledScript、Bindings等几个对象,ScriptEngineManager 为 ScriptEngine 类实现一个发现和实例化机制,还维护一个键/值对集合来存储所有 Manager 创建的引擎所共享的状态。此类使用服务提供者机制枚举所有的 ScriptEngineFactory 实现。ScriptEngineManager 提供了一个方法,可以返回一个所有工厂实现和基于语言名称、文件扩展名和 mime 类型查找工厂的实用方法所组成的数组。
ScriptEngine是JS的执行规范,所有的JS的引擎执行都需要实现这个接口,当前ScriptEngine中定义了一些基本的脚本功能法法。
CompiledScript是编译器,将JS编译后端的结果类扩展
Bindings是一个kv的mapping关系表,用于映射js脚本中用到的相关依赖对象的信息。ScriptEngineManager的全部共享数据是通过构造一个Bindings来实现的。
上面的各个类和简单的使用介绍了以后,是否可以直接调用其中的方法呢,而不是直接执行脚本,具体的如下:
@Test public void testJS1() throws Exception{ String script = "function add(a,b){return a + b;}"; ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); //开始对提交的脚本进行编译 engine.eval(script); Invocable invocable =(Invocable)engine; Object rs = invocable.invokeFunction("add",1,2); System.out.println(rs); }
类似于反射的方式来实现调用其中的方法。
经过上面的测试可以使用JS脚本做一些简单的东西,当然也可以进行绑定JAVA对象实例来做一些复杂的事情,下面基于JS脚本做动态接口的实现:
1 通过controller来调用某个接口查询某个接口的数据 2 不通的接口的具体实现通过JS的方式来动态提交编译
基于这个场景,编写的代码逻辑结构如下:
代码结构中部分内容未实现。下面看一下controller的代码:
@GetMapping(value = "/**") public RestDataResponse getMdataBasicData(HttpServletRequest request, HttpServletResponse response){ //从缓存队列中获取是否有对应的执行脚本。如果没有执行脚本,则抛出异常 JavaScript script = javaScriptManager.findScript(request.getRequestURI()); if(Objects.isNull(script)){ //抛出一个运行时的异常,用于在外部检查 throw new IllegalArgumentException("未查询到具体的执行脚本"); } return new SimpleResponse(new SimpleMetaResponse(request.getRequestURI(),""), basicApiService.executor(request.getRequestURI(),request.getParameterMap(),script)); }
在controller中自定义个方法处理所有的请求,请求过来后,首先从JS引擎集合中获取对应的脚本,如果如果获取到了,就可以进行后续的处理了,具体的处理是在BasicApiService汇总进行处理的,处理的方法为executor中,具体的逻辑为:
/** * 执行请求 */ public Object executor(String uri,Map<String,String[]> params,JavaScript script){ //构造开始开始执行 BasicQueryHandler queryHandler = BasicQueryHandler.builder().script(script).uri(uri).params(params).build() .checkRequestParameters() //处理参数,以及参数的校验 .builderBasicQueryParameters(); //构造查询数据库的对象 //开始执行请求 try { return queryHandler.executor(basicQueryExecutor).filter().result(); }catch (Exception e){ e.printStackTrace(); } }
具体的我们看下对应的JS的处理过程:
/** * 通过脚本执行请求 */ public void resolveQueryDataSourceByScript(BasicQueryExecutor basicQueryExecutor){ try { Map<String,Object> paramaters = Maps.newHashMap(); paramaters.put("mapper",basicQueryExecutor); paramaters.put("requestParameter",requestParameters); paramaters.put("handler",this); paramaters.put("LOGGER",LOGGER); paramaters.put("queryParameters", BasicQueryParametersBuilder.newInstance()); script.getCompiledScript().eval(new SimpleBindings(paramaters)); } catch (ScriptException e) { throw new IllegalArgumentException("执行脚本出现错误"); } script.getCompiledScript().getEngine().getBindings(ScriptContext.ENGINE_SCOPE).clear(); }
对应的JS脚本为:
function runExtractor(){ var now = new Date().getTime(); handler.setBody(mapper.selectOne(queryParameters .table('t') .of('tid','1') .limit(0,1) .builder())); LOGGER.info(' x -> ' + (new Date().getTime() - now)); } runExtractor();
截图中的为对应的JS,我们看下执行的结果:
数据库中的数据,我们也看一下:
可以看到明确的将数据查询出来了,并且打印出了查询结果,具体的测试代码如下:
@Test public void test() throws Exception{ //构造脚本对象 JavaScript script = builder("/api/test"); //提交加脚本 javaScriptManager.submit(script); //下面开始编写测试用例 //从缓存队列中获取是否有对应的执行脚本。如果没有执行脚本,则抛出异常 JavaScript javaScript = javaScriptManager.findScript("/api/test"); if(Objects.isNull(javaScript)){ //抛出一个运行时的异常,用于在外部检查 throw new IllegalArgumentException("未查询到具体的执行脚本"); } Object data = basicApiService.executor("/api/test",Maps.newHashMap(),script); System.out.println(data); } private JavaScript builder(String uuid){ //构造脚本对象 return JavaScript.builder() .auth(false).convert(true).type(JavaScript.RunningType.SCRIPT) .uuid(uuid) .uri("/api/test") .name("根据tid查询信息") .scirpt("function runExtractor(){\n" + " var now = new Date().getTime();\n" + " handler.setBody(mapper.selectOne(queryParameters\n" + " .table('t')\n" + " .of('tid','1')\n" + " .limit(0,1)\n" + " .builder()));\n" + " LOGGER.info(' x -> ' + (new Date().getTime() - now));\n" + "}\n" + "runExtractor();") .build(); }
测试测试通过后,可以看到接口耗时严重,具体的我们对比下执行查询的具体耗时是因为数据库查询本来就慢还是因为JS代码的加载耗时较高。我们针对这个接口做一下压测,看一下测试环境单台机器的QPS
直接压测,压测后的结果为:
通过观察日志可以看到,在查询和并发调用下JS的脚本性能和直接执行基本一致。JS脚本的这种业务常用于配置系统,早期爬虫为了动态配置,基本都采用这种模式来做。
2 动态类型
JDK 7 为了更好地支持动态类型语言,引入了第五条方法调用的字节码指令 invokedynamic,但前面一直没有再提到它,甚至把之前使用 MethodHandle 的示例代码反编译后也不会看见 invokedynamic 的身影
某种程度上可以说 invokedynamic 指令与 MethodHandle 机制的作用是一样的,都是为了解决原有四条 invoke* 指令方法分派规则固化在虚拟机之中的问题,把如何查找目标方法的决定权从虚拟机转嫁到具体用户代码之中,让用户(包含其他语言的设计者)有更高的自由度。而且,它们两者的思路也是可类比的,可以想象作为了达成同一个目的,一个用上层代码和 API 来实现,另一个是用字节码和 Class 中其他属性和常量来完成。因此,如果前面 MethodHandle 的例子看懂了,理解 invokedynamic 指令并不困难。
JDK 7 实现了 JSR 292 《Supporting Dynamically Typed Languages on the Java Platform》,新加入的 java.lang.invoke 包[ 3] 是就是 JSR 292 的一个重要组成部分,这个包的主要目的是在之前单纯依靠符号引用来确定调用的目标方法这条路之外,提供一种新的动态确定目标方法的机制,称为 Method Handle。
具体的测试例子如下:
@Test public void testRevoke() throws Throwable{ //构造执行参数 MethodExecutorParameters executorParameters = MethodExecutorParameters.builder() .object(new TestObject()) .method("add") .type(MethodTypeReferer.builder() .returnType(void.class) .argsList(new Class[]{int.class,int.class}).build()) .build(); //构造方法类型,填充方法对应的返回类型,参数类型 MethodType mt = MethodType.methodType(executorParameters.getType().getReturnType(), executorParameters.getType().getArgsList()); //构造方法执行handle MethodHandle handle = MethodHandles.lookup() .findVirtual(executorParameters.getObject().getClass(), executorParameters.getMethod(), mt) .bindTo(executorParameters.getObject()); //执行方法 handle.invokeExact(1,2); } @Builder @AllArgsConstructor @NoArgsConstructor public class MethodExecutorParameters { @Setter @Getter private MethodTypeReferer type; @Setter @Getter private Object object; @Setter @Getter private String method; } @Builder @AllArgsConstructor @NoArgsConstructor public class MethodTypeReferer { @Setter @Getter private Class returnType; @Setter @Getter private Class[] argsList; }
其实冲例子上看,这个和反射基本上没什么区别。首先构造方法的类型(这里类型包括返回结果类型,参数类型),构造处理器绑定方法名称,实例对象。执行时给定参数列表即可。
MethodHandle 的使用方法和效果上与 Reflection 都有众多相似之处。不过,它们也有以下这些区别:
- Reflection 和 MethodHandle 机制本质上都是在模拟方法调用,但是 Reflection 是在模拟 Java 代码层次的方法调用,而 MethodHandle 是在模拟字节码层次的方法调用。在 MethodHandles.Lookup 上的三个方法 findStatic()、findVirtual()、findSpecial() 正是为了对应于 invokestatic、invokevirtual & invokeinterface 和 invokespecial 这几条字节码指令的执行权限校验行为,而这些底层细节在使用 Reflection API 时是不需要关心的。
- Reflection 中的 java.lang.reflect.Method 对象远比 MethodHandle 机制中的 java.lang.invoke.MethodHandle 对象所包含的信息来得多。前者是方法在 Java 一端的全面映像,包含了方法的签名、描述符以及方法属性表中各种属性的 Java 端表示方式,还包含有执行权限等的运行期信息。而后者仅仅包含着与执行该方法相关的信息。用开发人员通俗的话来讲,Reflection 是重量级,而 MethodHandle 是轻量级。
- 由于 MethodHandle 是对字节码的方法指令调用的模拟,那理论上虚拟机在这方面做的各种优化(如方法内联),在 MethodHandle 上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。