Groovy脚本是应用比较广泛的一种基于JVM的动态语言,Groovy可以补充Java静态方面能力的不足。一般语java结合的时候会有三种方式:GroovyClassLoader、GroovyShell和GroovyScriptEngine。
这三种方式用起来差不多,GroovyShell底层也是通过GroovyClassLoader实现的,GroovyScriptEngine是侧重多个脚本。
1.GroovyClassLoader
一般这种方式的用法是,调用parse()生成一个groovyClass,然后再去执行脚本。
Class<?> groovyClass = groovyClassLoader.parseClass(textCode);
// 获得Groovy的实例
GroovyObject groovyTempObject = (GroovyObject)groovyClass.newInstance();
Object object = groovyObject.invokeMethod(methodName, objects);
public Class parseClass(String text) throws CompilationFailedException {
return parseClass(text, "script" + System.currentTimeMillis() +
Math.abs(text.hashCode()) + ".groovy");
}
public Class parseClass(final String text, final String fileName) throws CompilationFailedException {
GroovyCodeSource gcs = AccessController.doPrivileged(new PrivilegedAction<GroovyCodeSource>() {
public GroovyCodeSource run() {
return new GroovyCodeSource(text, fileName, "/groovy/script");
}
});
gcs.setCachable(false);
return parseClass(gcs);
}
public Class parseClass(GroovyCodeSource codeSource) throws CompilationFailedException {
return parseClass(codeSource, codeSource.isCachable());
}
public Class parseClass(GroovyCodeSource codeSource, boolean shouldCacheSource) throws CompilationFailedException {
synchronized (sourceCache) {
Class answer = sourceCache.get(codeSource.getName());
if (answer != null) return answer;
answer = doParseClass(codeSource);
if (shouldCacheSource) sourceCache.put(codeSource.getName(), answer);
return answer;
}
}
可以看到parseClass的流程,如果没有传入脚本名称,groovy会生成一个新的脚本名称,这个名称是和时间戳绑定的。
fileName = System.currentTimeMillis() +Math.abs(text.hashCode()) + ".groovy")
之后会生成一个GroovyCodeSource对象,在doParseClass中去做加载类的动作。所以每次调用parseClass都会生成一个新的class文件。
private Class doParseClass(GroovyCodeSource codeSource) {
validate(codeSource);
Class answer;
CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource());
if (recompile!=null && recompile || recompile==null && config.getRecompileGroovySource()) {
unit.addFirstPhaseOperation(TimestampAdder.INSTANCE, CompilePhase.CLASS_GENERATION.getPhaseNumber());
}
SourceUnit su = null;
File file = codeSource.getFile();
if (file != null) {
su = unit.addSource(file);
} else {
URL url = codeSource.getURL();
if (url != null) {
su = unit.addSource(url);
} else {
su = unit.addSource(codeSource.getName(), codeSource.getScriptText());
}
}
//生成一个InnerLoader加载器
ClassCollector collector = createCollector(unit, su);
unit.setClassgenCallback(collector);
int goalPhase = Phases.CLASS_GENERATION;
if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT;
unit.compile(goalPhase);
answer = collector.generatedClass;
String mainClass = su.getAST().getMainClassName();
for (Object o : collector.getLoadedClasses()) {
Class clazz = (Class) o;
String clazzName = clazz.getName();
definePackage(clazzName);
//缓存class对象
setClassCacheEntry(clazz);
if (clazzName.equals(mainClass)) answer = clazz;
}
return answer;
}
可以看到在doParseClass方法中,每次都会生成一个InnerLoader加载器,并且会无条件的缓存class对象。
这里就要提一下groovy的类加载方式了。一般java的双亲委派方式是,Bootstrap <- Extension <-System <-User ClassLoader,所以在groovy这层,在此基础上会变成Bootstrap <- Extension <-System <- RootLoader <- GroovyClassLoader <-InnerLoader ,当然如果直接用 GroovyClassLoader的方式,就不会有RootLoader加载器。
当项目中大量调用groovy脚本的时候,很容易会出现OOM的问题,这种情况大概率是因为InnerLoader和class文件都无法在gc的时候被回收,运行较长一段时间后将老年代占满,一直触发fullgc。
我们都知道,一个Class只有满足以下三个条件,才能被GC回收:
1. 该类所有的实例都已经被GC,也就是JVM中不存在该Class的任何实例;
2. 加载该类的ClassLoader已经被GC;
3. 该类的java.lang.Class对象没有在任何地方被引用,如不能在任何地方通过反射访问该类的方法。
显然classCache被GroovyClassLoader持有,每个class对象都存在引用,无法被gc清理掉。
解决:
(1)外层加个缓存,每次拿groovyClass的时候先从缓存中拿。
这样可以在一定程度上解决问题。对于同一个名称的脚本,每次调用缓存中的数据。但是如果时间比较长,不同名称的脚本数量就非常多了,还是会有gc的问题。
(2)clearCache()
以效率的代价,来解决gc的问题。所以clearCache的时机很重要,不可以太频繁。
例子:
import ch.qos.logback.classic.util.EnvUtil;
import com.cainiao.arrow.arrowcommon.constant.CacheKeysPrefixCenter;
import com.cainiao.arrow.arrowcommon.util.CreateHashAlgorithm;
import com.cainiao.arrow.arrowcommon.util.ExeLogUtil;
import com.cainiao.arrow.arrowcommon.util.LocalCacheUtil;
import com.cainiao.arrow.arrowcommon.util.SpringContextUtil;
import com.sun.javaws.CacheUtil;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class GroovyScriptBaseService {
private static Map<String, String> keyToHashMap = new ConcurrentHashMap<>();
@Resource
private EnvUtil envUtil;
private static GroovyClassLoader getGroovyClassLoader() {
CompilerConfiguration config = new CompilerConfiguration();
config.setSourceEncoding("UTF-8");
// 设置该GroovyClassLoader的父ClassLoader为当前线程的加载器(默认)
return new GroovyClassLoader(Thread.currentThread().getContextClassLoader(), config);
}
private static String getCacheKeyCode(String textCode) {
return CacheKeysPrefixCenter.GROOVY_OBJECT_CACHE_PREFIX + CreateHashAlgorithm.toHash(textCode);
}
private static void invalidCache(String key, String textCode) {
String newCacheKeyCode = getCacheKeyCode(textCode);
if (keyToHashMap.containsKey(key)) {
String oldCacheKeyCode = keyToHashMap.get(key);
if (!newCacheKeyCode.equals(oldCacheKeyCode)) {
LocalCacheUtil.clear(oldCacheKeyCode);
LocalCacheUtil.clearExpired();
}
}
keyToHashMap.put(key, newCacheKeyCode);
}
/**
* 通过groovy脚本的text,执行的入口
*/
public Object executeByCode(String textCode, String methodName, Object[] objects) {
GroovyObject groovyObject = compiledCodeToObject(textCode);
// 反射调用方法得到返回值
return groovyObject.invokeMethod(methodName, objects);
}
/**
* 把groovy脚本的文本,转成GroovyObject
*/
private GroovyObject compiledCodeToObject(String textCode) {
//LocalCacheUtil来缓存一个groovy文本对应的Groovy的实例
return LocalCacheUtil.get(getCacheKeyCode(textCode), 60 * 60L, () -> {
try {
GroovyClassLoader groovyClassLoader = GroovyScriptBaseService.getGroovyClassLoader();
groovyClassLoader.clearCache();
// 替换包名,反正包冲突,在包名前面添加groovy.script.
String finalTextCode = textCode.replaceFirst("public\\s+class\\s+", "public class Groovy");
// 加载class类进入内存
Class<?> groovyClass = groovyClassLoader.parseClass(finalTextCode);
// 获得Groovy的实例
GroovyObject groovyTempObject = (GroovyObject)groovyClass.newInstance();
// 解决依赖注入问题
this.solveDependInjection(groovyTempObject);
return groovyTempObject;
} catch (Throwable e) {
throw new RuntimeException(e);
}
});
}
private void solveDependInjection(Object object) {
Class<?> tempClass = object.getClass();
List<Field> allFieldList = new ArrayList<>();
while (tempClass != null && !tempClass.equals(Object.class)) {
allFieldList.addAll(Arrays.asList(tempClass.getDeclaredFields()));
tempClass = tempClass.getSuperclass();
}
List<Field> dependFieldList = new ArrayList<>();
List<String> dependResourceNameList = new ArrayList<>();
for (Field field : allFieldList) {
Annotation[] annotations = field.getDeclaredAnnotations();
if (ArrayUtils.isEmpty(annotations)) {
continue;
}
String resourceName = null;
for (Annotation annotation : annotations) {
Class<?> annotationType = annotation.annotationType();
// 只检查Resource和Autowired两个注解,其他就不管了
if (annotationType.equals(Resource.class) || annotationType.equals(Autowired.class)) {
// 如果是Resource注解,还要判断是否有name属性
if (annotationType.equals(Resource.class)) {
Resource resource = (Resource)annotation;
// name不为空,那么保存对应的name值
if (StringUtils.isNotBlank(resource.name())) {
resourceName = resource.name();
}
}
dependFieldList.add(field);
dependResourceNameList.add(resourceName);
break;
}
}
}
List<String> fieldNameList = new ArrayList<>();
for (int i = 0; i < dependFieldList.size(); i++) {
Field field = dependFieldList.get(i);
String resourceName = dependResourceNameList.get(i);
fieldNameList.add(field.getName());
try {
field.setAccessible(true);
Object beanObject;
if (resourceName == null) {
beanObject = SpringContextUtil.getBean(field.getType());
} else {
beanObject = SpringContextUtil.getBean(resourceName);
}
if (beanObject == null) {
throw new RuntimeException(String.format("字段%s通过spring注入失败", field.getName()));
}
field.set(object, beanObject);
} catch (Throwable e) {
ExeLogUtil.executeAdd(String.format("solveDependInjection: fieldName: %s, error: %s", field.getName(), ""));
}
}
ExeLogUtil.executeAdd(String.format("solveDependInjection: fieldNameList: %s", ""));
}
}