Groovy 1.6的新特性

Groovy是个成功而又强大的动态语言,它运行于Java虚拟机之上,由虚拟机保证其与Java的无缝集成,Groovy的语法和API深深植根于Java,其动态性则来源于其他语言如Smalltalk、Python及Ruby。

很多开源项目都用到了Groovy,如Grails、Spring及JBoss Seam等等,同时不少商业产品及财富500强的关键应用中也出现了它的影子,以此增加脚本化能力以对应用提供良好的扩展机制,凭借Groovy,专家和开发者可以通过领域特定语言以良好的可读性和维护性表达业务概念。

Groovy项目经理及SpringSource的Groovy开发领导Guillaume Laforge将通过本文介绍新发布的Groovy 1.6带来的众多新特性。

Groovy 1.6概览

Groovy 1.6的主要亮点列举如下:

  • 编译时与运行时性能的巨大提升
  • 多路赋值
  • if/else与try/catch块中可选的返回语句
  • Java 5注解定义
  • AST转换和众多转换注解,比如@Singleton、@Lazy、@Immutable、@Delegate及助手
  • Grape模块和依赖系统及其@Grab转换
  • Swing builder的若干改进、这要归功于Swing / Griffon团队,同时还有Swing console的几处改进 
  • 集成了JMX builder
  • 各种元编程的改进,像是EMC DSL,针对POJO的基于实例的元类(per-instance metaclasses),以及运行时的掺元(mixin)
  • 内置JSR-223脚本引擎
  • 开箱即用的OSGi支持

所有这些改进和新特性都是为了同一个目标:帮助开发者提高生产率,变得更加敏捷。这是通过如下手段实现的:

  • 将更多的精力集中在手头的任务而不是泛泛的代码上
  • 利用现有的企业API而不是重复发明轮子
  • 改进语言的整体性能和品质
  • 允许开发者根据需要对语言进行定制进而得到自己的领域特定语言

除了这些重要的方面以外,Groovy并不仅仅是个语言,它还是一个完整的生态系统

对Groovy所生成的字节码信息的改进有助于更多代码覆盖工具的普及,如Cobertura及其Groovy支持,同时还为新工具的加入(如对Groovy进行静态代码分析的CodeNarc)铺平了道路。

Groovy语法的可扩展性及其元编程能力促使了一些高级测试工具的诞生,如行为驱动开发项目Easyb、Mock库GMock及测试与规范框架Spock。

Groovy的灵活性、丰富的表现力及脚本能力为持续集成实践与项目构建解决方案提供了高级的构建脚本和基础设施,如Gant和Graddle。

从工具的角度来看,Groovy也在不断的进步,比如说通过groovydoc Ant任务可以为Groovy/Java的混合项目生成适当的JavaDoc封面、文档以及能够连接Groovy和Java源文件的内链接。

与此同时,IDE的开发者们也在不断改进其对Groovy的支持,他们向用户提供了强大的武器,比如跨语言的代码重构、对动态语言用法的深入理解、代码完成等等,以此提高Groovy开发者的生产率。

既然已经对Groovy世界有了初步的了解,让我们看看Groovy 1.6带来的众多创新吧!

性能改进

相比于之前的版本,我们将大量精力放到了Groovy编译时和运行时的性能改进上。

新的编译器要比之前的快3到5倍。该改进也被移植进了1.5.x分支中,这样旧的分支和当前的稳定分支都可以从中受益。感谢类寻找缓存(class lookup caches),项目越大,编译的速度就越快。

然而最值得关注的变化莫过于Groovy的运行时性能改进了。我们使用了Great Language Shootout的几个基准来度量其改进。对于选择的基准,相比于旧的Groovy 1.5.x,新版Groovy的性能提升了150%到460%。显然微观的基准并不会直接反映出项目中源代码的性能提升水平,但项目的整体性能肯定会提升很多。

多路赋值

Groovy 1.6只增加了一种语法来同时定义多个变量并为其赋值:

def (a, b) = [1, 2]

assert a == 1
assert b == 2

返回经纬度坐标的方法或许更有实际意义。如果使用带有两个元素的列表来表示坐标,那么你可以通过如下手段轻松获取每个元素:

def geocode(String location) {
    // implementation returns [48.824068, 2.531733] for Paris, France
}

def (lat, long) = geocode("Paris, France")

assert lat == 48.824068
assert long == 2.531733

还可以同时定义变量的类型,如下所示:

def (int i, String s) = [1, 'Groovy']


assert i == 1
assert s == 'Groovy'

赋值时无需使用def关键字(前提是变量已经定义好了):

def firstname, lastname


(firstname, lastname) = "Guillaume Laforge".tokenize()


assert firstname == "Guillaume"
assert lastname == "Laforge"

如果等号右边列表中的元素个数超过了左边的变量个数,那么只有前面的元素会被赋给左边的变量(自动忽略掉超出的元素——译者注)。如果元素个数少于变量个数,那么多出的变量将被赋为null。

