Groovy 1.6 的新特性

阅读数:6311 2009 年 5 月 12 日

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.'**'.a.@href.grep(~/.*\.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 上的文本与文本框中的内容绑定在一起了。

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

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

Swing console 改进

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

回到结果可视化这个主题上来,新加入的系统可以让我们定制结果的渲染方式。如果执行的脚本返回爵士音乐家的一个 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更容易理解,如下图所示:

显然 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 是 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


给 InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家加入到 InfoQ 中文站用户讨论组中与我们的编辑和其他读者朋友交流。

收藏

评论

微博

发表评论

注册/登录 InfoQ 发表评论