从Java 8开始(nashorn在Java 15中去掉了),可以使用nashorn来执行Javascript代码,使用方法如下:
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval("var s = 'Hello World!'; print(s);");
Nashorn提供了非常简单的方式让用户可以在JS代码中引用Java的方法
public class NashornTest {
@Test
public void test() throws ScriptException {
String js = "var test = Java.type('test.NashornTest'); test.printName('abc')";
ScriptEngine engine = new ScriptEngineManager().getEngineByName("nashorn");
engine.eval(js);
}
public static void printName(String name) {
System.out.println("name is " + name);
}
}
Nashorn虽然强大,但如果在生产环境中使用Nashorn来执行用户自己写的JS代码还是得考虑以下问题:
1,如何防止用户在JS中使用比较危险的Java方法,如System.exit()?
2,如何防止用户的JS占用过多的内存和CPU资源?
3,如何提升JS执行的并发性能?
delight.nashornsandbox项目为开发者提供了Nashorn沙箱环境来执行JS代码,解决以上问题
1,可以在初始化Nashorn沙箱时定义一个白名单,只允许JS引用白名单中的Java类
public class NashornTest {
public static void printName(String name) {
System.out.println("name is " + name);
}
@Test
public void testSandbox() throws ScriptCPUAbuseException, ScriptException {
String js = "var test = Java.type('test.NashornTest'); test.printName('abc')";
NashornSandbox sandbox = NashornSandboxes.create("--language=es6");
// 允许JS中引用NashornTest类
sandbox.allow(NashornTest.class);
sandbox.eval(js);
}
}
2,设置线程每次执行JS使用的CPU和内存上限,需要指定ExecutorService作为JS的线程执行器
ExecutorService executor = Executors.newCachedThreadPool();
NashornSandbox sandbox = NashornSandboxes.create("--language=es6");
// 允许JS中引用NashornTest类
sandbox.allow(NashornTest.class);
// 设置CPU时间上限为3000毫秒,在JS中sleep并不占用这个时间
sandbox.setMaxCPUTime(3000);
// 设置内存上限为5MB
sandbox.setMaxMemory(5 * 1024 * 1024);
sandbox.allowNoBraces(false);
sandbox.allowLoadFunctions(true);
sandbox.allowPrintFunctions(true);
// 设置缓存JS代码数量,JS预编译后缓存起来
sandbox.setMaxPreparedStatements(50);
sandbox.setExecutor(executor);
如果设置了CPU和内存上限,Nashorn沙箱在执行JS的时候,就会额外启动一个监控线程,这个监控线程就会循环去调用ThreadMXBean.getThreadAllocatedBytes(执行线程Id) 和ThreadMXBean.getThreadCpuTime(执行线程Id) 这两个方法来判断CPU和内存是否达到上限,如果其中一个达到上限,则会中断JS的执行并抛出异常。
需要注意的是,ThreadMXBean.getThreadAllocatedBytes并不是返回指定线程所创建的对象目前占用的堆内存大小,而是返回指定线程已经分配的内存大小(已分配的内存可能已经被释放)
3,并发调用的性能问题,
1)可以创建一个Nashorn沙箱池,省去频繁创建和销毁沙箱的开销
2)设置缓存JS代码数量,提高JS执行速度,sandbox.setMaxPreparedStatements(maxCacheSize);
public class NashornSandboxFactory implements PoolObjectFactory<NashornSandbox> {
private ExecutorService executor;
private long maxCpuTime;
private long maxMemorySize;
private int maxCacheSize;
/**
* @param executor
* @param maxCpuTime 最大CPU时间,毫秒
* @param maxMemorySize 最大内存,byte
* @param maxCacheSize 最大脚本缓存个数
*/
public NashornSandboxFactory(ExecutorService executor, long maxCpuTime, long maxMemorySize, int maxCacheSize) {
this.executor = executor;
this.maxCpuTime = maxCpuTime;
this.maxMemorySize = maxMemorySize;
this.maxCacheSize = maxCacheSize;
}
@Override
public NashornSandbox create() {
NashornSandbox sandbox = NashornSandboxes.create("--language=es6");
// 沙箱初始化
return sandbox;
}
@Override
public boolean readyToTake(NashornSandbox obj) {
return true;
}
@Override
public boolean readyToRestore(NashornSandbox obj) {
return true;
}
@Override
public void destroy(NashornSandbox obj) {
// do nothing
}
PoolService<NashornSandbox> pool = new ConcurrentPool<>(new ConcurrentLinkedQueueCollection<>(), factory, coreSize, maxSize, false);
NashornSandbox sandbox = pool.tryTake(timeout, TimeUnit.MILLISECONDS);
sandbox.eval(js);
虽然Java 15已经去掉了nashorn,但可以在pom.xml中单独引入
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.3</version>
</dependency>