下面的代码展示了变量个数多于列表元素的情况,这里的c被赋为null

def elements = [1, 2]
def (a, b, c) = elements


assert a == 1
assert b == 2
assert c == null

下面的代码展示了变量个数少于列表元素的情况:

def elements = [1, 2, 3, 4]
def (a, b, c) = elements


assert a == 1
assert b == 2
assert c == 3

这里我们可以联想到在学校中学到的标准数字交互程序,通过多路赋值可以轻松实现这个功能:

// given those two variables
def a = 1, b = 2


// swap variables with a list
(a, b) = [b, a]


assert a == 2
assert b == 1

注解定义

事实上,“多路赋值是增加的唯一一个语法”这种说法并不完全正确。从Groovy 1.5开始就已经支持注解定义语法了,但我们并没有完全实现这个特性。好在现在已经实现了,它对Groovy所支持的所有Java 5特性进行了包装,比如静态导入、泛型、注解和枚举,这使得Groovy成为唯一一个支持Java 5所有特性的JVM动态语言,这对于与Java的无缝集成非常关键,对那些依赖于注解、泛型等Java 5特性的企业框架来说也很重要,比如JPA、EJB3、Spring及TestNG等等。

if/else与try/catch/finally块中可选的返回语句

如果if/else与try/catch/finally块是方法或语句块中最后的表达式,那么他们也可以返回值了。无需在这些块中显式使用return关键字,只要他们是代码块中最后的表达式就行。

尽管省略了return关键字,但下面的代码示例中的方法仍将返回1。

def method() {
    if (true) 1 else 0
}


assert method() == 1

对于try/catch/finally块来说,最后计算的表达式也是返回的值。如果try块中抛出了异常,那么catch块中的最后一个表达式就会返回。注意,finally块不会返回任何值。

def method(bool) {
    try {
        if (bool) throw new Exception("foo")
        1
    } catch(e) {
        2
    } finally {
        3
    }
}


assert method(false) == 1
assert method(true) == 2

AST转换

尽管你可能觉得通过扩展Groovy语法来实现新特性是不错的想法(就好像多路赋值一样),但大多数情况下我们不能仅通过增加新的关键字或是创建某些新的语法结构来表示新概念。然而借助于AST(Abstract Syntax Tree——抽象语法树)转换的想法,我们可以不用改变语法就能实现创新性的新想法。

在Groovy编译器编译Groovy脚本和类的过程中,源代码在内存中是以具体语法树的形式表现的,接下来被转换成抽象语法树。AST转换的目的在于让开发者可以介入到编译过程中,这样就可以在其转换成JVM可执行的字节码前对AST进行修改了。

AST转换为Groovy提供了改进的编译时元编程能力,这就在语言级别上提供了强大的灵活性而不会损失运行时性能。

有两种转换类型:全局转换与局部转换。

  • 编译器会在代码编译期间使用全局转换。加到编译器类路径中的JAR应该在META-INF/services/org.codehaus.groovy.transform.ASTTransformation处包含一个服务定位器文件,其中有一行给出了转换类的名字。转换类必须具有无参构造方法并实现org.codehaus.groovy.transform.ASTTransformation接口。它在编译期间会触及所有代码,因此请确保创建的转换器不会扫描所有的AST(因为这样做非常浪费时间)以保证编译的速度。
  • 局部转换是局部使用的,它是通过注解你想要转换的代码元素来实现的。为此,我们再次用到注解符号,这些注解应该实现org.codehaus.groovy.transform.ASTTransformation。编译器会发现注解并转换这些代码元素。

Groovy 1.6提供了几个局部转换注解,比如Groovy Swing Builder中用于数据绑定的@Bindable@Vetoable、Grape模块系统中用于增加脚本库依赖的@Grab,还有一些不需要改变任何语法就可以支持的常规语言特性,如@Singleton@Immutable@Delegate@Lazy@Newify@Category@Mixin@PackageScope。现在来看看这些转换吧(我们将在Swing增强一节中介绍@Bindable@Vetoable,在Grape一节中介绍@Grab)。

@Singleton

不管singleton是模式还是反模式,在某些情况下我们还是会使用到它的。过去我们需要创建一个私有的构造方法,然后为static属性或是初始化过的public static final属性创建一个getInstance()方法,在Java中是这样表示的:

public class T {
    public static final T instance = new T();
    private T() {}
}

在Groovy 1.6中只需使用@Singleton注解你的类就行了:

@Singleton class T {}

接下来只需使用T.instance就可以访问该单例了(直接的public字段访问)。

你还可以通过额外的注解参数实现延迟加载:

@Singleton(lazy = true) class T {}

等价于下面的Groovy类:

class T {
    private static volatile T instance
    private T() {}
    static T getInstance () {
        if (instance) {
            instance
        } else {
            synchronized(T) {
                if (instance) {
                    instance
                } else {
                    instance = new T ()
                }
            }
        }
    }
}

