内容
- 需求说明
- java如何运行动态脚本
- ScriptEngine详解
需求说明
最近自己在用spring boot开发一个东西,遇到需要执行用户编写的动态语言脚本,如python,js,groovy,ruby等动态语言.
java如何运动动态脚本
就拿groovy来举例子吧
在查阅的资料中,java运行groovy语言主要通过三种方式
- GroovyShell
- GroovyClassLoader
- ScriptEngine:JSR-223是推荐的一种使用策略.规范化,简便的运行动态语言脚本
GroovyShell
创建GroovyShell实例
通过evaluate方法执行groovy代码片段
我用的是gradle,如果你想运行关于本文中关于GroovyShell的demo,需要添加以下依赖项
compile group: 'org.codehaus.groovy', name: 'groovy-jsr223', version: '3.0.0-alpha-4'
import groovy.lang.GroovyShell;
public class RunGroovyShell {
public static void main(String [] args) {
GroovyShell groovyShell = new GroovyShell();
groovyShell.evaluate("println 'Hello World'");
}
}
让我看一下GroovyShell里面有什么东西
/**
* Represents a groovy shell capable of running arbitrary groovy scripts
*
* @author James Strachan
* @author Guillaume Laforge
* @author Paul King
*/
在GroovyShell类头上有这么一段注释,意思就是GroovyShell类可以运行任何Groovy脚本
public GroovyShell() {
this(null, new Binding());
}
public GroovyShell(Binding binding) {
this(null, binding);
}
public GroovyShell(ClassLoader parent, CompilerConfiguration config) {
this(parent, new Binding(), config);
}
public GroovyShell(CompilerConfiguration config) {
this(new Binding(), config);
}
public GroovyShell(Binding binding, CompilerConfiguration config) {
this(null, binding, config);
}
public GroovyShell(ClassLoader parent, Binding binding) {
this(parent, binding, CompilerConfiguration.DEFAULT);
}
public GroovyShell(ClassLoader parent) {
this(parent, new Binding(), CompilerConfiguration.DEFAULT);
}
public GroovyShell(ClassLoader parent, Binding binding, final CompilerConfiguration config) {
if (binding == null) {
throw new IllegalArgumentException("Binding must not be null.");
}
if (config == null) {
throw new IllegalArgumentException("Compiler configuration must not be null.");
}
final ClassLoader parentLoader = (parent!=null)?parent:GroovyShell.class.getClassLoader();
this.loader = AccessController.doPrivileged(new PrivilegedAction() {
public GroovyClassLoader run() {
return new GroovyClassLoader(parentLoader,config);
}
});
this.context = binding;
this.config = config;
}
/**
* Creates a child shell using a new ClassLoader which uses the parent shell's
* class loader as its parent
*
* @param shell is the parent shell used for the variable bindings and the parent class loader
*/
public GroovyShell(GroovyShell shell) {
this(shell.loader, shell.context);
}
如上该类共有7种构造方法
public Object run(String scriptText, String fileName, List list) throws CompilationFailedException {
return run(scriptText, fileName, (String[]) list.toArray(EMPTY_STRING_ARRAY));
}
传入脚本和参数进行运行
public Object run(final File scriptFile, String[] args) throws CompilationFailedException, IOException{
String scriptName = scriptFile.getName();
int p = scriptName.lastIndexOf(".");
if (p++ >= 0) {
if (scriptName.substring(p).equals("java")) {
throw new CompilationFailedException(0, null);
}
}
// Get the current context classloader and save it on the stack
final Thread thread = Thread.currentThread();
//ClassLoader currentClassLoader = thread.getContextClassLoader();
class DoSetContext implements PrivilegedAction {
ClassLoader classLoader;
public DoSetContext(ClassLoader loader) {
classLoader = loader;
}
public Object run() {
thread.setContextClassLoader(classLoader);
return null;
}
}
AccessController.doPrivileged(new DoSetContext(loader));
// Parse the script, generate the class, and invoke the main method. This is a little looser than
// if you are compiling the script because the JVM isn't executing the main method.
Class scriptClass;
try {
scriptClass = AccessController.doPrivileged(new PrivilegedExceptionAction() {
public Class run() throws CompilationFailedException, IOException {
return loader.parseClass(scriptFile);
}
});
} catch (PrivilegedActionException pae) {
Exception e = pae.getException();
if (e instanceof CompilationFailedException) {
throw (CompilationFailedException) e;
} else if (e instanceof IOException) {
throw (IOException) e;
} else {
throw (RuntimeException) pae.getException();
}
}
return runScriptOrMainOrTestOrRunnable(scriptClass, args);
// Set the context classloader back to what it was.
//AccessController.doPrivileged(new DoSetContext(currentClassLoader));
}
上面一个运行脚本的run方法真正执行的方法
上面的run方法只是该类中的运行脚本的中某一个run方法,还有多个重载的run方法,进行运行脚本的操作
public Object evaluate(final String scriptText) throws CompilationFailedException {
return evaluate(scriptText, generateScriptName(), DEFAULT_CODE_BASE);
}
这个方法就是例子中我们调用的例子中的执行groovy脚本的方法,当然,该方法也有多个重载
通过查看evaluate方法可以得知,真正运行脚本的方法是parse方法,通过该方法进行运行传入的各种参数形式的groovy脚本.
关于GroovyShell通过调用evaluate执行groovy脚本,传入的参数可以是代码片段,也可以是groovy脚本文件
- 如果你想要在实际生产开发中是使用GroovyShell进行动态运行Groovy脚本,则关于FullGC问题你不得不注意,这里不做过多的解释,详情可以参考https://my.oschina.net/u/816594/blog/388590
GroovyClassLoader
GroovyClassLoader classLoader = new GroovyClassLoader(Thread.currentThread().getContextClassLoader());
File sourceFile = new File("D:\\TestGroovy.groovy");//文本内容的源代码
Class testGroovyClass = classLoader.parseClass(new GroovyCodeSource(sourceFile));
GroovyObject instance = (GroovyObject)testGroovyClass.newInstance();//proxy
Long time = (Long)instance.invokeMethod("getTime", new Date());
System.out.println(time);
Date date = (Date)instance.invokeMethod("getDate", time);
System.out.println(date.getTime());
//here
instance = null;
testGroovyClass = null;
这种方法不做深入介绍,同样是可以用来运行groovy脚本,也会编译生成class类,有可能出现FullGC问题
ScriptEngine
首先,这不只是为java运行动态编程语言groovy专门的方法,而是为了能够运行包括groovy在内的js,python等动态脚本.
首先给出通过这个类运行Groovy脚本的demo
我使用的gradle,如果你想运行本文下面的demo,你需要提价以下依赖
compile group: 'org.codehaus.groovy', name: 'groovy-jsr223', version: '3.0.0-alpha-4'
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
public class RunGroovyShell {
public static void main(String [] args) throws Exception {
ScriptEngineManager factory = new ScriptEngineManager();//step 1
ScriptEngine engine = factory.getEngineByName("groovy");//Step 2
engine.eval("println \"hellow word\"");//Step 3
}
}
demo执行结果为
My First Groovy shell
ScriptEngine详解
如上面的demo所示,通过SpringEngine执行一个简单的Groovy代码片段的的流程如下
创建一个ScriptEngineManager实体
通过String找到运行脚本所对应的engine实体
通过engine的eval方法运行代码片段
一起来看一下在new一个ScriptEngineManager实例的时候的数据流程
- 使用者执行new ScriptEngineManager(),进入该类的构造方法中
- 在构造方法中创建一个类加载器,将类加载器作为参数传到初始化方法init中
- 在init方法中初始化各属性,并将构造方法传入的类加载器实例传入初始化engines的initEngines方法
- 在initEngines方法中主要是通过serviceLoader类进行寻找执行动态语言的相关engines
- 至此,ScriptEngineManager实例的初始化就完毕了
public ScriptEngineManager() {
ClassLoader var1 = Thread.currentThread().getContextClassLoader();
this.init(var1);
}
private void init(ClassLoader var1) {
this.globalScope = new SimpleBindings();
this.engineSpis = new HashSet();
this.nameAssociations = new HashMap();
this.extensionAssociations = new HashMap();
this.mimeTypeAssociations = new HashMap();
this.initEngines(var1);
}
private void initEngines(final ClassLoader var1) {
Iterator var2 = null;
try {
ServiceLoader var3 = (ServiceLoader)AccessController.doPrivileged(new PrivilegedAction>() {
public ServiceLoader run() {
return ScriptEngineManager.this.getServiceLoader(var1);
}
});
var2 = var3.iterator();
} catch (ServiceConfigurationError var5) {
System.err.println("Can't find ScriptEngineFactory providers: " + var5.getMessage());
return;
}
try {
while(var2.hasNext()) {
try {
ScriptEngineFactory var7 = (ScriptEngineFactory)var2.next();
this.engineSpis.add(var7);
} catch (ServiceConfigurationError var4) {
System.err.println("ScriptEngineManager providers.next(): " + var4.getMessage());
}
}
} catch (ServiceConfigurationError var6) {
System.err.println("ScriptEngineManager providers.hasNext(): " + var6.getMessage());
}
}
在demo中真正执行动态编程语言的是ScriptEngine类,而该类的实例有四种方法获得
getEngineByName 通name获得相应的engine
getEngineByExtension 通过Extension获得相应的engine
getEngineByMimeType 通过MimeType获得相应的engine
getEngineFactories 获得该ScriptEngineManager中的List
注意事项
- 在平时用的比较多的就是byName,byMimeType
- 在使用byName的时候,如果没有引入动态语言相关的依赖包,比如你想要运行groovy脚本,这是你通过byName方法传入"groovy"字符串,这时候返回的是null,每一种动态语言都有不同的依赖包,可以到maven中心仓库搜索
public ScriptEngine getEngineByName(String var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
Object var2;
if (null != (var2 = this.nameAssociations.get(var1))) {
ScriptEngineFactory var3 = (ScriptEngineFactory)var2;
try {
ScriptEngine var13 = var3.getScriptEngine();
var13.setBindings(this.getBindings(), 200);
return var13;
} catch (Exception var11) {
;
}
}
Iterator var12 = this.engineSpis.iterator();
label50:
while(var12.hasNext()) {
ScriptEngineFactory var4 = (ScriptEngineFactory)var12.next();
List var5 = null;
try {
var5 = var4.getNames();
} catch (Exception var9) {
;
}
if (var5 != null) {
Iterator var6 = var5.iterator();
while(true) {
String var7;
do {
if (!var6.hasNext()) {
continue label50;
}
var7 = (String)var6.next();
} while(!var1.equals(var7));
try {
ScriptEngine var8 = var4.getScriptEngine();
var8.setBindings(this.getBindings(), 200);
return var8;
} catch (Exception var10) {
;
}
}
}
}
return null;
}
}
public ScriptEngine getEngineByExtension(String var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
Object var2;
if (null != (var2 = this.extensionAssociations.get(var1))) {
ScriptEngineFactory var3 = (ScriptEngineFactory)var2;
try {
ScriptEngine var13 = var3.getScriptEngine();
var13.setBindings(this.getBindings(), 200);
return var13;
} catch (Exception var11) {
;
}
}
Iterator var12 = this.engineSpis.iterator();
label51:
while(var12.hasNext()) {
ScriptEngineFactory var4 = (ScriptEngineFactory)var12.next();
List var5 = null;
try {
var5 = var4.getExtensions();
} catch (Exception var9) {
;
}
if (var5 != null) {
Iterator var6 = var5.iterator();
while(true) {
String var7;
do {
if (!var6.hasNext()) {
continue label51;
}
var7 = (String)var6.next();
} while(!var1.equals(var7));
try {
ScriptEngine var8 = var4.getScriptEngine();
var8.setBindings(this.getBindings(), 200);
return var8;
} catch (Exception var10) {
;
}
}
}
}
return null;
}
}
public ScriptEngine getEngineByMimeType(String var1) {
if (var1 == null) {
throw new NullPointerException();
} else {
Object var2;
if (null != (var2 = this.mimeTypeAssociations.get(var1))) {
ScriptEngineFactory var3 = (ScriptEngineFactory)var2;
try {
ScriptEngine var13 = var3.getScriptEngine();
var13.setBindings(this.getBindings(), 200);
return var13;
} catch (Exception var11) {
;
}
}
Iterator var12 = this.engineSpis.iterator();
label51:
while(var12.hasNext()) {
ScriptEngineFactory var4 = (ScriptEngineFactory)var12.next();
List var5 = null;
try {
var5 = var4.getMimeTypes();
} catch (Exception var9) {
;
}
if (var5 != null) {
Iterator var6 = var5.iterator();
while(true) {
String var7;
do {
if (!var6.hasNext()) {
continue label51;
}
var7 = (String)var6.next();
} while(!var1.equals(var7));
try {
ScriptEngine var8 = var4.getScriptEngine();
var8.setBindings(this.getBindings(), 200);
return var8;
} catch (Exception var10) {
;
}
}
}
}
return null;
}
}
public List getEngineFactories() {
ArrayList var1 = new ArrayList(this.engineSpis.size());
Iterator var2 = this.engineSpis.iterator();
while(var2.hasNext()) {
ScriptEngineFactory var3 = (ScriptEngineFactory)var2.next();
var1.add(var3);
}
return Collections.unmodifiableList(var1);
}
ScriptEngine实例是如何运行相应的动态编程语言的呢,让我们通过数据流程看一下
1.执行eval方法,进入实现ScriptEngine抽象类的AbstractScriptEngine类的eval方法
2.将参数进行加工,转到GroovyScriptEngineImpl类,该类继承了AbstractScriptEngine
使用该类的eval(String script, ScriptContext ctx)方法执行脚本
public Object eval(String var1) throws ScriptException {
return this.eval((String)var1, (ScriptContext)this.context);
}
public Object eval(String script, ScriptContext ctx)
throws ScriptException {
try {
String val = (String) ctx.getAttribute("#jsr223.groovy.engine.keep.globals", ScriptContext.ENGINE_SCOPE);
ReferenceBundle bundle = ReferenceBundle.getHardBundle();
if (val != null && val.length() > 0) {
if (val.equalsIgnoreCase("soft")) {
bundle = ReferenceBundle.getSoftBundle();
} else if (val.equalsIgnoreCase("weak")) {
bundle = ReferenceBundle.getWeakBundle();
} else if (val.equalsIgnoreCase("phantom")) {
bundle = ReferenceBundle.getPhantomBundle();
}
}
globalClosures.setBundle(bundle);
} catch (ClassCastException cce) { /*ignore.*/ }
try {
Class> clazz = getScriptClass(script);
if (clazz == null) throw new ScriptException("Script class is null");
return eval(clazz, ctx);
} catch (Exception e) {
if (debug) e.printStackTrace();
throw new ScriptException(e);
}
}
ScriptEngine为抽象类,里面的内容主要定义了执行脚本的方法
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//
package javax.script;
import java.io.Reader;
public interface ScriptEngine {
String ARGV = "javax.script.argv";
String FILENAME = "javax.script.filename";
String ENGINE = "javax.script.engine";
String ENGINE_VERSION = "javax.script.engine_version";
String NAME = "javax.script.name";
String LANGUAGE = "javax.script.language";
String LANGUAGE_VERSION = "javax.script.language_version";
Object eval(String var1, ScriptContext var2) throws ScriptException;
Object eval(Reader var1, ScriptContext var2) throws ScriptException;
Object eval(String var1) throws ScriptException;
Object eval(Reader var1) throws ScriptException;
Object eval(String var1, Bindings var2) throws ScriptException;
Object eval(Reader var1, Bindings var2) throws ScriptException;
void put(String var1, Object var2);
Object get(String var1);
Bindings getBindings(int var1);
void setBindings(Bindings var1, int var2);
Bindings createBindings();
ScriptContext getContext();
void setContext(ScriptContext var1);
ScriptEngineFactory getFactory();
}
真正开始进行执行脚本的工作是实现了ScriptEngine的AbstractScriptEngine,而AbstractScriptEngine会将将要执行的代码作为参数进行包装,转发给不同动态语言对应的专门运行类(如GroovyScriptEngineImpl),由此类进行脚本的运行.
在上面的源码中可以看出,在执行动态脚本的时候是可以通过Bindings传递参数的.