最近项目在做一个度量平台,项目目标是整合大量数据,结合各种度量指标的算法,以图表等形式展现数据优劣趋势等。
至于前台的实现技术、架构等内容不在我们讨论范围内,直接忽略,后台系统架构则采用纯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
很多朋友可能已经看出来了,没错,下面两个就是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来启用。