不管是不是延迟加载,只要使用T.instance(属性访问,T.getInstance()的简写)就可以访问实例了。

@Immutable

不变对象意指创建后无法改变的对象。人们经常需要这类对象,因为他们够简单并且可以在多线程环境下安全的共享。鉴于此,他们非常适合于功能性和并发性场景。创建此类对象的规则是众所周知的:

  • 无存取器(即可以修改内部状态的方法)
  • 类必须为final的
  • 属性必须为private和final的
  • 可变组件的保护性拷贝(defensive copying)
  • 如果想要比较对象或是将其作为Map等的键时需要根据属性来实现equals()hashCode()toString()

你无需根据上面这些原则编写长长的Java或Groovy类,Groovy可以按照下面的方式来定义一个不变的类:

@Immutable final class Coordinates {
    Double latitude, longitude
}


def c1 = new Coordinates(latitude: 48.824068, longitude: 2.531733)
def c2 = new Coordinates(48.824068, 2.531733)


assert c1 == c2

所有的样板式代码(boiler-plate code)都在编译期生成!你可以使用其所创建的两个构造方法来实例化不变的Coordinates对象,第一个构造方法接收一个Map,其键就是相应的属性,后跟其值;第二个构造方法的参数为属性值。assert表明equals()方法已被实现,我们可以正确的比较这些不变对象。

如果有兴趣可以看看该转换器的实现细节。上面使用了@Immutable的Groovy代码示例相当于50多行Java代码。

@Lazy

另一个转换就是@Lazy。有时你想延迟加载某些属性,即只在第一次使用时才会进行处理,这通常发生在处理时间长、内存消耗大的情况下。通常的解决办法是对这种字段的getter进行特殊的处理以在首次调用时完成初始化。但在Groovy 1.6中,我们可以使用@Lazy注解实现该目的:

class Person {
    @Lazy pets = ['Cat', 'Dog', 'Bird']
}


def p = new Person()
assert !(p.dump().contains('Cat'))

assert p.pets.size() == 3
assert p.dump().contains('Cat')

如果字段初始化需要复杂的计算,那么你需要调用某些方法而不是直接使用值(像下面的pets列表)来实现。接下来就可以通过闭包调用完成延迟赋值了,如下所示:

class Person {
    @Lazy List pets = { /* complex computation here */ }()
}

我们还可以对包含延迟字段的复杂数据结构使用适合垃圾收集器的软引用(Soft reference):

class Person {
    @Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird']
}


def p = new Person()
assert p.pets.contains('Cat')

编译器为pets所创建的内部字段实际上是个软引用,但访问p.pets会直接返回该引用所持有的值(也就是pets列表),这样软引用对于类的用户来说就是透明的了。

@Delegate

Java没有提供内置的代理机制,而到目前为止Groovy也没有提供。但借助于@Delegate,我们可以为类的属性加上注解使之成为接收方法调用的代理对象。在下面的示例中,Event类有个date代理,这样编译器就会将Event类上所有的Date方法调用代理给Date。如最后的assert所示,Event类拥有了before(Date)方法及Date的所有方法。

import java.text.SimpleDateFormat
class Event {
    @Delegate Date when
    String title, url
}


def df = new SimpleDateFormat("yyyy/MM/dd")


def gr8conf = new Event(title: "GR8 Conference",
                          url: "http://www.gr8conf.org",
                         when: df.parse("2009/05/18"))
def javaOne = new Event(title: "JavaOne",
                          url: "http://java.sun.com/javaone/",
                         when: df.parse("2009/06/02"))

assert gr8conf.before(javaOne.when)

Groovy编译器将Date的所有方法加到了Event类中,这些方法仅仅是将调用代理给Date对象。如果代理不是final类,我们甚至还可以通过继承Date使得Event类成为Date的子类,如下所示。要想实现代理,我们无需将Date的所有方法和属性手动加到Event类中,因为编译器会帮我们处理这些事情。

class Event extends Date {
    @Delegate Date when
    String title, url
}

假如要代理某个接口,那么你都无需显式实现该接口。@Delegate负责处理这些并实现该接口,这样,你的类的实例就自动成为代理的接口的实例了(instanceof )。

import java.util.concurrent.locks.*


class LockableList {
    @Delegate private List list = []
    @Delegate private Lock lock = new ReentrantLock()
}


def list = new LockableList()


list.lock()
try {
    list << 'Groovy'
    list << 'Grails'
    list << 'Griffon'
} finally {
    list.unlock()
}


assert list.size() == 3
assert list instanceof Lock
assert list instanceof List

在该示例中,LockableList包含了一个list和一个lock,这样它就instanceof ListLock了。如果不想让类实现这些接口,那么你可以通过指定注解参数来做到这一点:

@Delegate(interfaces = false) private List list = []

@Newify

@Newify提出了实例化类的两种新方式。第一种方式类似于Ruby,使用一个类方法new()来创建类:

