一、前言
因为之前在我的项目中应用了Groovy对业务能力进行一些扩大,成果比拟好,所以简略记录分享一下,这里你能够理解:
- 为什么选用Groovy作为脚本引擎
- 理解Groovy的基本原理和Java如何集成Groovy
- 在我的项目中应用脚本引擎时做的平安和性能优化
- 理论应用的一些倡议
二、为什么应用脚本语言
2.1 脚本语言可解决的问题
互联网时代随着业务的飞速发展,不仅产品迭代、更新的速度越来越快,个性化需要也是越来越多,如:多维度(条件)的查问、业务流转规定等。方法通常有如下几个方面:
- 最常见的形式是用代码枚举所有状况,即所有查问维度、所有可能的规定组合,依据运行时参数遍历查找;
- 应用开源计划,例如drools规定引擎,此类引擎实用于业务基于规定流转,且比较复杂的零碎;
- 应用动静脚本引擎,例如Groovy,JSR223。注:JSR即 Java标准申请,是指向JCP(Java Community Process)提出新增一个标准化技术规范的正式申请。任何人都能够提交JST,以向Java平台削减新的API和服务。JSR是Java界的一个重要规范。JSR223提供了一种从Java外部执行脚本编写语言的不便、规范的形式,并提供从脚本外部拜访Java资源和类的性能,即为各脚本引擎提供了对立的接口、对立的拜访模式。JSR223不仅内置反对Groovy、Javascript、Aviator,而且提供SPI扩大,笔者曾通过SPI扩大实现过Java脚本引擎,将Java代码“脚本化”运行。
引入动静脚本引擎对业务进行形象能够满足定制化需要,大大晋升我的项目效率。例如,笔者当初开发的内容平台零碎中,上游的内容需求方依据不同的策略会要求内容平台圈选指定内容推送到指定的解决零碎,这些解决零碎解决完后,内容平台接管到处理结果再依据散发策略(规定)下发给举荐零碎。每次圈选内容都要写一堆对于此次圈选的查问逻辑,内容下发的策略也常常须要变更。所以想利用脚本引擎的动静解析执行,应用规定脚本将查问条件以及下发策略形象进去,晋升效率。
2.2 技术选型
对于脚本语言来说,最常见的就是Groovy,JSR233也内置了Groovy。对于不同的脚本语言,选型时须要思考性能、稳定性、灵活性,综合思考后抉择Groovy,有如下几点起因:
- 学习曲线平缓,有丰盛的语法糖,对于Java开发者十分敌对;
- 技术成熟,功能强大,易于应用保护,性能稳固,被业界看好;
- 和Java兼容性强,能够无缝连接Java代码,能够调用Java所有的库。
2.3 业务革新
因为经营、产品同学对于内容的需要在一直的调整,内容平台圈选内容的能力须要可能反对各种查问维度的组合。内容平台起初开发了一个查问组合为(状态,入库工夫,起源方,内容类型),并定向散发到内容了解和打标的接口。然而这个接口曾经不能满足需要的变动,为此,最容易想到的设计就是枚举所有表字段(如公布工夫、作者名称等近20个),使其成为查问条件。然而这种设计的开发逻辑其实是很繁琐的,也容易造成慢查问;比方:筛选指定合作方和等级S的up主,且对没有内容了解记录的视频,调用内容了解接口,即对这部分视频进行内容了解。为了满足需要,须要从新开发,后果就是write once, run only once,造成开发和发版资源的节约。
不论是JDBC for Mysql,还是JDBC for MongoDB都是面向接口编程,即查问条件是被封装成接口的。基于面向接口的编程模式,查问条件Query接口的实现能够由脚本引擎动静生成,这样就能够满足任何查问场景。执行流程如下图3.1。
上面给出脚本的代码Demo:
/*** 构建查问对象Query* 分页查问mongodb*/public Query query(int page){ String source = "Groovy"; String articleType = 4; // (source,articleType) 组成联结索引,进步查问效率 Query query = Query.query(where("source").is(source)); // 查问条件1:source="Groovy" query.addCriteria(where("articleType").is(articleType)); // 查问条件2:articleType=4 Pageable pageable = new PageRequest(page, PAGESIZE); query.with(pageable);// 设置分页 query.fields().include("authorId"); // 查问后果返回authorId字段 query.fields().include("level"); // 查问后果返回level字段 return query;}
/*** 过滤每一页查问后果*/public boolean filter(UpAuthor upAuthor){ return !"S".equals(upAuthor.getLevel(); // 过滤掉 level != S 的作者}
/*** 对查问后果集逐条解决*/public void handle(UpAuthor upAuthor) { UpAthorService upAuthorService = SpringUtil.getBean("upAuthorService"); // 从Spring容器中获取执行java bean if(upAuthorService == null){ throw new RuntimeException("upAuthorService is null"); } AnalysePlatService analysePlatService = SpringUtil.getBean("analysePlatService"); // 从Spring容器中获取执行java bean if(analysePlatService == null){ throw new RuntimeException("analysePlatService is null"); } List<Article> articleList = upAuthorService.getArticles(upAuthor);// 获取作者名下所有视频 if(CollectionUtils.isEmpty(articleList)){ return; } articleList.forEach(article->{ if(article.getAnalysis() == null){ analysePlatService.analyse(article.getArticleId()); // 提交视频给内容了解解决 } })}
实践上,能够指定任意查问条件,编写任意业务逻辑,从而对于流程、规定常常变动的业务来说,解脱了开发和发版的时空解放,从而可能及时响应各方的业务变更需要。
三、Groovy与Java集成
3.1 Groovy基本原理
Groovy的语法很简洁,即便不想学习其语法,也能够在Groovy脚本中应用Java代码,兼容率高达90%,除了lambda、数组语法,其余Java语法根本都能兼容。这里对语法不多做介绍,有趣味能够自行浏览 https://www.w3cschool.cn/groovy 进行学习。
3.2 在Java我的项目中集成Groovy
3.2.1 ScriptEngineManager
依照JSR223,应用标准接口ScriptEngineManager调用。
ScriptEngineManager factory = new ScriptEngineManager();ScriptEngine engine = factory.getEngineByName("groovy");// 每次生成一个engine实例Bindings binding = engine.createBindings();binding.put("date", new Date()); // 入参engine.eval("def getTime(){return date.getTime();}", binding);// 如果script文本来自文件,请首先获取文件内容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.2.2 GroovyShell
Groovy官网提供GroovyShell,执行Groovy脚本片段,GroovyShell每一次执行时代码时会动静将代码编译成Java Class,而后生成Java对象在Java虚拟机上执行,所以如果应用GroovyShell会造成Class太多,性能较差。
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);
3.2.3 GroovyClassLoader
Groovy官网提供GroovyClassLoader类,反对从文件、url或字符串中加载解析Groovy Class,实例化对象,反射调用指定办法。
GroovyClassLoader groovyClassLoader = new GroovyClassLoader(); String helloScript = "package com.vivo.groovy.util" + // 能够是纯Java代码 "class Hello {" + "String say(String name) {" + "System.out.println(\"hello, \" + name)" + " return name;" "}" + "}";Class helloClass = groovyClassLoader.parseClass(helloScript);GroovyObject object = (GroovyObject) helloClass.newInstance();Object ret = object.invokeMethod("say", "vivo"); // 控制台输入"hello, vivo"System.out.println(ret.toString()); // 打印vivo
3.3 性能优化
当JVM中运行的Groovy脚本存在大量并发时,如果依照默认的策略,每次运行都会从新编译脚本,调用类加载器进行类加载。一直从新编译脚本会减少JVM内存中的CodeCache和Metaspace,引发内存泄露,最初导致Metaspace内存溢出;类加载过程中存在同步,多线程进行类加载会造成大量线程阻塞,那么效率问题就不言而喻了。
为了解决性能问题,最好的策略是对编译、加载后的Groovy脚本进行缓存,防止反复解决,能够通过计算脚本的MD5值来生成键值对进行缓存。上面咱们带着以上论断来探讨。
3.3.1 Class对象的数量
3.3.1.1 GroovyClassLoader加载脚本
下面提到的三种集成形式都是应用GroovyClassLoader显式地调用类加载办法parseClass,即编译、加载Groovy脚本,天然地脱离了Java驰名的ClassLoader双亲委派模型。
GroovyClassLoader次要负责运行时解决Groovy脚本,将其编译、加载为Class对象的工作。查看要害的GroovyClassLoader.parseClass办法,如下所示代码3.1.1.1(出自JDK源码)。
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; }}
零碎每执行一次脚本,都会生成一个脚本的Class对象,这个Class对象的名字由 "script" + System.currentTimeMillis()+Math.abs(text.hashCode()组成,即便是雷同的脚本,也会当做新的代码进行编译、加载,会导致Metaspace的收缩,随着零碎一直地执行Groovy脚本,最终导致Metaspace溢出。
持续往下跟踪代码,GroovyClassLoader编译Groovy脚本的工作次要集中在doParseClass办法中,如下所示代码3.1.1.2(出自JDK源码):
private Class doParseClass(GroovyCodeSource codeSource) { validate(codeSource); // 简略校验一些参数是否为null Class answer; CompilationUnit unit = createCompilationUnit(config, codeSource.getCodeSource()); SourceUnit su = null; if (codeSource.getFile() == null) { su = unit.addSource(codeSource.getName(), codeSource.getScriptText()); } else { su = unit.addSource(codeSource.getFile()); } ClassCollector collector = createCollector(unit, su); // 这里创立了GroovyClassLoader$InnerLoader unit.setClassgenCallback(collector); int goalPhase = Phases.CLASS_GENERATION; if (config != null && config.getTargetDirectory() != null) goalPhase = Phases.OUTPUT; unit.compile(goalPhase); // 编译Groovy源代码 answer = collector.generatedClass; // 查找源文件中的Main Class String mainClass = su.getAST().getMainClassName(); for (Object o : collector.getLoadedClasses()) { Class clazz = (Class) o; String clazzName = clazz.getName(); definePackage(clazzName); setClassCacheEntry(clazz); if (clazzName.equals(mainClass)) answer = clazz; } return answer; }
持续来看一下GroovyClassLoader的createCollector办法,如下所示代码3.1.1.3(出自JDK源码):
protected ClassCollector createCollector(CompilationUnit unit, SourceUnit su) { InnerLoader loader = AccessController.doPrivileged(new PrivilegedAction<InnerLoader>() { public InnerLoader run() { return new InnerLoader(GroovyClassLoader.this); // InnerLoader extends GroovyClassLoader } }); return new ClassCollector(loader, unit, su); } public static class ClassCollector extends CompilationUnit.ClassgenCallback { private final GroovyClassLoader cl; // ... protected ClassCollector(InnerLoader cl, CompilationUnit unit, SourceUnit su) { this.cl = cl; // ... } public GroovyClassLoader getDefiningClassLoader() { return cl; } protected Class createClass(byte[] code, ClassNode classNode) { GroovyClassLoader cl = getDefiningClassLoader(); // GroovyClassLoader$InnerLoader Class theClass = cl.defineClass(classNode.getName(), code, 0, code.length, unit.getAST().getCodeSource()); // 通过InnerLoader加载该类 this.loadedClasses.add(theClass); // ... return theClass; } // ... }
ClassCollector的作用,就是在编译的过程中,将编译进去的字节码,通过InnerLoader进行加载。另外,每次编译groovy源代码的时候,都会新建一个InnerLoader的实例。那有了 GroovyClassLoader ,为什么还须要InnerLoader呢?次要有两个起因:
加载同名的类
类加载器与类全名能力确立Class对象在JVM中的唯一性。因为一个ClassLoader对于同一个名字的类只能加载一次,如果都由GroovyClassLoader加载,那么当一个脚本里定义了com.vivo.internet.Clazz这个类之后,另外一个脚本再定义一个com.vivo.internet.Clazz类的话,GroovyClassLoader就无奈加载了。
回收Class对象
因为当一个Class对象的ClassLoader被回收之后,这个Class对象才可能被回收,如果由GroovyClassLoader加载所有的类,那么只有当GroovyClassLoader被回收了,所有这些Class对象才可能被回收,而如果用InnerLoader的话,因为编译完源代码之后,曾经没有对它的内部援用,它就能够被回收,由它加载的Class对象,才可能被回收。上面具体探讨Class对象的回收。
3.3.1.2 JVM回收Class对象
什么时候会触发Metaspace的垃圾回收?
- Metaspace在没有更多的内存空间的时候,比方加载新的类的时候;
- JVM外部又一个叫做\_capacity\_until_GC的变量,一旦Metaspace应用的空间超过这个变量的值,就会对Metaspace进行回收;
- FGC时会对Metaspace进行回收。
大家可能这里会有疑难:就算Class数量过多,只有Metaspace触发GC,那应该就不会溢出了。为什么下面会给出Metaspace溢出的论断呢?这里引出下一个问题:JVM回收Class对象的条件是什么?
- 该类所有的实例都曾经被GC,也就是JVM中不存在该Class的任何实例;
- 加载该类的ClassLoader曾经被GC;
- java.lang.Class对象没有在任何中央被援用。
条件1,GroovyClassLoader会把脚本编译成一个类,这个脚本类运行时用反射生成一个实例并调用它的入口函数执行(详见图3.1),这个动作个别只会被执行一次,在利用外面不会有其余中央援用该类或它生成的实例,该条件至多是能够通过标准编程来满足。条件2,下面曾经剖析过,InnerClassLoader用完后即可被回收,所以条件能够满足。条件3,因为脚本的Class对象始终被援用,条件无奈满足。
为了验证条件3是无奈满足的论断,持续查看GroovyClassLoader中的一段代码3.1.2.1(出自JDK源码):
/*** this cache contains the loaded classes or PARSING, if the class is currently parsed*/protected final Map<String, Class> classCache = new HashMap<String, Class>(); protected void setClassCacheEntry(Class cls) { synchronized (classCache) { // 同步块 classCache.put(cls.getName(), cls); }}
加载的Class对象,会缓存在GroovyClassLoader对象中,导致Class对象不可被回收。
3.3.2 高并发时线程阻塞
下面有两处同步代码块,详见代码3.1.1.1和代码3.1.2.1。当高并发加载Groovy脚本时,会造成大量线程阻塞,肯定会产生性能瓶颈。
3.3.3 解决方案
- 对于 parseClass 后生成的 Class 对象进行缓存,key 为 Groovy脚本的md5值,并且在配置端批改配置后可进行缓存刷新。这样做的益处有两点:(1)解决Metaspace爆满的问题;(2)因为不须要在运行时编译加载,所以能够放慢脚本执行的速度。
- GroovyClassLoader的应用用参考Tomcat的ClassLoader体系,无限个GroovyClassLoader实例常驻内存,减少解决的吞吐量。
- 脚本动态化:Groovy脚本外面尽量都用Java动态类型,能够缩小Groovy动静类型查看等,进步编译和加载Groovy脚本的效率。
四、平安
4.1 被动平安
4.1.1 编码平安
Groovy会主动引入java.util,java.lang包,不便用户调用,但同时也减少了零碎的危险。为了避免用户调用System.exit或Runtime等办法导致系统宕机,以及自定义的Groovy片段代码执行死循环或调用资源超时等问题,Groovy提供了SecureASTCustomizer平安管理者和SandboxTransformer沙盒环境。
final SecureASTCustomizer secure = new SecureASTCustomizer();// 创立SecureASTCustomizersecure.setClosuresAllowed(true);// 禁止应用闭包List<Integer> tokensBlacklist = new ArrayList<>();tokensBlacklist.add(Types.**KEYWORD_WHILE**);// 增加关键字黑名单 while和gototokensBlacklist.add(Types.**KEYWORD_GOTO**);secure.setTokensBlacklist(tokensBlacklist);secure.setIndirectImportCheckEnabled(true);// 设置间接导入查看List<String> list = new ArrayList<>();// 增加导入黑名单,用户不能导入JSONObjectlist.add("com.alibaba.fastjson.JSONObject");secure.setImportsBlacklist(list);List<Class<? extends Statement>> statementBlacklist = new ArrayList<>();// statement 黑名单,不能应用while循环块statementBlacklist.add(WhileStatement.class);secure.setStatementsBlacklist(statementBlacklist);final CompilerConfiguration config = new CompilerConfiguration();// 自定义CompilerConfiguration,设置ASTconfig.addCompilationCustomizers(secure);GroovyClassLoader groovyClassLoader = new GroovyClassLoader(this.getClass().getClassLoader(), config);
4.1.2 流程平安
通过标准流程,减少脚本执行的可信度。
4.2 被动平安
尽管SecureASTCustomizer能够对脚本做肯定水平的平安限度,也能够标准流程进一步强化,然而对于脚本的编写依然存在较大的平安危险,很容易造成cpu暴涨、疯狂占用磁盘空间等重大影响零碎运行的问题。所以须要一些被动平安伎俩,比方采纳线程池隔离,对脚本执行进行无效的实时监控、统计和封装,或者是手动强杀执行脚本的线程。
五、总结
Groovy是一种动静脚本语言,实用于业务变动多又快以及配置化的需要实现。Groovy极易上手,其本质也是运行在JVM的Java代码。Java程序员能够应用Groovy在进步开发效率,放慢响应需要变动,进步零碎稳定性等方面更进一步。
作者:vivo互联网服务器团队-Gao Xiang