Java Script Engine
Java 脚本引擎可以将脚本嵌入Java代码中,可以自定义和扩展Java应用程序,自JDK1.6被引入,基于Rhino引擎,JDK1.8后使用Nashorn引擎,支持ECMAScript 5,但后期还可能会换。
脚本引擎包位于javax.script中,各个类名及描述如下
接口
- Bindings
键值对映射,所有key都为String - Compilable
由具体的脚本引擎实现,用于将脚本进行编译,可重复使用。 - Invocable 由具体的脚本引擎实现,其允许调用先前已执行的脚本
- ScriptContext
脚本引擎上下文,用于将应用程序与脚本引擎进行绑定 - ScriptEngine
由具体的脚本引擎实现,定义了执行脚本的方法、键值对映射关系、脚本引擎上下文 - ScriptEngineFactory
脚本引擎工厂,每一个ScriptEngine都有一个对应的工厂。ScriptEngineManager会从ClassLoader中获取所有的ScriptEngineFactories实例
类
- AbstractScriptEngine
ScriptEngine的抽象实现类,提供了ScriptEngine的标准实现 - CompiledScript
由存储编译结果的类扩展。可以以Java类、Java类文件或者脚本操作码的形式存储,可以重复执行无需重新解析。每个CompiledScript都与一个ScriptEngine相关联,调用CompiledScript的eval方法会导致ScriptEngine执行 - ScriptEngineManager
脚本引擎管理器,提供ScriptEngine的实例化机制,并维护了一些键/值对集合,供所有创建的ScriptEngine共享使用 - SimpleBindings
使用HashMap 或者其他Map实现的一种简单键值映射 - SimpleScriptContext
ScriptContext 的一种简单实现
异常
- ScriptException
脚本API的通用异常类,抛出的异常类具有文件名、行号、列号信息
示例
简单的
@Test public void scriptTest() throws ScriptException { ScriptEngineManager engineManager = new ScriptEngineManager(); //获取JavaScript解析引擎 ScriptEngine engine = engineManager.getEngineByName("JavaScript"); //将x变量映射为Hello World! engine.put("x", "Hello World!"); engine.eval("print(x)"); } //输出 //Hello World!
较复杂的
从文件中读取脚本
/** * 从文件中读取Js脚本 * test.js 中的内容: * var obj = new Object(); * obj.hello = function (name) { * print('Hello, ' + name); * } * @throws Exception */ @Test public void file() throws Exception{ ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); engine.eval(new FileReader(new File("script/test.js"))); Invocable inv = (Invocable) engine; Object obj = engine.get("obj"); inv.invokeMethod(obj, "hello", "Script Test!" ); }
将Java变量注入脚本中
有可能需要在脚本中使用Java变量
@Test public void scriptVar() throws Exception{ ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); File file = new File("F:/test/test.txt"); //将File对象f直接注入到js脚本中并可以作为全局变量使用 engine.put("files", file); engine.eval("print(files.getPath());print(files.getName());"); }
调用脚本中的方法
使用Invocable 调用已经加载脚本中的方法
@Test public void scriptTest1() throws ScriptException, NoSuchMethodException { ScriptEngineManager engineManager = new ScriptEngineManager(); ScriptEngine engine = engineManager.getEngineByName("JavaScript"); StringBuilder sb = new StringBuilder(); sb.append("var obj = new Object();"); sb.append("obj.hello = function(name){print('Hello, ' + name);}"); engine.eval(sb.toString()); //Invocable 可以调用已经加载过的脚本 Invocable invocable = (Invocable) engine; //获取脚本的obj对象 Object object = engine.get("obj"); //调用obj对象的hello函数 invocable.invokeMethod(object, "hello", "Script Method!"); } //输出 //Hello, Script Method!
多个作用域
一个脚本引擎,多个scope,x变量并没有覆盖之前的变量
@Test public void scriptTest() throws ScriptException { ScriptEngineManager engineManager = new ScriptEngineManager(); ScriptEngine engine = engineManager.getEngineByName("JavaScript"); engine.put("x", "Hello World!"); engine.eval("print(x)"); ScriptContext context = new SimpleScriptContext(); //新的Script context绑定ScriptContext的ENGINE_SCOPE Bindings bindings = context.getBindings(ScriptContext.ENGINE_SCOPE); // 增加一个新变量到新的范围 engineScope 中 bindings.put("x", "word hello!!"); // 执行同一个脚本 - 但这次传入一个不同的script context engine.eval("print(x);", bindings); engine.eval("print(x);"); } //输出 //Hello World! //word hello!! //Hello World!
使用脚本实现Java接口
@Test public void runnableImpl() throws Exception{ ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); // String里定义一段JavaScript代码脚本 String script = "function run() { print('run called'); }"; // 执行这个脚本 engine.eval(script); // 从脚本引擎中获取Runnable接口对象(实例). 该接口方法由具有相匹配名称的脚本函数实现。 Invocable inv = (Invocable) engine; // 在上面的脚本中,我们已经实现了Runnable接口的run()方法 Runnable runnable = inv.getInterface(Runnable.class); // 启动一个线程运行上面的实现了runnable接口的script脚本 Thread thread = new Thread(runnable); thread.start(); Thread.sleep(1000); }
如果脚本是基于对象的,则可以通过执行脚本的方法来实现Java接口,避免调用脚本的全局函数。
@Test public void runnableObject() throws ScriptException { ScriptEngineManager manager = new ScriptEngineManager(); ScriptEngine engine = manager.getEngineByName("JavaScript"); String script = "var obj = new Object();obj.run = function() {println('run method called')}"; engine.eval(script); //获得脚本对象 Object object = engine.get("obj"); Invocable invocable = (Invocable) engine; Runnable runnable = invocable.getInterface(object, Runnable.class); Thread thread = new Thread(runnable); thread.start(); Thread.sleep(1000); }
Groovy
1、特点
- groovy跟java都是基于jvm的语言,可以在java项目中集成groovy并充分利用groovy的动态功能;
- groovy兼容几乎所有的java语法,开发者完全可以将groovy当做java来开发,甚至可以不使用groovy的特有语法,仅仅通过引入groovy并使用它的动态能力;
- groovy可以直接调用项目中现有的java类(通过import导入),通过构造函数构造对象并直接调用其方法并返回结果;
- groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。因此我们可以通过将spring的bean预设到GroovyShell运行环境中,在groovy动态脚本中直接调用spring容器中bean来调用其方法
2、groovy动态脚本的使用
2.1 直接调用java类
在上一节中集成groovy的好处中提到,groovy可以通过import的方式直接调用java类,直接上代码:
package pers.doublebin.example.groovy.script.service; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; public class TestService { public String testQuery(long id){ return "Test query success, id is " + id; } public static void main(String[] args) { Binding groovyBinding = new Binding(); GroovyShell groovyShell = new GroovyShell(groovyBinding); String scriptContent = "import pers.doublebin.example.groovy.script.service.TestService\n" + "def query = new TestService().testQuery(1L);\n" + "query"; Script script = groovyShell.parse(scriptContent); System.out.println(script.run()); } }
返回结果:
Test query success, id is 1
这种方式在groovy动态脚本中将类import后直接new了一个新对象,并调用对象的方法。
2.2 通过GroovyShell预设对象
在上一节中提到,groovy支持通过GroovyShell预设对象,在groovy动态脚本中直接调用预设对象的方法。直接上代码:
package pers.doublebin.example.groovy.script.service; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; public class TestService { public String testQuery(long id){ return "Test query success, id is " + id; } public static void main(String[] args) { Binding groovyBinding = new Binding(); groovyBinding.setVariable("testService", new TestService()); GroovyShell groovyShell = new GroovyShell(groovyBinding); String scriptContent = "def query = testService.testQuery(2L);\n" + "query"; Script script = groovyShell.parse(scriptContent); System.out.println(script.run()); } }
返回结果:
Test query success, id is 1
这种方式通过Binding对象的setVariable方法设置了预设对象testService,在动态脚本中便可以直接调用testService的方法。简单看下Binding类setVariable方法的源码:
public void setVariable(String name, Object value) { if (this.variables == null) { this.variables = new LinkedHashMap(); } this.variables.put(name, value); }
实际上,Binding对象维护了一个Map类型的属性variables,通过setVariable方法将预设对象和预设对象名称存储到了variables属性中,动态运行时会尝试道variables中获取对应名称的对象,如果存在再尝试调用其方法。
2.3 groovy脚本中调用springbean的方法
到这里已经很清晰了,我们只要能获取spring容器中所有的bean,通过Binding的setVariable将spring所有的bean预设进GroovyShell运行环境,在动态脚本中便可以直接调用bean的方法。这种我们对spring项目中的service层、controller层、DAO层等注册的bean均可以通过这种方式实现动态调用。
三、springboot接口动态运行groovy脚本
下面以一个springboot接口动态运行groovy脚本的示例工程为例,讲述如何在springboot接口中动态运行groovy脚本。
3.1 引入groovy-all依赖
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>2.4.7</version> </dependency>
3.2 Service层示例类
package pers.doublebin.example.groovy.script.service; import groovy.lang.Binding; import groovy.lang.GroovyShell; import groovy.lang.Script; import org.springframework.stereotype.Service; @Service public class TestService { public String testQuery(long id){ return "Test query success, id is " + id; }
TestService 类实现了一个简单的testQuery方法,springboot通过扫描到@Service注解会将生成TestService的bean并注册到应用上下文中,beanName为"testService".
3.3 springboot的Configuration类中设置Binding
首先配置类可以实现org.springframework.context.ApplicationContextAware接口用来获取应用上下文,然后再配置类中通过应用上下文获取所有的bean并注册到groovy的Binding中,看源码:
package pers.doublebin.example.groovy.script.config; import groovy.lang.Binding; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.Map; @Configuration public class GroovyBindingConfig implements ApplicationContextAware { private ApplicationContext applicationContext; @Bean("groovyBinding") public Binding groovyBinding() { Binding groovyBinding = new Binding(); Map<String, Object> beanMap = applicationContext.getBeansOfType(Object.class); //遍历设置所有bean,可以根据需求在循环中对bean做过滤 for (String beanName : beanMap.keySet()) { groovyBinding.setVariable(beanName, beanMap.get(beanName)); } return groovyBinding; } /*@Bean("groovyBinding1") public Binding groovyBinding1() { Map<String, Object> beanMap = applicationContext.getBeansOfType(Object.class); return new Binding(beanMap); //如果不需要对bean做过滤,直接用beanMap构造Binding对象即可 }*/ @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; } }
如果不需要对bean做过滤,可以通过注释掉的方法直接从应用上下文中获取beanMap并直接构造Binding的variables中;当然上面示例applicationContext.getBeansOfType方法也可以指定获取bean的类型。
需要注意的是:上面这种方法注册的到binding中beanMap是不包含groovyBinding这个对象本身的(先后顺序的原因),如果需要将binding对象本身(也是一个bean)注册,也很简单,只需要将Binding的bean生成放在GroovyBindingConfig之前,并且在实现ApplicationContextAware接口的setApplicationContext方法中进行variables的设置即可,但建议不这样做,因为这样就可以通过脚本对Binding对象本身造成破坏,不太优雅~
3.4 实现用于groovy动态脚本运行的controller
直接看源码:
package pers.doublebin.example.groovy.script.controller; import groovy.lang.Binding; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyShell; import groovy.lang.Script; import org.codehaus.groovy.control.CompilerConfiguration; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import pers.doublebin.example.groovy.script.component.TestScript; import javax.annotation.PostConstruct; @RestController @RequestMapping("/groovy/script") public class GroovyScriptController { @Autowired private Binding groovyBinding; private GroovyShell groovyShell; @PostConstruct public void init(){ GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader()); CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); compilerConfiguration.setSourceEncoding("utf-8"); compilerConfiguration.setScriptBaseClass(TestScript.class.getName()); groovyShell = new GroovyShell(groovyClassLoader, groovyBinding, compilerConfiguration); } @RequestMapping(value = "/execute", method = RequestMethod.POST) public String execute(@RequestBody String scriptContent) { Script script = groovyShell.parse(scriptContent); return String.valueOf(script.run()); } }
将binding对象注入后,在初始化方法init()中用binding对象构造GroovyShell对象,在提供的execute接口实现中用GroovyShell对象生成Script脚本对象,并调用Script的run()方法运行动态脚本并返回结果。
上述示例中只是一个简单实现,在接口方法execute中,每次脚本运行前都会通过groovyShell来parse出一个Script 对象,这其实是有成本的,实际应用中可根据脚本特征(如md5值等)将script存储, 下次运行时可根据脚本特征直接获取Script对象,避免parse的成本。
3.5 实现用于groovy动态脚本运行的controller
使用示例:
上述接口定义了一个post方法,path:/groovy/script/execute,运行后直接用postman调用测试testService的方法,结果如下:
显然,通过接口直接用groovy脚本调用了testService这个bean的方法,非常简单。