@Newify rubyLikeNew() {
    assert Integer.new(42) == 42
}


rubyLikeNew()

第二种方式类似于Python,连关键字new都省略了。看看下面代码中Tree对象的创建过程:

class Tree {
    def elements
    Tree(Object... elements) { this.elements = elements as List }
}


class Leaf {
    def value
    Leaf(value) { this.value = value }
}


def buildTree() {
    new Tree(new Tree(new Leaf(1), new Leaf(2)), new Leaf(3))
}


buildTree()

Tree对象的创建过程可读性太差,因为一行代码中出现了好几个new关键字。Ruby的方式也好不到哪去,因为还是要通过new()方法来创建每个元素。但借助于@Newify,我们可以改进Tree对象的创建过程使之可读性更好:

@Newify([Tree, Leaf]) buildTree() {
    Tree(Tree(Leaf(1), Leaf(2)), Leaf(3))
}

你可能注意到了我们仅仅将@Newify加在了TreeLeaf上。默认情况下,注解范围内的所有实例都是newified的,但你可以通过指定类来限定该范围。Groovy builder可能更适合该示例,因为其目的就是构建层级/树形的结构。

再来看看之前的那个coordinates示例,通过@Immutable@Newify来创建一个path是多么的简洁,同时也是类型安全的:

@Immutable final class Coordinates {
    Double latitude, longitude
}


@Immutable final class Path {
    Coordinates[] coordinates
}


@Newify([Coordinates, Path])
def build() {
    Path(
        Coordinates(48.824068, 2.531733),
        Coordinates(48.857840, 2.347212),
        Coordinates(48.858429, 2.342622)
    )
}


assert build().coordinates.size() == 3

这里我想多说几句:鉴于生成的是Path(Coordinates[] coordinates),我们可以通过Groovy中的可变参数方式来使用该构造方法,就好象其定义形式是这样的:Path(Coordinates... coordinates)

@Category与@Mixin

如果使用Groovy有一段时间了,那么你肯定知道Category这个概念。它是继承现有类型(甚至可以继承JDK中的final类以及第三方类库)并增加方法的一种机制。该技术还可用来编写领域特定语言。先来看看下面这个示例:

final class Distance {
    def number
    String toString() { "${number}m" }
}


class NumberCategory {
    static Distance getMeters(Number self) {
        new Distance(number: self)
    }
}


use(NumberCategory) {
    def dist = 300.meters


    assert dist instanceof Distance
    assert dist.toString() == "300m"
}

这里我们假想了一个简单的Distance类,它可能来自于第三方,他们将该类声明为final以防止其他人继承该类。但借助于Groovy Category,我们可以用额外的方法装饰Distance类。这里我们通过装饰Number类型为number增加一个getMeters()方法。通过为number增加一个getter,你可以使用Groovy优雅的属性语法来引用number。这样就不必使用300.getMeters()了,只需使用300.meters即可。

这种Category系统和记号不利的一面是:要想给其它类型增加实例方法,我们需要创建static方法,而且该方法的第一个参数代表了我们要影响的类型。其他参数就是方法所使用的一般参数了。因此这与加到Distance中的一般方法相比不那么直观,我们是否应该访问源代码以对其增强。现在就是@Category注解发挥作用的时候了,它会将拥有(要增加的)实例方法的类转换为Groovy category

@Category(Number)
class NumberCategory {
    Distance getMeters() {
        new Distance(number: this)
    }
}

无需将方法声明为static的,同时这里所用的this实际上是category所操纵的number而不是我们所创建的category实例的this。然后可以继续使用use(Category) {}构造方法来应用category。这里需要注意的是这些category一次只能用在一种类型上,这与传统的category不同(可以应用到多种类型上)。

现在通过搭配@Category@Mixin,我们可以将多种行为混合到一个类中,类似于多继承:

@Category(Vehicle) class FlyingAbility {
    def fly() { "I'm the ${name} and I fly!" }
}


@Category(Vehicle) class DivingAbility {
    def dive() { "I'm the ${name} and I dive!" }
}


interface Vehicle {
    String getName()
}


@Mixin(DivingAbility)
class Submarine implements Vehicle {
    String getName() { "Yellow Submarine" }
}


@Mixin(FlyingAbility)
class Plane implements Vehicle {
    String getName() { "Concorde" }
}


@Mixin([DivingAbility, FlyingAbility])
class JamesBondVehicle implements Vehicle {
    String getName() { "James Bond's vehicle" }
}


assert new Plane().fly() ==
       "I'm the Concorde and I fly!"
assert new Submarine().dive() ==
       "I'm the Yellow Submarine and I dive!"


assert new JamesBondVehicle().fly() ==
       "I'm the James Bond's vehicle and I fly!"
assert new JamesBondVehicle().dive() ==
       "I'm the James Bond's vehicle and I dive!"

