Groovy是一种非常成功且功能强大的Java虚拟机动态语言,它提供了与Java的无缝集成 ,并且其根源牢固地植根于Java本身中,以提供语法和API以及其他语言(例如Smalltalk,Python或Ruby)以实现其动态功能。
Groovy用于许多开放源代码项目(例如Grails , Spring ,JBoss Seam等)中,并且由于其脚本功能提供了对这些应用程序的不错的扩展机制,或者其功能也集成在商业产品和《财富》 500强关键任务应用程序中让主题专家和开发人员编写嵌入式领域特定语言,以可读和可维护的方式表达业务概念 。
在本文中,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 / Griffon团队,以及对Swing控制台的一些改进,对Swing构建器进行了各种改进
- 集成JMX Builder
- 各种元编程改进 ,例如EMC DSL,针对POJO的按实例元类,以及运行时混合
- 内置JSR-223脚本引擎
- 开箱即用的OSGi准备
所有这些改进和新功能实现了一个目标:通过以下方式帮助开发人员提高生产力和敏捷性 :
- 将重点更多地放在手头的任务上,而不是样板技术规范
- 利用现有的企业API,而不是浪费时间
- 改善语言的整体表现和质量
- 使开发人员能够随意自定义语言以派生自己的特定领域语言
但是,除了所有这些重要方面之外, Groovy不仅是一种语言,而且是一个完整的生态系统 。
Groovy生成的字节码信息的改进有助于像Cobertura及其Groovy支持这样的功能强大的工具覆盖范围,或为诸如CodeNarc之类的新实用程序铺平道路, 从而对Groovy进行静态代码分析。
语言语法的可延展性及其元编程功能催生了高级测试工具,例如Easyb行为驱动开发项目, 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分支中,因此旧的维护分支和当前的稳定分支都将从这项工作中受益。 多亏了类查找缓存,项目越大,编译速度就越快。
但是,最明显的变化将是Groovy在常规运行时性能方面的改进。 我们使用了《 大语言枪战》中的多个基准来衡量我们的进步。 与旧的Groovy 1.5.x产品线相比,在我们选择的产品上, 性能提高了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"
如果右侧列表包含的元素多于左侧变量的数目,则仅前几个元素将按顺序分配到变量中。 同样,当元素少于变量时,多余的变量将被分配为空。
因此,对于变量多于列表元素的情况,此处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 variablesdef a = 1, b = 2
// swap variables with a list
(a, b) = [b, a]
assert a == 2
assert b == 1
注释定义
实际上,当我说多个分配是唯一的语法添加时,这并不是完全正确的。 Groovy即使在Groovy 1.5中也支持注释定义的语法,但是我们尚未完全实现该功能。 幸运的是,现在它已修复,并且包装了Groovy支持的所有Java 5功能,例如静态导入 , 泛型 , 注释和枚举 ,使Groovy成为支持所有Java 5功能的JVM的唯一替代动态语言 ,对于无缝的Java集成故事以及对于依赖注释,泛型等等(例如JPA,EJB3,Spring,TestNG等)的Enterprise框架的用法至关重要。
if / else和try / catch / finally块的可选返回
现在, if/else
和try/catch/finally
块是方法或闭包中的最后一个表达式,则可以返回它们的值。 只要它们是代码块中的最新表达式,就无需在这些结构内显式使用return
关键字。
例如,以下方法将返回1
,尽管省略了return
关键字。
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(抽象语法树)转换的思想,我们就能够解决新的创新思想,而无需进行必要的语法更改。
当Groovy编译器编译Groovy脚本和类时,在过程中的某个时刻,源代码最终将以具体语法树的形式在内存中表示,然后转换为抽象语法树。 AST Transformations的目的是使开发人员能够进入编译过程,以便能够在将AST转换为将由JVM运行的字节码之前对其进行修改。
AST Transformations为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
)。 让我们来看看一些这些转变( @Bindable
和@Vetoable
将在相关的Swing增强的部分被覆盖,并且@Grab
约葡萄节)。
@辛格尔顿
无论单例是模式还是反模式,在某些情况下,我们仍然需要创建单例。 我们习惯于创建一个私有的构造函数,一个静态字段的getInstance()
方法,甚至是一个初始化的public static final
字段。 因此,与其在Java中编写这样的代码:
public class T {
public static final T instance = new T();
private T() {}
}
您只需要使用@Singleton
批注来批注您的类型:
@Singletonclass T {}
然后可以使用T.instance
(直接公共字段访问)简单地访问单例实例。
您还可以使用带有附加注释参数的延迟加载方法:
@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()
快捷方式)。
@不可变
不可变的对象是最初创建后不会更改的对象。 此类对象经常是理想的,因为它们很简单并且即使在多线程上下文中也可以安全地共享。 这使它们非常适合功能和并发方案。 创建此类对象的规则是众所周知的:
- 没有变种器(修改内部状态的方法)
- 上课必须是最后的
- 字段必须是私有的并且是最终的
- 防御性复制可变组件
- 如果要比较对象或将它们用作键(例如,地图
toString()
必须根据字段实现equals()
,hashCode()
和toString()
Groovy无需编写一个模仿这种不可变性行为的很长的Java或Groovy类,而是让您只需编写一个不可变类,如下所示:
@Immutablefinal 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
所有样板代码都是在编译时为您生成的! 该示例显示,要实例化此类不可变的坐标,可以使用通过转换创建的两个构造函数之一,一个采用一个映射,其键是将属性设置为与那些键相关联的值的映射,另一个使用该键的值。属性作为参数。 该assert
还显示了equals()
已实现,并允许我们正确比较此类不可变对象。
您可以查看此转换的实现细节 。 作为记录,上面使用@Immutable
转换的Groovy示例超过50行等效Java代码。
@懒
另一个转换是@Lazy
。 有时,您希望懒惰地处理clas字段的初始化,以便仅在首次使用时才计算其值,这通常是因为创建时间可能很耗时或占用内存。 通常的方法是自定义所述字段的getter,以便在首次调用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')
对于用于初始化字段的复杂计算,您可能需要调用某种方法来完成工作,而不是像宠物列表这样的值。 然后,可以通过闭包调用完成延迟评估,如以下示例所示:
class Person {
@Lazy List pets = { /* complex computation here */ }()
}
还有一个选项可以利用软引用来实现垃圾回收的友好性,从而使此类惰性字段可能包含昂贵的数据结构:
class Person {
@Lazy(soft = true) List pets = ['Cat', 'Dog', 'Bird']
}
def p = new Person()
assert p.pets.contains('Cat')
编译器为pets
创建的内部字段实际上将是一个Soft引用,但是直接访问p.pets
将返回该引用所p.pets
的值(即p.pets
列表),从而使用户对soft引用的使用透明该类的。
@代表
Java不提供任何内置的委托机制,到目前为止,Groovy也不提供。 但是,通过@Delegate
转换,可以对类字段或属性进行注释,并成为将方法调用委托给它的对象。 在下面的示例中, Event
类具有日期委托,并且编译器会将在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
字段。 如果委托不是最终类,那么甚至可以通过扩展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
。 但是,如果您不希望您的类实现这些接口,则仍然可以通过在注释上指定一个参数来实现:
@Delegate(interfaces =false ) private List list = []
@Newify
@Newify
转换提出了两种实例化类的新方法。 第一个方法是提供类似Ruby的方法来使用new()
类方法创建实例:
@Newify rubyLikeNew() {assert Integer.new(42) == 42
}
rubyLikeNew()
但是也可以遵循Python方法,省略new
关键字。 想象一下以下树的创建:
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()
由于所有这些new
关键字遍布整个行,因此树的创建不是很容易阅读。 Ruby的方法更具可读性,因为需要使用new()
方法调用来创建每个元素。 但是,通过使用@Newify
,我们可以稍微改善树的构建以使其更容易使用:
@Newify([Tree, Leaf]) buildTree() {
Tree(Tree(Leaf(1), Leaf(2)), Leaf(3))
}
您还会注意到,我们只是让Tree
和Leaf
被newified。 默认情况下,在带注释的范围内,所有实例化都是新的 ,但是您可以通过指定您感兴趣的类来限制范围。此外,请注意,对于我们的示例,也许Groovy构建器可能更合适,因为它的目的实际上是创建任何类型的层次结构/树结构。
如果我们从前面几节中再看一下我们的坐标示例,那么使用@Immutable
和@Newify
可以以简洁但类型安全的方式创建路径可能会很有趣:
@Immutablefinal 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)
。
@类别和@Mixin
如果您已经使用Groovy一段时间了,那么您肯定对Categories的概念很熟悉。 这是一种扩展现有类型(甚至包括JDK或第三方库中的最终类),向其添加新方法的机制。 这也是编写领域特定语言时可以使用的技术。 让我们考虑以下示例:
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类别,我们能够使用其他方法来装饰Distance
类型。 在这里,我们将通过实际装饰Number
类型向数字添加getMeters()
方法。 通过将吸气剂添加到数字,您可以使用Groovy的nice属性语法来引用它。 因此,您无需编写300.getMeters()
,而是可以编写300.meters
。
该类别系统和符号的缺点是,要将实例方法添加到其他类型,则必须创建static
方法,此外,还有第一个参数表示我们正在处理的类型的实例。 其他参数是该方法将作为参数的普通参数。 因此,如果我们可以访问它的源代码来增强它,它可能比我们在Distance
添加的普通方法定义的直观性要差一些。 这是@Category
批注,它将带有实例方法的类转换为Groovy类别:
@Category(Number)class NumberCategory {
Distance getMeters() {
new Distance(number: this )
}
}
无需将方法声明为static
,在this
使用的this
实际上是类别将应用于其上的数字,如果我们创建一个类别实例,则不是真正的this
。 然后,要使用类别,可以继续使用use(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!"
您不必继承各种接口,而是在每个子类中注入相同的行为,而是将类别混入类中。 在这里,我们出色的詹姆斯·邦德(James Bond)车辆可通过mixins获得飞行和潜水功能。
这里要说明的重要一点是,与@Delegate
可以将接口注入声明了委托的类不同, @Mixin
只是进行运行时混合-正如我们将在本文@Mixin
的元编程增强中看到的那样。
@PackageScope
Groovy对于属性的约定是,没有任何可见性修饰符的任何字段都将作为属性公开,并为您透明地生成getter和setter。 例如,此Person
类为私有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的知识,Grape是一种在Groovy脚本中添加和利用依赖关系的机制。 Groovy脚本可能需要某些库:通过使用@Grab转换或Grape.grab()方法调用在脚本中明确表示,运行时将为您找到所需的JAR。 使用Grape,您可以轻松分发脚本而无需依赖它们,并在首次使用脚本时将其下载并缓存。 在后台,Grape使用Ivy和Maven存储库,其中包含您可能需要在脚本中使用的库。
假设您要获取Java 5文档所引用的所有PDF文档的链接。 您想使用Groovy XmlParser
来解析HTML页面,就好像它是XML兼容文档一样(不是),因此您可以使用TagSoup SAX兼容解析器,将HTML转换为格式正确的有效XML。 您甚至不必在运行脚本时弄乱您的类路径,只需通过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改进
为了总结我们对AST转换的概述,让我们最后讲两个对Swing开发人员非常有用的转换: @Bindable
和@Vetoable
。 创建Swing UI时,您通常对监视某些UI元素的值变化感兴趣。 为此,通常的方法是使用JavaBeans PropertyChangeListener
,以在类字段的值更改时得到通知。 然后,您最终在Java Bean中编写了以下非常常见的样板代码:
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 ,以了解地图和列表上的类似机制。
与@Bindable
,还有一个@Vetoable
转换,用于当您需要否决某些属性更改时。 让我们考虑一个Trompetist
类,其中表演者的名字不允许包含字母'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构建器示例:
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 }
}
}
运行此脚本将在下面的框架中显示一个文本字段和一个下方的标签,并且标签的文本绑定在文本字段的内容上。
在过去的一年中,SwingBuilder的发展非常出色,以至于Groovy Swing团队决定启动一个基于该项目的新项目,并在Grails的基础上: Griffon项目诞生了。 Griffon建议引入Grails的Configuration over Condig范式,以及其所有项目结构,插件系统,gant脚本功能等。
如果您正在开发Swing富客户,请确保查看Griffon 。
Swing控制台改进
随着UI主题的发展,Swing控制台也得到了发展:
- 该控制台可以作为Applet(
groovy.ui.ConsoleApplet
)运行。 - 除了语法突出显示之外,编辑器还支持代码缩进。
- 将Groovy脚本拖放到文本区域上将打开文件。
- 您可以通过在类路径中添加新的JAR或目录来修改运行控制台中脚本的类路径,如下面的屏幕快照所示。
- 视图菜单项中已添加了两个选项:用于显示正在输出区域中运行的脚本,以及可视化执行结果。
- 在脚本中引发异常时,可单击相对于脚本的stacktrace的行,以轻松导航到发生错误的地方。
- 另外,当脚本包含编译错误时,错误消息也可以单击。
回到脚本输出区域中结果的可视化显示,添加了一个有趣的系统,可让您自定义呈现某些结果的方式。 当执行脚本返回爵士乐音乐家的地图时,您可能会在控制台中看到以下内容:
您在这里看到的是Map
的通常文本表示形式。 但是,如果我们启用某些结果的自定义可视化怎么办? Swing控制台允许您执行此操作。 首先,您必须确保选中了可视化选项: View -> Visualize Script Results
—为了进行记录,Groovy Console的所有设置都被存储并记住了,这要归功于Preference API。 内置了一些结果可视化效果:如果脚本返回没有父级的java.awt.Image
, javax.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控制台将在启动时执行该脚本,并在脚本绑定中注入一个transforms
列表,以便您可以添加自己的脚本结果表示形式。 在我们的例子中,我们将Map
转换为漂亮的Swing JTable
。 现在,我们能够以友好和有吸引力的方式可视化地图,如以下屏幕截图所示:
显然,不要将Swing控制台与真正成熟的IDE混淆,但是对于日常脚本编写任务,该控制台是工具箱中的便捷工具。
元编程增强
使Groovy成为动态语言的原因是它的元对象协议和元类概念,这些概念代表了类和实例的运行时行为。 在Groovy 1.6中,我们将继续改进此动态运行时系统,并添加一些新功能。
每个实例元类,甚至对于POJO
到目前为止,Groovy POGO(普通的旧Groovy对象)可以具有每个实例的元类,但是POJO对于所有实例只能具有一个元类(即,每个类的元类)。 现在不再是这种情况了,因为POJO也可以具有每个实例的元类。 同样,将metaclass属性设置为null将恢复默认的元类。
ExpandoMetaClass DSL
ExpandoMetaClass最初是在Grails伞下开发的,然后又重新集成到Groovy 1.5中,它是一种非常方便的方法,用于更改对象和类的运行时行为,而不是编写功能MetaClass
类。 每次我们想要添加/更改现有类型的几个属性或方法时,都会重复执行Type.metaClass.xxx
。 以处理操作员过载的单元操作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 */ }
,但是,在有多个方法时,应使用append操作符并遵循patten methodName << { /* closure */ }
。 也可以通过这种机制添加静态方法,因此可以代替经典方法:
// 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向现有类中添加属性的通常方法是添加一个getter和setter作为方法。 例如,假设您要添加一种计算文本文件中单词数量的方法,则可以尝试以下操作:
File.metaClass.getWordCount = {
delegate.text.split(/\w/).size()
}new File('myFile.txt').wordCount
当吸气剂内部存在某种逻辑时,这当然是最好的方法,但是当您只想通过ExpandoMetaClass DSL拥有包含简单值的新属性时,可以定义它们。 在以下示例中,将lastAccessed
属性添加到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'}"
在我们的示例中,在DSL中,我们通过闭包的delegate
访问该属性,并使用delegate.lastAccessed = new Date()
。 而且,由于invokeMethod()
,我们拦截了所有方法调用,将调用委托给原始方法,并在该方法不存在的情况下抛出异常。 稍后,通过执行此脚本,您可以看到,一旦我们在实例上调用方法, lastAccessed
就会更新。
运行时混合
我们今天将介绍的最后一个元编程功能:运行时混合。 @Mixin
允许您将新行为混入您拥有和正在设计的类中。 但是您不能将任何东西混入您不拥有的类型。 运行时混合器建议通过允许您在运行时在任何类型上添加混合来填补这一空白。 如果我们再次考虑具有某些混合功能的车辆的示例,如果我们不拥有詹姆斯·邦德的车辆并赋予其某些潜水功能,则可以使用以下机制:
// provided by a third-partyinterface 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作为参数传递给Groovy在Class
上添加的static mixin()
方法。
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
请注意, javax.script.*
API仅在Java 6上可用。
JMX生成器
作为源自Google Code的外部开源项目 ,JMX Builder已集成到Groovy 1.6中,以简化需要交互或公开JMX服务的开发人员的生活。 JMX Builder的功能:
- 使用构建器模式的JMX API的域特定语言(DSL)
- 简化的JMX API的可编程性
- 以声明方式将Java / Groovy对象公开为JMX管理的MBean
- 支持类嵌入或显式描述符
- 对JMX事件模型的内在支持
- 无缝创建JMX事件广播器
- 将事件侦听器附加为内联闭包
- 利用Groovy的动态特性轻松响应JMX事件通知
- 为MBean提供灵活的注册策略
- 没有特殊的接口或类路径限制
- 使开发人员免于JMX API的复杂性
- 公开属性,构造函数,操作,参数和通知
- 简化连接器服务器和连接器客户端的创建
- 支持导出JMX计时器
您可以找到有关JMX Builder及其对JMX系统的广泛介绍的更多信息 。 许多示例将向您展示如何创建JMX连接器服务器或客户端,如何轻松将POGO导出为JMX受管bean,如何侦听JMX事件,等等。
改进的OSGi支持
Groovy jar文件是使用正确的OSGi元数据发布的,因此可以将它们作为捆绑包加载到任何OSGi兼容容器中,例如Eclipse Equinox或Apache Felix。 您可以在Groovy项目网站上找到有关如何使用Groovy和OSGi的更多信息 。 本教程将说明如何:
- 将Groovy加载为OSGi服务
- 编写Groovy OSGi服务
- 包含捆绑包中的Groovy JAR
- 润滑用Groovy编写的服务
- 使用Groovy的服务
- 如果您在途中遇到任何问题,请进行故障排除
借助 OSGi,您可能还对例如如何在应用程序中使用不同版本的Groovy感兴趣。
摘要
Groovy继续朝着简化开发人员生命的目标迈进,在此新版本中提供了各种新功能和改进:AST转换大大减少了表达某些问题和模式的代码行数,并向开发人员开放了该语言以进行进一步扩展,多项元编程增强功能可简化代码并让您编写更具表现力的业务规则 ,并支持常见的企业API,例如Java 6的脚本API,JMX管理系统或OSGi的编程模型。 所有这些工作显然都在不影响与Java的无缝集成的前提下完成,此外,其性能水平也比以前的版本更高 。
现在,我们已经到了本文的结尾,如果您还不是Groovy的用户,我希望这个技巧可以使您更好地了解Groovy在您的项目中所提供的功能,如果您已经知道并使用过Groovy,您了解了该语言的所有新功能。 亲爱的读者,对您来说,下一步就是下载Groovy 1.6 。 如果您想更深入地研究Groovy , Grails和Griffon ,我也邀请您加入GR8会议 , GR8会议是专门针对Groovy,Grails和Griffon的 会议 , 会议在丹麦哥本哈根举行,这些技术的专家和制造商将通过实用的演示文稿和动手实验指导您。翻译自: https://www.infoq.com/articles/groovy-1-6/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1