最近项目在做一个度量平台,项目目标是整合大量数据,结合各种度量指标的算法,以图表等形式展现数据优劣趋势等。
至于前台的实现技术、架构等内容不在我们讨论范围内,直接忽略,后台系统架构则采用纯Java的后台,结合多线程、Quartz定时器等技术实现采集、计算,但只是实现了预定义指标、算法的计算(使用系统预定义算法,即程序固定写死的算法)。说这么多,大家应该发现了,问题就在这,大多比较强大的度量系统,肯定有一套自己独有的算法规则,可以使用定义好的规则自定义算法,而我们的系统则是一成不变的固定算法,即便说可以添加,也是改Java代码实现,带来的工作量可是不小,而且系统会越来越庞大,很难维护。
废话不说,下面就大概聊一下这里要出厂的主角——Groovy,Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy 代码能够与 Java 代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy 可以使用其他 Java 语言编写的库。看样子是很诱人,而且还可以直接使用而不必编译(这里的不用编译实质上是有点争议的,因为虽然Groovy脚本可以及时生效,但在其作为对象使用时还是使用Groovy本身提供的类库生成了JVM所认识的字节码,只不过我们看不到这个编译后的文件而已,当然,为了运行效率的提高,你依然可以将其编译成class文件,但前提是你写好的*.groovy文件放在编译目录,而且一旦编译,就不能实现我们的动态算法功能了,这里我们要讨论的就是动态算法的融入,故不再赘述)。
先说下动态算法的实现吧,打破陈规,我们先不管Java如何调用Groovy,先看下Groovy的优势,下面列出了我们常用的List、Map在Groovy中的使用
- List:
- 定义list:def list = []
- list = [1,2,3,4,5]
-
- list操作:
- def list = [1,2,3,4,5]
- list[1] //Result: 2
- list[-2] //Result: 4
- list[1..3] //Result: [2, 3, 4]
- list[1..<3] //Result: [2, 3]
- list + [6,7] //Result: [1, 2, 3, 4, 5, 6, 7]
- list - [4,5,6] //Result: [1, 2, 3]
- list < < 6 //Result: [1, 2, 3, 4, 5, 6]
- list << [6,7] //Result: [1, 2, 3, 4, 5, 6, [6, 7]]
-
- list方法:
- [2,5].add(7) //Result: true; list = [2, 5, 7]
- [2,5].add(1,9) //list = [2, 7, 5]
- [2,5].add([7,9]) //Result: [2, 5, [7, 9]]
- [2, 5, [7, 9]].flatten() //Result: [2, 5, 7, 9];克隆并解开下层list
- [2,5].get(1) //Result: 5
- [2,5].size() //Result: 2
- [2,5].isEmpty() //Result: false
- [2,5].getAt(1) //Result: 5
- [2,5,7].getAt(1..2) //Result: [5, 7]
- [2,5,7].getAt(-1) //Result: 7;get()不支持负数参数,getAt()支持
- [2,5,7].getAt([1,2]) //Result: [5, 7]
- [2,5,7].intersect([5,9,2]) //Result: [5, 2];交集
- [2,5,7].pop() //Result: 7
- [2,5,7].plus([3,6]) //Result: [2, 5, 7, 3, 6]
- [2,5,7,2].minus(2) //Result: [5, 7]
- [2,5,7].remove(1) //Result: 5; list = [2, 7]
- [2,7,5].reverse() //Result: [5, 7, 2]
- [2,7,5].sort() //Result: [2, 5, 7]
-
- Map:
- 定义Map:def map = [:]
- map = ['name':'Bruce', 'age':27]
-
- 键被解释成字符串:
- def x = 3
- def y = 5
- def map = [x:y, y:x] //Result: ["x":5, "y":3]
-
- 如果要把值作为键,像下面这样:
- def city = 'shanghai'
- map."${city}" = 'china'
- map.shanghai //Result: "china"
-
- map操作:
- def map = [3:56, 'name':'Bruce']
- def a = 'name'
- map.name //Result: "Bruce"
- map['name'] //Result: "Bruce"
- map[a] //Result: "Bruce"
- map[3] //Result: 56
- 以下访问是错误的,会抛出异常
- map[name]
- map.3
-
- map方法:
- def map = ['name':'Bruce', 'age':27]
- map.containsKey('name') //Result: true
- map.get('name') //Result: "Bruce"
- map.get('weight', '60kg') //Result: "60kg";会把key:value加进去
- map.getAt('age') //Result: 27
- map.keySet() //Result: [name, age, weight]
- map.put('height', '175') //Result: ["name":"Bruce", "age":27, "weight":"60kg", "height":"175"]
- map.values().asList() //Result: ["Bruce", 27, "60kg", "175"]
- map.size() //Result: 4
-
- 下列方法可以应用于范围、List和Map(inject和reverseEach方法只适合List和范围)
- each void each(Closure clos)迭代集合中每个元素。
- find List find(Closure clos)返回集合中第一个符合条件的元素。
- findAll List findAll(Closure clos)返回集合中所有符合条件的元素。
- collect List collect(Closure clos)返回计算后的列表。
- collect List collect(Collection col, Closure clos)返回计算后的列表,同时把返回值保存到col集合里。
- any boolean any(Closure clos)集合中有一个符合条件即返回true,否则返回false。
- every boolean every(Closure clos)集合中所有都符合条件即返回true,否则返回false。
- findIndexOf int findIndexOf(Closure clos)返回第一个符合条件元素在集合中的索引值(从0开始计算)。
- findLastIndexOf int findLastIndexOf(Closure clos)返回最后一个符合条件元素在集合中的索引值(从0开始计算)。
- inject Object inject(Object value, Closure clos)返回调用列表和参数的计算值。
- [1,2,3,4].inject(5) {x,y->
- x + y
- }
- //Result: 15
- reverseEach void reverseEach(Closure clos)反响迭代集合中每个元素。
- [1,2,3,4].reverseEach {x->
- print x + '-'
- }
- //4-3-2-1-
- sort List sort(Closure clos)按照闭包的返回条件排序。
可以看出,脚本语言该有的,我们Groovy基本都有实现,而且,我这边现有系统的计算参数,就是以List
如下,最简单的计算:
- def compute(list) {
- //TODO
- return list;
- }
我们可以不用定义返回值类型、不用定义传入值类型,他一样能工作,如果你不习惯,也可以像下面这样:
- def compute(def list) {
- return list;
- }
- def List compute(def list) {
- //TODO
- return list;
- }
- def List compute(List list) {
- //TODO
- return list;
- }
- public List compute(List list) {
- //TODO
- return list;
- }
- public List< map <String,Object> > compute(List< <map <String,Object> > list) {
- //TODO
- return list;
- }
很多朋友可能已经看出来了,没错,下面两个就是Java的写法,Groovy完全兼容,但是这里我们甚至可以把这个方法存在数据库,在计算之前拿出来直接使用,如果某一天计算方法变了,我们只用更新数据库字段值即可,是不是很方便呢?既然可以这样,那么我们原有的连接池什么的公共接口是不是也可以在Groovy脚本里面使用了?答案是肯定的,我们只需显式的引入相应包、相应类即可,不过要提的一点是:如果你想引入外部类库等,且希望在脚本内部使用全局变量,你需要在你的方法外层套上class X{},不然解释器会报错,如下情况是不被允许的:
- def name = "AVG";
-
- def compute(def list) {
- return println(format(list));
- }
需要改成:
- class Avg{
- def name = "AVG";
-
- def compute(def list) {
- //TODO
- return list;
- }
- }
同样的,可以写成标准Java类,如
- public class Avg{
- private String name = "AVG";
-
- public List< map <String,Object> > compute(List< map <String,Object> > list) {
- //TODO
- return list;
- }
- }
真正的整合环节到了,说了Groovy的好处,我们到底怎么样整合到Java中呢?Java和groovy混合使用的方法有几种呢?
实际上,我们有4种方式可以整合:
1、静态编译,在java工程中直接写groovy的文件,然后可以在Groovy的文件中引用Java工程的类,这种方式能够有效的利用groovy自身的语言特性,例如闭包; (这种方式上面已经提及,不适合我们目前需求)
2、通过groovyShell类直接执行脚本,例如:
- Binding bind = new Binding();
- bind.setVariable("str", "test");
- GroovyShell shell = new GroovyShell(bind);
- Object obj = shell.evaluate("return str");
- System.out.println(obj);
3、通过groovyScriptEngine执行文件或者脚本,例如:
- GroovyScriptEngine engine = new GroovyScriptEngine("groovy");
- Object obj = engine.run("test.groovy","test");
- System.out.println(obj);
4、通过GroovyClassLoader来执行,例如:
- String script="";//groovy script
- ClassLoader parent = ClassLoader.getSystemClassLoader();
- GroovyClassLoader loader = new GroovyClassLoader(parent);
- Class< ?> clazz = loader.parseClass(script);
- GroovyObject clazzObj = (GroovyObject)clazz.newInstance();
- System.out.println(clazzObj.invokeMethod("test", "str"));
需要注意的是,通过看groovy的创建类的地方,就能发现,每次执行的时候,都会新生成一个class文件,这样就会导致JVM的perm区持续增长,进而导致FullGCc问题,解决办法很简单,就是脚本文件变化了之后才去创建文件,之前从缓存中获取即可,缓存的实现可以采用简单的Map或者使用之前文章提到的EhCache(同时可以设置缓存有效期,降低服务器压力)。
在使用时,最好每次重新new classloader,因为如果脚本重新加载了,这时候就会有新老两个class文件,如果通过一个classloader持有的话,这样在GC扫描的时候,会认为老的类还在存活,导致回收不掉,所以每次new一个就能解决这个问题了。
注意CodeCache的设置大小(来自:http://hellojava.info/)
对于大量使用Groovy的应用,尤其是Groovy脚本还会经常更新的应用,由于这些Groovy脚本在执行了很多次后都会被JVM编译为native进行优化,会占据一些CodeCache空间,而如果这样的脚本很多的话,可能会导致CodeCache被用满,而CodeCache一旦被用满,JVM的Compiler就会被禁用,那性能下降的就不是一点点了。
Code Cache用满一方面是因为空间可能不够用,另一方面是Code Cache是不会回收的,所以会累积的越来越多(其实在不采用groovy这种动态更新/装载class的情况下的话,是不会太多的),所以解法一可以是增大code cache的size,可通过在启动参数上增加-XX:ReservedCodeCacheSize=256m(Oracle JVM Team那边也是推荐把code cache调大的),二是启用code cache的回收机制(关于Code Cache flushing的具体策略请参见此文),可通过在启动参数上增加:-XX:+UseCodeCacheFlushing来启用。