我们并没有继承多个接口并将相同的行为注入到每个子类中,相反我们将category混合到类中。示例中James Bond(007)的交通工具通过掺元获得了飞翔和潜水的能力。

值得注意的是,不像@Delegate可以将接口注入到声明代理的类中,@Mixin是在运行时实现掺元——在后面的元编程增强一节中将会深入探讨这一点。

@PackageScope

Groovy对属性的约定是这样的:没有可视化修饰符(visibility modifier)修饰的任何属性都会暴露给外界,Groovy会自动生成getter和setter。例如,下面的Person类会为private name属性生成getter getName()setter setName()这两个方法:

class Person {
    String name
}

这等价于下面的Java类:

public class Person {
    private String name;
    public String getName() { return name; }
    public void setName(name) { this.name = name; }
}

但这种方式有个弊端——无法为属性定义包范围的可视性,要想实现这一点,你可以用@PackageScope来注解属性。

Grape——适合Groovy的高级打包引擎

为了继续AST转换的讲解,我们需要了解一下Grape——在Groovy脚本中增加并使用依赖的一种机制。Groovy脚本可能需要某些库:这可以通过显式声明@Grab,或是通过Grape.grab()方法调用来实现,这样运行时就会找到所需的JAR文件。借助于Grape,我们可以轻松发布脚本而无需依赖,在首次使用脚本时再去下载这些依赖并缓存起来。在背后,Grape使用了Ivy和Maven仓库(包含了脚本所需的库)。

假如你想要获得Java 5文档所引用的所有PDF文档的链接,你可能想通过Groovy XmlParser来解析HTML页面,就好象页面是个兼容于XML的文档。可实际上HTML并不是兼容于XML的,因此你会使用兼容于SAX的 TagSoup将HTML转换为格式良好的XML。如果使用了Grape,在运行脚本时你甚至都无需配置类路径,只需通过Grape获取到TagSoup 库即可:

import org.ccil.cowan.tagsoup.Parser


// find the PDF links in the Java 1.5.0 documentation
@Grab(group='org.ccil.cowan.tagsoup', module='tagsoup', version='0.9.7')
def getHtml() {
    def tagsoupParser = new Parser()
    def parser = new XmlParser(tagsoupParser)
    parser.parse("http://java.sun.com/j2se/1.5.0/download-pdf.html")
}

html.body.'**'[email protected](~/.*\.pdf/).each{ println it }

再来看个示例:使用Jetty Servlet容器通过寥寥数行代码暴露Groovy模板:

import org.mortbay.jetty.Server
import org.mortbay.jetty.servlet.*
import groovy.servlet.*


@Grab(group = 'org.mortbay.jetty', module = 'jetty-embedded', version = '6.1.0')
def runServer(duration) {
    def server = new Server(8080)
    def context = new Context(server, "/", Context.SESSIONS);
    context.resourceBase = "."
    context.addServlet(TemplateServlet, "*.gsp")
    server.start()
    sleep duration
    server.stop()
}


runServer(10000)

Grape会在脚本首次运行时下载Jetty及其依赖并将其缓存起来。我们在8080端口上创建一个新的Jetty Server,接下来在上下文的根路径上暴露Groovy的TemplateServlet——Groovy拥有强大的模板引擎机制。启动服务器并运行一段时间,用户每次访问http://localhost:8080/somepage.gsp时服务器都会显示somepage.gsp模板——这些模板页面应该放在与服务器脚本相同的目录下。

除了注解以外,Grape还可以方法调用的方式使用。你还可以通过命令行使用grape命令来安装、展示及解析依赖。请参考其文档来了解关于Grape的更多信息。

Swing builder增强

现在让我们来看看对Swing开发者大有裨益的两个转换吧:@Bindable@Vetoable。在创建Swing UI时通常要检查某些UI元素值的变化,为了做到这一点,常规的手段就是在类的属性值发生变化时通知JavaBean PropertyChangeListener。下面展示了该样板式的Java代码:

import java.beans.PropertyChangeSupport;
import java.beans.PropertyChangeListener;


public class MyBean {
    private String prop;


    PropertyChangeSupport pcs = new PropertyChangeSupport(this);


    public void addPropertyChangeListener(PropertyChangeListener l) {
        pcs.add(l);
    }


    public void removePropertyChangeListener(PropertyChangeListener l) {
        pcs.remove(l);
    }


    public String getProp() {
        return prop;
    }


    public void setProp(String prop) {
        pcs.firePropertyChanged("prop", this.prop, this.prop = prop);
    }

}

还好通过Groovy@Bindable注解可以极大的简化上面的代码:

class MyBean {
    @Bindable String prop
}

再利用上Groovy Swing builder新的bind()方法,定义一个文本输入域并将其绑定到数据模型的属性上:

textField text: bind(source: myBeanInstance, sourceProperty: 'prop')

甚至还可以这样写:

textField text: bind { myBeanInstance.prop }

绑定还可以处理闭包中的简单表达式,如下所示:

