加入依赖
<dependency>
<groupId>org.codehaus.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>3.0.11</version>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.craftercms</groupId>
<artifactId>groovy-sandbox</artifactId>
<version>4.0.0</version>
</dependency>
创建Groovy工具类GroovyUtils
和Groovy拦截器GroovyNotSupportInterceptor
GroovyNotSupportInterceptor :用来防止在Groovy 中使用Java一些api 导致安全问题
package com.poctip.common.utils.groovy;
import cn.hutool.core.lang.Pair;
import cn.hutool.crypto.SecureUtil;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import groovy.transform.ThreadInterrupt;
import lombok.extern.slf4j.Slf4j;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.kohsuke.groovy.sandbox.SandboxTransformer;
import org.springframework.beans.factory.DisposableBean;
import java.util.Objects;
import java.util.concurrent.*;
import java.util.function.Supplier;
/**
* groovy 工具
*
* @author minjianguo
* @date 2022/10/12
*/
@Slf4j
public class GroovyUtils implements DisposableBean {
/**
* groovy脚本执行线程池
*/
private final ThreadPoolExecutor threadPool;
/**
* groovy Scriopt对象缓存缓存
*/
private final Cache<String, Script> innerScriptLruCache;
public GroovyUtils() {
threadPool = new ThreadPoolExecutor(Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(), 30, TimeUnit.SECONDS, new LinkedBlockingQueue<>(100));
innerScriptLruCache = CacheBuilder.newBuilder()
//最大容量
.maximumSize(500)
//当缓存项在指定的时间段内没有被读或写就会被回收
.expireAfterAccess(12, TimeUnit.HOURS)
//当缓存项上一次更新操作之后的多久会被刷新
//.refreshAfterWrite(5, TimeUnit.SECONDS)
// 设置并发级别为cpu核心数
.concurrencyLevel(Runtime.getRuntime().availableProcessors())
.build();
}
/**
* 创建Groovy脚本实例
* Create Groovy script instance
*
* @return
*/
private static GroovyShell createGroovyShell() {
CompilerConfiguration config = new CompilerConfiguration();
config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt.class));
// 沙盒环境
config.addCompilationCustomizers(new SandboxTransformer());
// 注册方法拦截
new GroovyNotSupportInterceptor().register();
return new GroovyShell(config);
}
public void clearCache() {
innerScriptLruCache.cleanUp();
}
/**
* 执行groovy脚本
*
* @param data 数据
* @param dataName 数据名称
* @param scriptString 脚本字符串
* @param invokeMethodSupplier invoke方法供应商
* @return {@link Object}
*/
public <T> Object executeGroovyScript(T data, String dataName, String scriptString, Supplier<Pair<String, Object>> invokeMethodSupplier) {
Binding binding = new Binding();
binding.setProperty(dataName, data);
GroovyShell groovyShell = createGroovyShell();
String scriptKey = SecureUtil.md5(scriptString);
Script groovyScript = innerScriptLruCache.getIfPresent(scriptKey);
if (Objects.isNull(groovyScript)) {
log.debug("reload script cache:\n ----------\n" + scriptString + "\n ----------");
groovyScript = groovyShell.parse(scriptString);
innerScriptLruCache.put(scriptKey, groovyScript);
}
groovyScript.setBinding(binding);
// 执行方法
Pair<String, Object> pair = invokeMethodSupplier.get();
if (Objects.nonNull(pair)) {
return groovyScript.invokeMethod(pair.getKey(), pair.getValue());
}
Future<Object> future = threadPool.submit((Callable<Object>) groovyScript::run);
try {
return future.get(60, TimeUnit.SECONDS);
} catch (TimeoutException exception) {
future.cancel(true);
log.error("TimeoutException,try cancel future task, is cancelled", exception);
//do something else
} catch (InterruptedException exception) {
future.cancel(true);
log.error("InterruptedException,try cancel future task, is cancelled", exception);
} catch (ExecutionException exception) {
future.cancel(true);
log.error("ExecutionException,try cancel future task, is cancelled", exception);
}
return null;
}
/**
* Invoked by the containing {@code BeanFactory} on destruction of a bean.
*
* @throws Exception in case of shutdown errors. Exceptions will get logged
* but not rethrown to allow other beans to release their resources as well.
*/
@Override
public void destroy() throws Exception {
log.info("clear GroovyUtils");
threadPool.shutdown();
innerScriptLruCache.cleanUp();
}
}
GroovyNotSupportInterceptor
package com.poctip.common.utils.groovy;
import org.kohsuke.groovy.sandbox.GroovyInterceptor;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* groovy不支持拦截器
*
* @author minjianguo
* @date 2022/09/29
*/
public class GroovyNotSupportInterceptor extends GroovyInterceptor {
public static final List<String> defaultMethodBlacklist = Arrays.asList( "class", "wait", "notify", "notifyAll", "invokeMethod", "finalize", "sleep");
/**
* 静态方法拦截
*
* @param invoker
* @param receiver
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object onStaticCall(Invoker invoker, Class receiver, String method, Object... args) throws Throwable {
if (receiver == System.class && "exit".equals(method)) {
// System.exit(0)
throw new SecurityException("No call on System.exit() please");
} else if (receiver == Runtime.class) {
// 通过Java的Runtime.getRuntime().exec()方法执行shell, 操作服务器…
throw new SecurityException("No call on RunTime please");
} else if (receiver == Class.class && "forName".equals(method)) {
throw new SecurityException("No call on forName please");
} else if (receiver == Class.class && Objects.equals(method, "newInstance")) {
throw new SecurityException("No call on Class.newInstance() please");
} else if (receiver.getName().startsWith("java.lang.reflect.")) {
throw new SecurityException("deny to use java.lang.reflect.* !");
}
return super.onStaticCall(invoker, receiver, method, args);
}
/**
* 普通方法拦截
*
* @param invoker
* @param receiver
* @param method
* @param args
* @return
* @throws Throwable
*/
@Override
public Object onMethodCall(Invoker invoker, Object receiver, String method, Object... args) throws Throwable {
if (defaultMethodBlacklist.contains(method)) {
// 方法列表黑名单
throw new SecurityException("Not support method: " + method);
}
return super.onMethodCall(invoker, receiver, method, args);
}
@Override
public Object onGetProperty(Invoker invoker, Object receiver, String property) throws Throwable {
if ("class".contains(property)) {
throw new SecurityException("Not support clz.class");
}
return super.onGetProperty(invoker, receiver, property);
}
}
注入到Spring
package com.poctip.common.config;
import com.poctip.common.utils.groovy.GroovyUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* groovy配置
*
* @author minjianguo
* @date 2022/10/12
*/
@Configuration(proxyBeanMethods = false)
public class GroovyConfig {
@Bean
public GroovyUtils groovyUtils() {
return new GroovyUtils();
}
}
使用
调用groovy脚本
@Test
public void test_1() {
GroovyUtils groovyUtils = new GroovyUtils();
StopWatch stopWatch = StopWatch.create("test1");
stopWatch.start();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hcy", 7);
String dataName = "data";
// 传入 groovy 代码字符串
String s = "1+2";
String o = (String) groovyUtils.executeGroovyScript(jsonObject, dataName, s, () -> null);
System.out.println("-------------" + o);
stopWatch.stop();
System.out.println(stopWatch.prettyPrint(TimeUnit.MILLISECONDS));
System.out.println();
System.out.println(jsonObject);
}
调用groovy方法
/**
* Rigorous Test :-)
*/
@Test
public void shouldAnswerWithTrue() {
GroovyUtils groovyUtils = new GroovyUtils();
JSONObject jsonObject = new JSONObject();
jsonObject.put("hcy", 123);
String dataName = "data";
String s = FileUtil.readString("hello.groovy", "utf-8");
ArrayList<Integer> integers = Lists.newArrayList(1, 2, 34, 5, 6);
JSONArray objects = new JSONArray();
objects.add(jsonObject);
Object o = groovyUtils.executeGroovyScript(null, dataName, s, () -> Pair.of("HelloWorld", new Object[]{objects,"我是方法"}));
System.out.println(o);
}