java如何运行动态脚本

内容

  • 需求说明
  • 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实例的时候的数据流程

  1. 使用者执行new ScriptEngineManager(),进入该类的构造方法中
  2. 在构造方法中创建一个类加载器,将类加载器作为参数传到初始化方法init中
  3. 在init方法中初始化各属性,并将构造方法传入的类加载器实例传入初始化engines的initEngines方法
  4. 在initEngines方法中主要是通过serviceLoader类进行寻找执行动态语言的相关engines
  5. 至此,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传递参数的.

你可能感兴趣的:(java如何运行动态脚本)