bean location: bind { pos.x + ', ' + pos.y }

你或许还有兴趣看看ObservableMap与ObservableList,他们在Map和List上增加了的类似机制。

除了@Bindable以外,还有一个@Vetoable,用于阻止属性的改变。下面来看看Trompetist类,其中的name不允许包含字母‘z’:

import java.beans.*
import groovy.beans.Vetoable


class Trumpetist {
    @Vetoable String name
}


def me = new Trumpetist()
me.vetoableChange = { PropertyChangeEvent pce ->
    if (pce.newValue.contains('z'))
        throw new PropertyVetoException("The letter 'z' is not allowed in a name", pce)
}


me.name = "Louis Armstrong"


try {
    me.name = "Dizzy Gillespie"
    assert false: "You should not be able to set a name with letter 'z' in it."
} catch (PropertyVetoException pve) {
    assert true
}

下面来看个完整的使用了绑定的Swing builder示例:

import groovy.swing.SwingBuilder
import groovy.beans.Bindable
import static javax.swing.JFrame.EXIT_ON_CLOSE


class TextModel {
    @Bindable String text
}


def textModel = new TextModel()


SwingBuilder.build {
    frame( title: 'Binding Example (Groovy)', size: [240,100], show: true,
          locationRelativeTo: null, defaultCloseOperation: EXIT_ON_CLOSE ) {
        gridLayout cols: 1, rows: 2
        textField id: 'textField'
        bean textModel, text: bind{ textField.text }
        label text: bind{ textModel.text }
    }
}

下图展示了该脚本运行后的界面,Frame中有个文本框,下面是个label,label上的文本与文本框中的内容绑定在一起了。

Groovy 1.6的新特性_第1张图片

在过去几年中,SwingBuilder得到了长足的发展,因此Groovy Swing团队决定基于SwingBuilder和Grails创建一个新项目:Griffon。Griffon意在引入Grails的约定优于配置策略,同时还有其项目结构、插件系统、gant脚本功能等等。

如果你在开发Swing富客户端,请一定要看看Griffon。

Swing console改进

除了UI以外,Swing console也发生了很多变化:

  • Console可作为Applet运行(groovy.ui.ConsoleApplet)。
  • 除了语法高亮以外,编辑器还支持代码缩进。
  • 在编辑区拖放Groovy脚本会打开相应的文件。
  • 可以修改console脚本运行的classpath,这是通过向classpath中增加新的JAR和目录实现的,如下图所示
  • View菜单中增加了两个选项:一个用来显示输出域中的脚本,另一个用来以可视化方式显示执行结果。
  • 如果脚本抛出了异常,那么你可以点击与脚本相关的堆栈信息行,这样就可以轻松转到错误发生处。
  • 如果脚本有编译错误,错误消息也是可以点击的。

回到结果可视化这个主题上来,新加入的系统可以让我们定制结果的渲染方式。如果执行的脚本返回爵士音乐家的一个Map,那么结果可能如下所示:

这里展现的是Map的常规文本显示样式。那如何定制可视化结果的显示样式呢?Swing console可以助我们一臂之力。首先要确保勾上了菜单上的可视化选项:View -> Visualize Script ResultsPreference API会存储并记得Groovy Console所有设定的信息。有几个内置的可视化结果:如果脚本返回java.awt.Imagejavax.swing.Icon或是java.awt.Component(没有父亲),那么对象就不会用toString()的方式显示,否则仍然会用文本的方式显示。现在请在~/.groovy/OutputTransforms.groovy中编写如下Groovy脚本:

import javax.swing.*

transforms << { result ->
    if (result instanceof Map) {
        def table = new JTable(
            result.collect{ k, v -<
                [k, v?.inspect()] as Object[]
            } as Object[][],
            ['Key', 'Value'] as Object[])
        table.preferredViewportSize = table.preferredSize
        return new JScrollPane(table)
    }
}

Groovy Swing console会在启动时执行该脚本,将transforms列表注入到脚本绑定中,这样就可以增加自己的脚本结果表示了。该示例将Map转换为好看的Swing JTable。现在所显示的Map更容易理解,如下图所示: Groovy 1.6的新特性_第2张图片

显然Swing console并不是一个功能完全的IDE,它只用来处理日常的脚本任务,是你工具箱中不可或缺的组成部分。

增强的元编程

之所以称Groovy为动态语言的原因在于其元对象协议与元类的概念,他们代表了类和实例的运行期行为。在Groovy 1.6中,我们继续对动态的运行期系统进行改进,增加了几个新功能。

针对POJO的基于实例的元类

到目前为止,Groovy POGOs(Plain Old Groovy Objects)拥有基于实例的元类,但POJOs的所有实例只对应于一个元类(也就是基于类的元类)。这种情况已经一去不复返了,现在POJOs也拥有基于实例的元类了,而且将元类属性设为null会恢复默认的元类。

ExpandoMetaClass DSL

