Groovy研究
知识背景
Groovy是用于Java虚拟机的一种敏捷的动态语言,它是一种成熟的面向对象编程语言,既可以用于面向对象编程,又可以用作纯粹的脚本语言。使用该种语言不必编写过多的代码,同时又具有闭包和动态语言中的其他特性。
Groovy是JVM的一个替代语言(替代是指可以用 Groovy 在Java平台上进行 Java 编程),使用方式基本与使用Java代码的方式相同,该语言特别适合与Spring的动态语言支持一起使用,设计时充分考虑了Java集成,这使Groovy与Java代码的互操作很容易。(注意:不是指Groovy替代java,而是指Groovy和java很好的结合编程)。
Groovy和Java的结合
Groovy与java集成的方式
重温下Groovy调用Java方式,包括使用GroovyClassLoader、GroovyShell和GroovyScriptEngine。
1. GroovyClassLoader
用Groovy的GroovyClassLoader,动态地加载一个脚本并执行它的行为。GroovyClassLoader是一个定制的类装载器,
负责解释加载Java类中用到的Groovy类。
GroovyClassLoader loader = new GroovyClassLoader();
Class groovyClass = loader.parseClass(new File(groovyFileName));
GroovyObject groovyObject = (GroovyObject) groovyClass.newInstance();
groovyObject.invokeMethod("run", "helloworld");
invokeMethod中,第一个参数指的是调用的方法名,第二个是指该方法的参数,没有则赋值null。
2. GroovyShell
GroovyShell允许在Java类中(甚至Groovy类)求任意Groovy表达式的值。您可使用Binding对象输入参数给表达式,
并最终通过GroovyShell返回Groovy表达式的计算结果。
GroovyShell shell = new GroovyShell();
Script groovyScript = shell.parse(new File(groovyFileName));
Object[] args = {};
groovyScript.invokeMethod("run", args);
invokeMethod中,第一个参数指的是调用的方法名,第二个是指该方法的参数,没有则赋值null。
3. GroovyScriptEngine
GroovyShell多用于推求对立的脚本或表达式,如果换成相互关联的多个脚本,使用GroovyScriptEngine会更好些。
GroovyScriptEngine从您指定的位置(文件系统,URL,数据库,等等)加载Groovy脚本,并且随着脚本变化而重新加载它们。如同GroovyShell一样,GroovyScriptEngine也允许您传入参数值,并能返回脚本的值。
Zuul对Groovy的使用
Netflix的Zuul使用了Groovy用来实现动态网关,Zuul的主要思想就是通过一组Fliter来实现网关功能。
启用groovy的filter代码如下,以下代码从Zuul 1.X中筛选,加上了一些中文注释。
备注:以下代码分散在不同的包路径下,本文摘取他们只是为了说明zuul中groovy的加载使用,故在一个代码段中展示。
private void initGroovyFilterManager() {
//设置过滤器Loader的编译器为GroovyCompiler
FilterLoader.getInstance().setCompiler(new GroovyCompiler());
//获取groovy文件的路径位置
String scriptRoot = System.getProperty("zuul.filter.root", "");
if (scriptRoot.length() > 0) scriptRoot = scriptRoot + File.separator;
try {
FilterFileManager.setFilenameFilter(new GroovyFileFilter());
//filter文件管理器开始工作
//5代表轮训路径间隔,后边两个参数都为轮训路径
FilterFileManager.init(5, scriptRoot + "route", scriptRoot + "post");
} catch (Exception e) {
throw new RuntimeException(e);
}
}
/**
* Initialized the GroovyFileManager.
*
* @param pollingIntervalSeconds the polling interval in Seconds
* @param directories Any number of paths to directories to be polled may be specified
* @throws IOException
* @throws IllegalAccessException
* @throws InstantiationException
*/
public static void init(int pollingIntervalSeconds, String... directories) throws Exception, IllegalAccessException, InstantiationException {
if (INSTANCE == null) INSTANCE = new FilterFileManager();
INSTANCE.aDirectories = directories;
INSTANCE.pollingIntervalSeconds = pollingIntervalSeconds;
INSTANCE.manageFiles();
INSTANCE.startPoller();
}
void manageFiles() throws Exception, IllegalAccessException, InstantiationException {
List<File> aFiles = getFiles();
processGroovyFiles(aFiles);
}
/**
* puts files into the FilterLoader. The FilterLoader will only addd new or changed filters
*
* @param aFiles a List<File>
* @throws IOException
* @throws InstantiationException
* @throws IllegalAccessException
*/
void processGroovyFiles(List<File> aFiles) throws Exception, InstantiationException, IllegalAccessException {
for (File file : aFiles) {
FilterLoader.getInstance().putFilter(file);
}
}
/**
* From a file this will read the ZuulFilter source code, compile it, and add it to the list of current filters
* a true response means that it was successful.
*
* @param file
* @return true if the filter in file successfully read, compiled, verified and added to Zuul
* @throws IllegalAccessException
* @throws InstantiationException
* @throws IOException
*/
public boolean putFilter(File file) throws Exception {
String sName = file.getAbsolutePath() + file.getName();
//首先判断文件是否被修改,通过保存的文件最后一次修改时间来进行比较
if (filterClassLastModified.get(sName) != null && (file.lastModified() != filterClassLastModified.get(sName))) {
LOG.debug("reloading filter " + sName);
filterRegistry.remove(sName);
}
ZuulFilter filter = filterRegistry.get(sName);
//判断当前的groovy对应的filter是否存在
if (filter == null) {
//真正的读取并且加载groovy到jvm中
Class clazz = COMPILER.compile(file);
if (!Modifier.isAbstract(clazz.getModifiers())) {
//类型强转为ZuulFilter
filter = (ZuulFilter) FILTER_FACTORY.newInstance(clazz);
List<ZuulFilter> list = hashFiltersByType.get(filter.filterType());
if (list != null) {
hashFiltersByType.remove(filter.filterType()); //rebuild this list
}
//保存一下此次加载的groovy的信息,以便跟下一次加载作比对(主要用来判断groovy是否修改)
filterRegistry.put(file.getAbsolutePath() + file.getName(), filter);
filterClassLastModified.put(sName, file.lastModified());
return true;
}
}
return false;
}
/**
* Compiles groovy class from a file
*
* @param file
* @return
* @throws java.io.IOException
*/
@Override
public Class compile(File file) throws IOException {
GroovyClassLoader loader = getGroovyClassLoader();
Class groovyClass = loader.parseClass(file);
return groovyClass;
}
/**
* Returns a new implementation of ZuulFilter as specified by the provided
* Class. The Class is instantiated using its nullary constructor.
*
* @param clazz the Class to instantiate
* @return A new instance of ZuulFilter
*/
@Override
public ZuulFilter newInstance(Class clazz) throws InstantiationException, IllegalAccessException {
return (ZuulFilter) clazz.newInstance();
}
/**
* @return a new GroovyClassLoader
*/
GroovyClassLoader getGroovyClassLoader() {
return new GroovyClassLoader();
}
void startPoller() {
poller = new Thread("GroovyFilterFileManagerPoller") {
public void run() {
while (bRunning) {
try {
sleep(pollingIntervalSeconds * 1000);
manageFiles();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
poller.setDaemon(true);
poller.start();
}
从以上代码分析,Zuul对Groovy文本的加载主要通过GroovyClassLoader,通过该实例对象的parseClass方法将groovy文件转换为Class(java可识别的Class),然后通过该class实例化一个对象且强制转换为ZuulFilter类型(groovy脚本都实现了ZuulFilter这个接口)。
Groovy性能测试
性能评估中,我们使用GroovyCalssLoader来进行groovy的装载。代码片段如下:
private static void loadGroovy(String filePath) throws CompilationFailedException, IOException, InstantiationException, IllegalAccessException {
File sourceFile = new File(filePath);
boolean modify = isModify(filePath, map);
if(modify) {
classLoader = new GroovyClassLoader();
groovyClass = classLoader.parseClass(new GroovyCodeSource(sourceFile));
groovy = (GroovyObject) groovyClass.newInstance();
map.put(sourceFile.getName(), sourceFile.lastModified());
}
}
主要思想采用了zuul对groovy解析的方法。
1. 递归性能比对
采用递归实现斐波那契数列,并计算斐波那契数列的第N项的值。
Groovy代码段:
long count(int m) {
if(m == 1 || m ==2) {
return 1
}
return count(m - 1) + count(m - 2)
}
Java代码段:
private static long count(int m) {
if(m == 1 || m ==2) {
return 1;
}
return count(m - 1) + count(m - 2);
}
测试结果:
递归斐波那契数列 | Groovy运行时间(ms) | Java运行时间(ms) |
---|---|---|
第一次测试(斐波那契数列第40项) | 1036 | 358 |
第二次测试(斐波那契数列第40项) | 1041 | 355 |
第三次测试(斐波那契数列第40项) | 1012 | 357 |
第四次测试(斐波那契数列第40项) | 1020 | 370 |
第五次测试(斐波那契数列第40项) | 1015 | 358 |
第六次测试(斐波那契数列第40项) | 1009 | 356 |
第七次测试(斐波那契数列第45项) | 11022 | 3484 |
第八次测试(斐波那契数列第45项) | 11196 | 3457 |
第九次测试(斐波那契数列第45项) | 11113 | 3445 |
第十次测试(斐波那契数列第45项) | 11147 | 3441 |
第十一次测试(斐波那契数列第45项) | 11123 | 3463 |
第十二次测试(斐波那契数列第45项) | 11103 | 3450 |
2. 字符串反转性能比对
字符串反转的算法代码Groovy和Java采用相同的逻辑实现,我们测试字符串反转50000次,Groovy和Java代码所消耗的时间。
Groovy代码段:
String reserve(String str) {
int len = str.length()
if(len <= 1) {
return str
}
int index = len / 2
String left = str.subString(0, index)
String right = str.subString(index)
return reserve(right) + reserve(left)
}
Java代码段:
private static String reserve(String str) {
int len = str.length();
if(len <= 1) {
return str;
}
int index = len / 2;
String left = str.subString(0, index);
String right = str.subString(index);
return reserve(right) + reserve(left);
}
测试结果:
字符串反转50000次 | Groovy运行时间(ms) | Java运行时间(ms) |
---|---|---|
第一次测试(字符串长度100位) | 1040 | 466 |
第二次测试(字符串长度100位) | 608 | 360 |
第三次测试(字符串长度100位) | 631 | 375 |
第四次测试(字符串长度100位) | 588 | 364 |
第五次测试(字符串长度100位) | 587 | 365 |
第六次测试(字符串长度100位) | 592 | 362 |
第七次测试(字符串长度200位) | 1758 | 767 |
第八次测试(字符串长度200位) | 1252 | 749 |
第九次测试(字符串长度200位) | 1317 | 737 |
第十次测试(字符串长度200位) | 1243 | 723 |
第十一次测试(字符串长度200位) | 1284 | 732 |
第十二次测试(字符串长度200位) | 1248 | 732 |
3. XML转换为JSON性能比对
XML转换为JSON,我们使用javaAPI中的XMLSerializer进行转换处理。
Groovy代码段:
String parseXML(String xmlPath) {
File file = new File(xmlPath)
BufferReader reader = new BufferReader(new InputStreamReader(new FileInputStream(file), "UTF-8"))
String str = null
StringBuffer sb = new StringBuffer()
while((str = reader.readLine()) != null) {
sb.append(str)
}
XMLSerializer xml = new XMLSerializer()
JSON json = xml.read(sb.toString())
reader.close()
return json
}
Java代码段:
public static String parseXML(String xmlPath) {
File file = new File(xmlPath);
BufferReader reader = new BufferReader(new InputStreamReader(new FileInputStream(file), "UTF-8"));
String str = null;
StringBuffer sb = new StringBuffer();
while((str = reader.readLine()) != null) {
sb.append(str);
}
XMLSerializer xml = new XMLSerializer();
JSON json = xml.read(sb.toString());
reader.close();
return json;
}
测试结果:
XML转换 | Groovy运行时间(ms) | Java运行时间(ms) |
---|---|---|
第一次测试(5次) | 893 | 360 |
第二次测试(5次) | 258 | 261 |
第三次测试(5次) | 188 | 143 |
第四次测试(5次) | 144 | 149 |
第五次测试(5次) | 193 | 149 |
第六次测试(5次) | 143 | 141 |
第七次测试(10次) | 1158 | 489 |
第八次测试(10次) | 508 | 323 |
第九次测试(10次) | 315 | 281 |
第十次测试(10次) | 276 | 326 |
第十一次测试(10次) | 276 | 283 |
第十二次测试(10次) | 267 | 278 |
结果分析
上述三组测试中,递归是属于纯数学计算,字符串反转是针对字符文本的操作,而XML解析则是读取XML文件之后进行解析。
递归计算中,Groovy代码的执行消耗时间大约均是java代码执行消耗时间的3倍。字符串反转中,Groovy代码执行消耗时间大约是java代码执行消耗时间的2倍。但是groovy代码第一次执行之后,之后的执行,可以明显看出消耗时间有所下降。Java代码的消耗时间也是有所下降,下降幅度不如groovy明显,groovy的执行消耗时间下降为java代码消耗时间的1.5倍左右。
XML解析中,Groovy代码初次运行时间大约是java代码初次运行时间的2倍。但是随着程序的多次执行,groovy和java代码运行时间都有所下降。且两者最后消耗的时间趋近与一致。
结果推论
Groovy语言本身在运算方面的处理效率比java语言要低,java大概是groovy处理效率的3倍-4倍之间,且多次执行,该消耗时间并未有所改观。
但是在文本方面的处理(文本+数学运算),groovy的除此执行消耗时间还是比较大,大概是java消耗时间的2-3倍之间,groovy的后续运行,消耗时间有所下降,但还是趋近与java消耗时间的2倍。
在文件处理方面,groovy和java的初次运行都占用比较大的时间,但groovy的消耗时间仍然是java的2倍左右。多次运行之后,groovy运行消耗时间有了大幅度提升,java代码略微提升,最后两者运行消耗时间趋近与相等。
结论:groovy单纯的数学运算(递归)与java之间有较大的差距,java效率可以约等为groovy的3倍。文本(字符串反转)和文件(XML转换为JSON)中,groovy的后续运行效率都会有所改观,这个我倾向于jvm本身对文本和文件的一个多次执行的优化,而不是groovy运算效率的提升。