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)){
//抛出一个运行时的异常,用于在外部检查
.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 上也应当可以采用类似思路去支持(但目前实现还不完善)。而通过反射去调用方法则不行。