最初ExpandoMetaClass是在Grails下开发的,后来集成到了Groovy 1.5中,凭借ExpandoMetaClass,我们可以轻松改变对象和类的运行期行为而无需每次都完整的写一遍MetaClass。每次增加或是修改现有类型的属性和方法时都要做大量重复写Type.metaClass.xxx。看看下面这个示例,它来自于Unit manipulation DSL,用于处理操作符重载:

Number.metaClass.multiply = { Amount amount -> amount.times(delegate) }
Number.metaClass.div =      { Amount amount -> amount.inverse().times(delegate) }


Amount.metaClass.div =      { Number factor -> delegate.divide(factor) }
Amount.metaClass.div =      { Amount factor -> delegate.divide(factor) }
Amount.metaClass.multiply = { Number factor -> delegate.times(factor) }
Amount.metaClass.power =    { Number factor -> delegate.pow(factor) }
Amount.metaClass.negative = { -> delegate.opposite() }

显然这里有大量重复。但借助于ExpandoMetaClass DSL,我们可以通过重新分组每个类型的操作符来精简代码:

Number.metaClass {
    multiply { Amount amount -> amount.times(delegate) }
    div      { Amount amount -> amount.inverse().times(delegate) }
}


Amount.metaClass {
    div <<   { Number factor -> delegate.divide(factor) }
    div <<   { Amount factor -> delegate.divide(factor) }
    multiply { Number factor -> delegate.times(factor) }
    power    { Number factor -> delegate.pow(factor) }
    negative { -> delegate.opposite() }
}

metaClass()方法的唯一参数是个闭包,里面包含了各种方法和属性定义,它并没有在每一行重复Type.metaClass。如果没有同名方法就使用methodName { /* closure */ }模式,如果有就需要附加操作符并遵循methodName << { /* closure */ }模式。通过这种机制还可以增加static方法,以前典型的写法是这样的:

// add a fqn() method to Class to get the fully
// qualified name of the class (ie. simply Class#getName)
Class.metaClass.static.fqn = { delegate.name }


assert String.fqn() == "java.lang.String"

现在可以这样写:

Class.metaClass {
    'static' {
        fqn { delegate.name }
    }
}

注意,你必须将static关键字用引号引起来,否则该构造方法看起来就像静态初始化一样。如果只增加一个方法,以前的那种方式更简洁,但要想增加多个方法,EMC DSL更适合。

通过ExpandoMetaClass将属性加到现有类中的常见手段是添加gettersetter方法。比如,如果想要添加一个方法来计算某个文本文件中的单词数,你可能会这样做:

File.metaClass.getWordCount = {
    delegate.text.split(/\w/).size()
}


new File('myFile.txt').wordCount

在getter中有些逻辑,当然这是最好的方式了,但如果仅仅想增加几个新属性来保存简单的值该怎么做呢?借助于ExpandoMetaClass,这简直是小菜一碟。在下面的示例中,我们将lastAccessed属性加到Car类中,这样每个实例都会拥有该属性。如果调用了car的某个方法,我们就会用新的时间戳更新该属性。

class Car {
    void turnOn() {}
    void drive() {}
    void turnOff() {}
}


Car.metaClass {
    lastAccessed = null
    invokeMethod = { String name, args ->
        def metaMethod = delegate.metaClass.getMetaMethod(name, args)
        if (metaMethod) {
            delegate.lastAccessed = new Date()
            metaMethod.doMethodInvoke(delegate, args)
        } else {
            throw new MissingMethodException(name, delegate.class, args)
        }
    }
}



def car = new Car()
println "Last accessed: ${car.lastAccessed ?: 'Never'}"


car.turnOn()
println "Last accessed: ${car.lastAccessed ?: 'Never'}"


car.drive()
sleep 1000
println "Last accessed: ${car.lastAccessed ?: 'Never'}"


sleep 1000
car.turnOff()
println "Last accessed: ${car.lastAccessed ?: 'Never'}"

在该示例中,我们通过闭包的delegate访问属性,即delegate.lastAccessed = new Date(),通过invokeMethod()拦截所有的方法调用,invokeMethod()会将调用代理给最初的方法,如果方法不存在就抛出异常。接下来,你会看到只要实例上的方法被调用就会更新lastAccessed

运行时掺元

现在来介绍今天的最后一个元编程特性:运行时掺元。凭借@Mixin,你可以将新的行为混合到你所拥有的类中。但是你不能给无法拥有的类混入任何东西。运行时掺元旨在运行时向任意类型中添加掺元来解决这个问题。回想一下之前混入了能力的vehicle示例,如果我们无法拥有James Bond的vehicle却想为其增加潜水功能,可以这么做:

// provided by a third-party
interface Vehicle {
    String getName()
}


// provided by a third-party
class JamesBondVehicle implements Vehicle {
    String getName() { "James Bond's vehicle" }
}


JamesBondVehicle.mixin DivingAbility, FlyingAbility


