2.后面可能还需要接入新的运营商
2.与 Java 兼容,语法甚至更加简单;
<dependency> <groupId>org.codehaus.groovy</groupId> <artifactId>groovy-all</artifactId> <version>3.0.17</version> <type>pom</type> </dependency>2、新建一个 Hello.groovy 文件,声明一个 say() 方法
class Hello { String say(String name) { return name + "World!" } }3、在 Java 类中用 GroovyClassLoader 加载 Hello.groovy 文件生成 Class ,然后生成实例,最后通过反射调用方法即可。
public class QuickStart { public static void main(String[] args) throws Exception { // 文件路径 String filePath = "src/main/java/com/zhang/awesome/groovy/Hello.groovy"; File groovyFile = new File(filePath); GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); // 加载class Class groovyClass = groovyClassLoader.parseClass(groovyFile); // 生成实例 GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance(); // 反射调用say方法 Object result = groovyObject.invokeMethod("say", "Hello"); System.out.println("return: " + result.toString()); } }是不是非常简单?只需要简单的3个步骤,就可以实现Groovy动态脚本能力,并且 Groovy 语法基本与 Java 是兼容的,所以写起来也是很方便。
public static void main(String[] args) { // 堆代码 duidaima.com final String script = "Runtime.getRuntime().availableProcessors()"; Binding intBinding = new Binding(); GroovyShell shell = new GroovyShell(intBinding); final Object eval = shell.evaluate(script); System.out.println(eval); }2、ScriptEngineManager
public static void main(String[] args) throws ScriptException, NoSuchMethodException { ScriptEngineManager factory = new ScriptEngineManager(); // 每次生成一个engine实例 ScriptEngine engine = factory.getEngineByName("groovy"); Bindings binding = engine.createBindings(); // 入参 binding.put("date", new Date()); // 如果script文本来自文件,请首先获取文件内容 engine.eval("def getTime(){return date.getTime();}", binding); engine.eval("def sayHello(name,age){return 'Hello,I am ' + name + ',age' + age;}"); // 反射到方法 Long time = (Long) ((Invocable) engine).invokeFunction("getTime", null); System.out.println(time); String message = (String) ((Invocable) engine).invokeFunction("sayHello", "zhangsan", 12); System.out.println(message); }3、GroovyClassLoader
public static void groovyClassLoader() throws InstantiationException, IllegalAccessException { GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); // 可以是纯Java代码 String helloScript = "package com.vivo.groovy.util" + "class Hello {" + "String say(String name) {" + "System.out.println(\"hello, \" + name)" + " return name;" + "}" + "}"; Class helloClass = groovyClassLoader.parseClass(helloScript); GroovyObject object = (GroovyObject) helloClass.newInstance(); // 控制台输出"hello, vivo" Object ret = object.invokeMethod("say", "vivo"); // 打印vivo System.out.println(ret.toString()); }Groovy 官方提供 GroovyClassLoader 类,支持从文件、URL或字符串中加载解析 Groovy Class,实例化对象,反射调用指定方法。GroovyShell、ScriptEngineManager 底层核心也是调用了 GroovyClassLoader ,并且还会存在性能问题。所以一般场景来说还是比较推荐使用 GroovyClassLoader
public interface IRewardRule { Integer getRewardCount(User user); }而获取用户抽奖次数的规则逻辑则是放在 Groovy 脚本中实现 RewardRule.groovy
class RewardRule implements IRewardRule { @Override Integer getRewardCount(User user) { if (user.getAge() <= 10) { return 5 }else if (user.getAge() <= 20 && user.getAge() > 10) { return 3 }else { return 1 } } }在业务调用的时候我们可以直接用 GroovyClassLoader 生成对应的类,调用 getRewardCount 方法
GroovyClassLoader classLoader = new GroovyClassLoader(); Class<?> groovyClazz = classLoader.parseClass(script); Object instance = groovyClazz.newInstance(); classLoader.clearCache(); IRewardRule rewardRule = clazz.cast(instance); rewardRule.getRewardCount(user)踩坑指南
public Class parseClass(String text) throws CompilationFailedException { return parseClass(text, "script" + System.currentTimeMillis() + Math.abs(text.hashCode()) + ".groovy"); } 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 方法,都会生成一个 Class 对象,而对象名是 script + System.currentTimeMillis()+Math.abs(text.hashCode() 组成,也就是说,即使是相同内容的脚本,都会被认为是新的代码,进行新的编译和加载。而你的业务逻辑不断重复执行就会一直生成新的类,最终导致 Metaspace 溢出。
protected void setClassCacheEntry(Class cls) { synchronized (classCache) { classCache.put(cls.getName(), cls); } }要解决这个性能问题,我们通常是对加载后的 Groovy 脚本进行缓存,避免重复编译加载,可以通过计算脚本的MD5值来生成键值对进行缓存。通过应用层自己维护一个cache,从而解决 Metaspace 内存溢出的问题。当然,这里还有一个小细节点,在初始化的时候加上同步锁,可以避免并发的问题。
private final static Map<String, Object> SCRIPT_CACHE = new ConcurrentHashMap<>(); public synchronized <T> T initialize(String cacheKey, String script, Class<T> clazz) { if (SCRIPT_CACHE.containsKey(cacheKey)) { return clazz.cast(SCRIPT_CACHE.get(cacheKey)); } GroovyClassLoader classLoader = new GroovyClassLoader(); try { Class<?> groovyClazz = classLoader.parseClass(script); if (clazz != null) { Object instance = groovyClazz.newInstance(); // 清除GroovyClassLoader的缓存 classLoader.clearCache(); // 应用缓存 SCRIPT_CACHE.put(cacheKey, instance); return clazz.cast(instance); } } catch (Exception e) { log.error("initialize exception", e); } return null; }小结