assert new JamesBondVehicle().fly() ==
       "I'm the James Bond's vehicle and I fly!"
assert new JamesBondVehicle().dive() ==
       "I'm the James Bond's vehicle and I dive!"

我们可以将一个或多个掺元以参数的形式传到静态的mixin()方法中(该方法由GroovyClass上添加)来实现。

JSR-223 Groovy脚本引擎

在Groovy 1.6之前,如果想通过JSR-223 / javax.script.*将Groovy集成到Java项目中,我们需要从java.net下载Groovy脚本引擎实现并将该JAR放到类路径中。开发者们并不太喜欢这繁琐的步骤,但也没办法,因为Groovy发布包中并没有该JAR。还好1.6版带有一个javax.script.* API的实现。

下面的示例用来计算Groovy表达式(代码是用Groovy编写的,但可以轻松转换为Java代码):

import javax.script.*


def manager = new ScriptEngineManager()
def engine = manager.getEngineByName("groovy")


assert engine.evaluate("2 + 3") == 5

请注意只有Java 6才有javax.script.* API。

JMX Builder

最初JMX Builder是个位于Google Code上的外部开源项目,现在集成到了Groovy 1.6中以简化与JMX服务的交互及暴露服务。JMX Builder的特性列举如下:

  • 使用建造者模式(Builder pattern)的针对JMX API的领域特定语言
  • 简化JMX API编程
  • 以声明的方式将Java/Groovy对象暴露为JMX受管理的MBeans
  • 支持嵌入类(class-embedded)及显式描述符(explicit descriptors)
  • 对JMX事件模型的内在支持
  • 无缝创建JMX事件广播
  • 将事件监听器放到内联闭包中
  • 使用Groovy的动态特性轻松响应JMX事件通知
  • 为MBean提供灵活的注册策略
  • 没有特别的接口或类路径限制
  • 让开发者摆脱JMX API的复杂性
  • 暴露属性、构造方法、操作、参数及通知
  • 简化连接器(connector )服务器和客户端的创建
  • 支持JMX定时器的导出

在这里可以找到关于JMX Builder的更多信息及其对JMX系统的广泛支持,其中的众多示例展示了如何创建JMX连接器服务器及客户端、如何轻松的将POGOs导出为JMX受管理的Beans,如何监听JMX事件等等。

改进的OSGi支持

Groovy的jar文件还带有OSGi元数据,这样他们就可以bundle的形式加到任何兼容OSGi的容器中,比如Eclipse Equinox及Apache Felix。可以从Groovy项目站点上找到关于使用Groovy和OSGi的更多信息。上面的指南介绍了如何:

  • 以OSGi服务的形式加载Groovy
  • 编写Groovy OSGi服务
  • 在bundle中引入Groovy JAR  
  • 发布Groovy编写的服务
  • 使用Groovy消费服务
  • 解决学习过程中所遇到的各种问题

你可能还会对如何在应用中使用不同版本的Groovy感兴趣,多亏OSGi了。

小结

Groovy继续朝着减轻开发者负担的目标大踏步前进着,在新版本中提供了各种新特性和改进:AST转换极大降低了表达某些关系和模式所需的代码量,同时将语言开放给开发者以供进一步扩展,几个元编程增强简化了代码,使我们可以编写表达力更强的业务规则,同时还支持常见的企业级API,如Java 6的scripting APIs、JMX管理系统及OSGi编程模型。所有这些成就都没有牺牲与Java的无缝集成特性而且性能要比之前的版本上了一个新台阶。

至此为止本文也行将结束,如果还没有使用过Groovy,我希望这篇文章会让你明白Groovy能为你的项目带来什么,如果已经使用过Groovy,那么你将了解到该语言的所有新特性。接下来就去下载Groovy 1.6吧。如果想深入了解Groovy、Grails及Griffon,我建议你参加我们的GR8会议,该会议将在丹麦首都哥本哈根举办,主要讨论Groovy、Grails及Griffon,届时这些技术方面的专家和创建者将通过现场的展示和手把手的实验给予你指导。

关于作者

Guillaume Laforge, Groovy Project ManagerGuillaume Laforge是SpringSource下Groovy部门的领军人物,也是官方的Groovy项目经理,同时还是JSR-241规范的领导者,该规范主要用来标准化Groovy动态语言。他是 JavaOne、SpringOne、QCon、Sun TechDays及JavaPolis/Devoxx上的常客,经常做Groovy和Grails方面的演讲。Guillaume还与Dierk König合著了《Groovy in Action》。在创建G2One(Groovy/Grails背后的公司,去年底被SpringSource收购了)并担任技术副总裁一职之前,Guillaume就职于OCTO Technology,从事架构和敏捷方法论方面的顾问工作。在OCTO之时,Guillaume围绕Groovy与Grails为其客户进行开发。

查看原文:What's New in Groovy 1.6。

你可能感兴趣的:(Groovy 1.6的新特性)