Java语言一直想引入元编程和函数式编程风格
这说明不需要侵入一个类的源代码,即可使用便捷的方法轻松扩展该类,即便是神圣不可侵犯的
元对象协议(MetaObject Protocol,MOP)的基础入手,介绍如何在Groovy中完成类AOP操作,并探讨方法/属性的动态发现与分派
Groovy包含并扩展了JDK
命令行工具groovysh 是把玩Groovy最为快速的方式之一。打开一个终端窗口,输入groovysh ,如下所示,我们会看到一个shell
如果不太确定要输入的命令,可以输入所知道的尽可能多的字符,然后按Tab键。shell会打印以我们输入的部分名字打头的可用方法
Groovy也有些偏爱图形用户界面(Graphical User Interface,GUI)的用户——只需要在Windows资源管理器中双击groovyConsole.bat
随着时间的推移,groovyConsole 变得越来越好了,可以执行保存脚本、打开现有脚本等操作
在命令行中运行Groovy
实际上groovy 命令对于执行较大的Groovy脚本和类非常有用
IntelliJ IDEA
联合编译Java和Groovy代码,重构与调试Java和Groovy代码,以及在同一项目中使用Java和Groovy代码
因为Groovy支持Java语法,并且保留了Java语义,所以我们尽可以随心所欲地混用两种语言风格
Groovy自动导入下列包
java.lang 、java.util 、java.io 和java.net 。它也会导入java.math.BigDecimal 和java.math.BigInteger 两个类。此外,它还导入了groovy.lang 和groovy.util 这些Groovy包
Groovy能够理解println() ,因为该方法已经被添加到java.lang.Object 中
upto() ,它是Groovy向java.lang.Integer 类中添加的一个便于使用的实例方法,可用于迭代
$it 是什么呢?在这个上下文中,它代表进行循环时的索引值
Groovy并没有强迫我们学习一组新的类和库。通过向JDK的各种类中添加便捷方法,Groovy扩展了强大的JDK。这些扩展可以在称作GDK(或Groovy JDK,
安全导航(safe-navigation)操作符(?.
def foo(str) {
//if (str != null) { str.reverse() }
str?.reverse()
}
println foo('evil')
println foo(null)
使用?. 在空引用上调用reverse() ,其结果是产生了一个null ,而没有抛出空指针异常(NullPointerException ),这是Groovy减少噪音、节省开发者力气的另一手段。
Groovy还有其他一些使这门语言更为轻量级、更为易用的特性,试举几例如下
return 语句几乎总是可选的(参见2.11.1节)。
尽管可以使用分号分隔语句,但它几乎总是可选的(参见2.11.6节)。
方法和类默认是公开(public )的。
?. 操作符只有对象引用不为空时才会分派调用。
可以使用具名参数初始化JavaBean(参见2.2节)。
Groovy不强迫我们捕获自己不关心的异常,这些异常会被传递给代码的调用者。
静态方法内可以使用this 来引用Class 对象
笔记内容:这点也和Java不一样.
在下面的例子中,learn() 方法返回的是Class 对象,所以可以使用链式调用:
class Wizard {
def static learn(trick, action) {
//...
this
}
Groovy会在背后默默地为其创建一个访问器和一个更改器
当在代码中调用miles 时,其实并非引用一个字段,而是调用该属性的访问器
要把属性设置为只读的,需要使用final 来声明该属性,这和Java中一样。在这种情况下,Groovy会为该属性提供一个访问器,但不提供更改器
可以把字段标记为private ,但是Groovy并不遵守这一点3 。因此,如果想把变量设置为私有的,必须实现一个拒绝任何修改的更改器
private miles = 0
private void setMiles(miles) {
throw new IllegalAccessException("you're not allowed to change miles")
}
灵活初始化与具名参数
笔记内容:具名参数其实就是Map参数, 整鸡巴这么多名称干啥. 有病. 还真以为你能上天不成.
在构造对象时,可以简单地以逗号分隔的名值对来给出属性值
如果类有一个无参构造器,该操作会在构造器之后执行。
需要把第一个形参定义为Map
class Robot {
def type, height, width
def access(location, weight, fragile) {
println "Received fragile? $fragile, weight: $weight, loc: $location"
}
}
robot = new Robot(type: 'arm', width: 10, height: 40)
println "$robot.type, $robot.height, $robot.width"
robot.access(x: 30, y: 20, z: 10, 50, true)
//可以修改参数顺序
robot.access(50, true, x: 30, y: 20, z: 10)
// 运行上面代码,看一下输出:
arm, 40, 10
Received fragile? true, weight: 50, loc: [x:30, y:20, z:10]
Received fragile? true, weight: 50, loc: [x:30, y:20, z:10]
如果发送的实参的个数多于方法的形参的个数,而且多出的实参是名值对,那么Groovy会假设方法的第一个形参是一个Map ,然后将实参列表中的所有名值对组织到一起,作为第一个形参的值。之后,再将剩下的实参按照给出的顺序赋给其余形参
但是可能会给人带来困惑,所以请谨慎使用。如果想使用具名参数,那最好只接受一个Map 形参,而不要混用不同的形参
通过显式地将第一个形参指定为Map ,可以避免这种混乱
可选形参
def log(x, base=10) {
Math.log(x) / Math.log(base)
}
// 使用多赋值
// 我们可以返回一个数组,然后将多个变量以逗号分隔,放在圆括号中,置于赋值表达式左侧即可。
def name1 = "Thomson"
def name2 = "Thompson"
println "$name1 and $name2"
(name1, name2) = [name2, name1]
println "$name1 and $name2"
而当变量与值的数目不匹配时,Groovy也可以优雅地处理。如果有多余的变量,Groovy会将它们设置为null ,多余的值则会被丢弃。
def (String cat, String mouse) = ['Tom', 'Jerry', 'Spike', 'Tyke']
println "$cat and $mouse"
在Groovy中,可以把一个映射或一个代码块转化为接口,因此可以快速实现带有多个方法的接
```java
// Java代码
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent ae) {
JOptionPane.showMessageDialog(frame, "You clicked!");
}
});
```
button.addActionListener(
{ JOptionPane.showMessageDialog(frame, "You clicked!") } as ActionListener
)
接口中的每个方法需要不同的实现。不用担心,Groovy可以摆平。只需要创建一个映射,以每个方法的名字作为键,以方法对应的代码体作为键值,同时使用简单的Groovy风格,用冒号(: )分隔方法名和代码块即可
此外,不必实现所有方法,只需实现真正关心的那些即可。如果未予实现的方法从未被调用过,那么也就没有必要浪费精力去实现这些伪存根。当然,如果没提供的方法被调用了,则会出现NullPointerException
如果知道所实现接口的名字,使用as 操作符即可,但如果应用要求的行为是动态的,而且只有在运行时才能知道接口的名字,又该如何呢?asType() 方法可以帮忙。通过将欲实现接口的Class 元对象作为一个参数发送给asType() ,可以把代码块或映射转化为接口。我们来看一个例子。
events = ['WindowListener', 'ComponentListener']
// 上面的列表可能是动态的,而且可能来自某些输入
handler = { msgLabel.setText("$it") }
for (event in events) {
handlerImpl = handler.asType(Class.forName("java.awt.event.${event}"))
frame."add${event}"(handlerImpl)
}
类型与布尔求值对它们的特殊处理
类型 为真的条件
Boolean 值为true
Collection 集合不为空
Character 值不为0
CharSequence 长度大于0
Enumeration hasMoreElements() 为true
Iterator hasNext() 为true
Number Double值不为0
Map 该映射不为空
Matcher 至少有一个匹配
Object[] 长度大于0
其他任何类型 引用不为null
Groovy支持操作符重载,可以巧妙地应用这一点来创建DSL
Java是不支持操作符重载的,那Groovy又是如何做到的呢?其实很简单:每个操作符都会映射到一个标准的方法1 。在Java中,可以使用那些方法;而在Groovy中,既可以使用操作符,也可以使用与之对应的方法。
要向集合中添加元素,可以使用<< 操作符,该操作符会被转换为Groovy在Collection 上添加的leftShift() 方法
lst = ['hello']
lst << 'there'
println lst
// 在完成追加元素后,可以在输出中看到完整的集合:
[hello, there]
通过添加映射方法,我们可以为自己的类提供操作符,比如为+ 操作符添加plus() 方法
ComplexNumber 类重载了+ 操作符
笔记内容:定义了plus就是重载了+
在Groovy中可以为静态方法和类名定义别名。要定义别名,需要在import 语句中使用as 操作符:
使用@Delegate
Groovy使委托变得非常容易,所以我们可以做出正确的设计选择。
class Worker {
def work() { println 'get work done' }
def analyze() { println 'analyze...' }
def writeReport() { println 'get report written' }
}
class Expert {
def analyze() { println "expert analysis..." }
}
class Manager {
@Delegate Expert expert = new Expert()
@Delegate Worker worker = new Worker()
}
def bernie = new Manager()
bernie.analyze()
bernie.work()
bernie.writeReport()
在编译时,Groovy会检查Manager 类,如果该类中没有被委托类中的方法,就把这些方法从被委托类中引入进来。因此,首先它会引入Expert 类中的analyze() 方法。而从Worker 类中,只会把work() 和writeReport() 方法因为进来。这时候,因为从Expert 类带来的analyze() 方法已经出现在Manager 类中,所以Worker 类中的analyze() 方法会被忽略
因为有了@Delegate 注解,Manager 类是可扩展的。如果在Worker 或Expert 类上添加或去掉了方法,不必对Manager 类做任何修改, 相应的变化就会生效
@Newify
在Groovy中,经常会按照传统的Java语法,使用new 来创建实例。然而,在创建DSL时,去掉这个关键字,表达会更流畅
使用@Singleton
要实现单件模式,正常来讲,我们会创建一个静态字段,并创建一个静态方法来初始化该字段,然后返回单件实例。
而通过使用@Singleton 变换则完全可以避免这种麻烦
@Singleton(lazy = true)
class TheUnique {
private TheUnique() { println 'Instance created' }
def hello() { println 'hello' }
}
println "Accessing TheUnique"
TheUnique.instance.hello()
TheUnique.instance.hello()
这里使用@Singleton 注解标记了TheUnique 类,以生成静态的getInstance() 方法。因为此处将lazy 属性的值设为了true ,所以会将实例的创建延迟到请求时
Groovy编译器groovyc 大多数情况下不会执行完整的类型检查
,而是在遇到类型定义时执行强制类型转换
// Java代码
public void method() {
System.out.println("in method1");
{
System.out.println("in block");
}
}
Java中的代码块定义了一个新的作用域,但是Groovy会感到困扰。Groovy编译器会错误地认为我们是要定义一个闭包,并给出编译错误。在Groovy中,方法内不能有任何这样的代码块。
闭包与匿名内部类的冲突
Groovy的闭包是使用花括号({…} )定义的,而定义匿名内部类也是使用花括号
当构造器接收一个闭包作为参数时,就出现问题了:
class Calibrator {
Calibrator(calculationBlock) {
print "using..."
calculationBlock()
}
}
正常情况下,可以通过把一个代码块附到函数调用末尾,将闭包传递给函数:instance.method() {…} 。
按照这种习惯,我们可以通过向Calibrartor 的构造器传递一个闭包来实例化一个实例,如下面代码所示:
GroovyForJavaEyes/AnonymousConflict.groovy
def calibrator = new Calibrator() {
println "the calculation provided"
}
事与愿违,Groovy却认为我们是要创建一个匿名内部类,因而报告了一个错误。
要绕开这个陷阱,必须修改调用方式,将闭包放在构造器调用语句的圆括号内。我们仍然可以在调用时定义闭包,或传递一个引用该闭包的变量
def calibrator1 = new Calibrator({
println "the calculation provided"
})
def calculation = { println "another calculation provided" }
def calibrator2 = new Calibrator(calculation)
在Java中,可以像下面这样创建整型的数组
int[] arr = new int[] {1, 2, 3, 4, 5};
而在Groovy中,上述代码会导致编译错误。Groovy以如下方式定义基本类型的数组:
int[] arr = [1, 2, 3, 4, 5]
使用动态类型
def takeHelp(helper) {
//...
helper.helpMoveThings()
//...
}
// takeHelp() 接受一个helper ,但是没有指定其类型
class Man {
void helpMoveThings() {
//...
println "Man's helping"
}
//...
}
class Woman {
void helpMoveThings() {
//...
println "Woman's helping"
}
//...
}
class Elephant {
void helpMoveThings() {
//...
println "Elephant's helping"
}
void eatSugarcane() {
//...
println "I love sugarcanes..."
}
//...
}
//下面是一个调用takeHelp() 方法的例子:
TypesAndTyping/TakeHelp.groovy
takeHelp(new Man())
takeHelp(new Woman())
takeHelp(new Elephant())
使用动态类型需要自律
Groovy会聪明地选择正确的实现——不仅基于目标对象(调用方法的对象),还基于所提供的参数。因为方法分派基于多个实体——目标加参数,所以这被称作多分派或多方法(Multimethods)。
笔记内容:请注意Groovy的多态和Java的多态不太一样哟
如果闭包是最后一个实参,可以用下面这种优雅的语法:
笔记内容:我觉得恶心. 这不但不优雅, 还特别操蛋. 很容易让人搞错的好吗.
这里判断他是一个调用,而不是一个方法的创建通过她没有def,或者类型修饰. 如果是定义的话是这样
def pickEven(xxx) {
代码块中的it 是什么呢?如果只向代码块中传递一个参数,那么可以使用it 这个特殊的变量名来指代该参数。
不费吹灰之力,这个例子就实现了策略模式
closure
笔记内容:这个闭包什么时候运行了???
卧槽. 这个他省略了(). 麻蛋. 能不能好好写代码.
动态闭包
可以确定一个闭包是否已经提供。如果尚未提供,比如说是一个算法,我们可以决定使用该算法的一个默认实现来代替调用者未能提供的特殊实现。下面是确定一个闭包是否存在的例子:
def doSomeThing(closure) {
if (closure) {
closure()
} else {
println "Using default implementation"
}
}
doSomeThing() { println "Use specialized implementation" }
doSomeThing()
def completeOrder(amount, taxComputer) {
tax = 0
if (taxComputer.maximumNumberOfParameters == 2) {// 期望传入税率
tax = taxComputer(amount, 6.05)
} else {// 使用默认税率
tax = taxComputer(amount)
}
println "Sales tax is ${tax}"
}
completeOrder(100) { it * 0.0825 }
completeOrder(100) { amount, rate -> amount * (rate/100) }
maximumNumberOfParameters 属性(或getMaximumNumberOfParameters() 方法)告诉我们给定的闭包接受的参数个数
除了参数个数,还可以使用parameterTypes 属性(或getParameterTypes() 方法)获知这些参数的类型
如果希望闭包完全不接受任何参数,必须使用{-> } 这种语法:在-> 之前没有形参,说明该闭包不接受任何参数
this 、owner 和delegate 是闭包的三个属性,用于确定由哪个对象处理该闭包内的方法调用
闭包内的方法调用解析顺序
而在Java中,‘a’ 是一个char ,“a” 才是一个String 对象。Groovy中没有这样的分别。在Groovy中,二者都是String 类的实例。如果想显式地创建一个字符,只需要输入’a’ as char
Groovy就有。可以通过将字符串包含在3个单引号内(‘’‘…’‘’ )来定义多行字面常量
~ 操作符可以方便地创建RegEx模式
Groovy通过便捷方法对JDK的增强,
其中很多方法大量使用了闭包
GDK是基于JDK的,所以在Java代码和Groovy代码之间传递对象时,无需任何转换。当处在同一JVM中时,Java端和Groovy端使用的是同一对象。对于Groovy端看到的对象,因为Groovy向其中添加了便于使用、可以提高开发效率的方法,所以看上去更时髦。
使用上下文with()方法
支持创建一个上下文 (context)
在with 的作用域内调用的任何方法,都会被定向到该上下文对象,这样就去掉了对该实例的多余引用
这里没有隐含的上下文,我们重复地(或者说冗余地)使用了对象引用lst 。在Groovy中,可以使用with() 方法设置一个上下文,因此代码可以改成下面这样:
间接访问属性
这时可以使用[] 操作符(该操作符映射到Groovy添加的getAt() 方法)动态地访问属性。
- class Car {
int miles, fuelLevel
}
car = new Car(fuelLevel: 80, miles: 25)
properties = ['miles', 'fuelLevel']
// 上面的列表可能通过一些输入来填充
// 或者来自一个Web应用中的动态表单
properties.each { name ->
println "$name = ${car[name]}"
}
car[properties[1]] = 100
println "fuelLevel now is ${car.fuelLevel}"
我们能够间接地与该实例交互,如输出所示:
miles = 25
fuelLevel = 80
fuelLevel now is 100
如果以String 形式接收到方法名,而且想调用该方法,使用反射要这样实现——首先必须从实例取到Class 元对象,然后调用getMethod() 方法得到Method 实例,最后在该实例上调用invoke() 方法
在Groovy中不需要做这些,而只需简单地调用invokeMethod() 方法。Groovy中的所有对象都支持该方法。这是一个例子
- class Person {
def walk() { println "Walking..." }
def walk(int miles) { println "Walking $miles miles..." }
def walk(int miles, String where) { println "Walking $miles miles $where..." }
}
peter = new Person()
peter.invokeMethod("walk", null)
peter.invokeMethod("walk", 10)
peter.invokeMethod("walk", [2, 'uphill'] as Object[])
下面是间接调用该方法的输出:
Walking...
Walking 10 miles...
Walking 2 miles uphill...
Thread.start
笔记内容:能这么写吗.
// Java代码
import java.io.*;
public class ReadFile {
public static void main(String[] args) {
try {
BufferedReader reader = new BufferedReader(
new FileReader("thoreau.txt"));
String line = null;
while((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch(FileNotFoundException ex)
ex.printStackTrace();
} catch(IOException ex) {
ex.printStackTrace();
}
}
}
Groovy通过向BufferedReader 、InputStream 和File 添加一个text 属性,使之简单了许多,我们可以把文件的全部内容都读到一个String 中
println new File(‘thoreau.txt’).text
如果不想一次性读入整个文件,而想一次读取并处理一行,可以使用eachLine() 方法,它会对读入的每行文本调用一个闭包
new File(‘thoreau.txt’).eachLine { line ->
println line // 或者在这里执行自己想对该行进行的任何处理
}
我们还可以在编译时向现有类添加实例方法或静态方法,并在运行时在应用中使用它们
要使用该特性,需要做到两点:首先,想要添加的方法必须定义在一个扩展模块类中;其次,需要在清单文件(manifest)中放一些描述信息,告诉Groovy编译器要查找的扩展模块。
两类扩展方法都必须定义为static 的,而且第一个参数应该是该方法要加入到的类型
(. )符号遍历层次结构
@ 符号说明要访问的是属性,而非子元素。
“”"
MarkupBuilder 或StreamingMarkupBuilder
eachRow
rows
firstRow
withStatement
可以设置一个将在查询执行之前调用的闭包。如果想
Weather.groovy
Java和Groovy的混合
要把一个Groovy脚本拉到我们的Groovy代码中,可以使用GroovyShell
而如果要在Java类中使用Groovy脚本,则可以使用JSR 223提供的ScriptEngine API
如果想在Java中使用Groovy类,或者想在Groovy中使用Java类,我们可以利用Groovy的联合编译(joint-compilation)工具
请记住,当我们的Java类依赖其他Java类时,如果没有找到字节码,javac 将编译它认为必要的任何Java类。不过javac 对Groovy可没这么友好
幸好groovyc 支持联合编译。当我们编译Groovy代码时,它会确定是否有任何需要编译的Java类,并负责编译它们。因此我们可以自由地在项目中混合使用Java代码和Groovy代码,而且不必执行单独的编译步骤。简单地调用groovyc 就好。
要联合编译这两个文件,我们输入这个命令:groovyc -j AJavaClass.java UseJavaClass.groovy -Jsource 1.6 。-Jsource 1.6 会把可选的选项source = 1.6 发送给Java编译器。使用javap 检查生成的字节码,会发现AJavaClass 作为一个普通的Java类,扩展了java.lang.Object ,而UseJavaClass 扩展了groovy.lang.Script
可以与Java无缝地在项目中混用,使Groovy成为企业级应用中可以与Java漂亮集成的一种奇妙语言
在Java中创建与传递Groovy闭包
如果仔细检查,我们会发现,当Groovy调用一个闭包时,它只是使用了一个名为call() 的特殊方法。要在Java中创建一个闭包,我们只需要一个包含该方法的类。如果Groovy代码要向闭包传递实参,我们必须确保call() 方法接受这些实参作为形参。
ClassesAndScripts/AGroovyClass.groovy
class AGroovyClass {
def useClosure(closure) {
println "Calling closure"
closure()
}
def passToClosure(int value, closure) {
println "Simply passing $value to the given closure"
closure(value)
}
}
ClassesAndScripts/UseAGroovyClass.java
//Java代码
public class UseAGroovyClass {
public static void main(String[] args) {
AGroovyClass instance = new AGroovyClass();
Object result = instance.useClosure(new Object() {
public String call() {
return "You called from Groovy!";
}
});
System.out.println("Received: " + result);
}
}
// Calling closure
// Received: You called from Groovy!
ClassesAndScripts/UseAGroovyClass2.java
//Java代码
System.out.println("Received: " +
instance.passToClosure(2, new Object() {
public String call(int value) {
return "You called from Groovy with value " + value;
}
}));
//Simply passing 2 to the given closure
//Received: You called from Groovy with value 2
每个Groovy对象都实现了GroovyObject 接口,该接口有一个叫作invokeMethod() 的特殊方法。该方法接受要调用的方法的名字,以及要传递的参数。
ClassesAndScripts/Script1.groovy
println “Hello from Script1”
这里有一个命名为Script1.groovy的文件,我们想将其作为另一个脚本——Script2.groovy——的一部分来执行,如下所示:
ClassesAndScripts/Script2.groovy
println "In Script2"
shell = new GroovyShell()
shell.evaluate(new File('Script1.groovy'))
// 或是更简单点
evaluate(new File('Script1.groovy'))
如果我们想向脚本传递一些参数,又该怎么办呢?
ClassesAndScripts/Script1a.groovy
println "Hello ${name}"
name = "Dan"
这个脚本需要一个变量name 。我们可以使用一个Binding 实例来绑定变量,如下所示:
ClassesAndScripts/Script2a.groovy
println "In Script2"
name = "Venkat"
shell = new GroovyShell(binding)
result = shell.evaluate(new File('Script1a.groovy'))
println "Script1a returned : $result"
println "Hello $name"
在发起调用的脚本中,我们创建了一个变量name (与被调用脚本中用到的变量名字相同)。当创建GroovyShell 的实例时,我们将当前的Binding 对象传给了它(每个脚本执行时都有一个这样的对象)。被调用脚本现在就可以使用(读取和设置)发起调用脚本所知道的变量了。
如果不希望影响当前的binding , 而是想将其与被调用脚本的binding 分开,我们只需要创建一个新的Binding 实例,在该实例上调用setProperty() 来设置变量名和值,并将其作为创建GroovyShell 实例时的一个参数,如下所示:
ClassesAndScripts/Script3.groovy
println "In Script3"
binding1 = new Binding()
binding1.setProperty('name', 'Venkat')
shell = new GroovyShell(binding1)
shell.evaluate(new File('Script1a.groovy'))
binding2 = new Binding()
binding2.setProperty('name', 'Dan')
shell.binding = binding2
shell.evaluate(new File('Script1a.groovy'))
如果想向脚本传递一些命令行参数,可以使用GroovyShell 类的run() 方法来代替evaluate() 方法。
从Java中使用Groovy脚本
要从Java中调用一个(非编译好的)脚本,需要使用脚本引擎。而脚本引擎可以通过调用ScriptEngineManager 的getEngineByName() 方法获得。从Java代码中执行脚本,则要调用脚本引擎的eval() 方法。
要使用Groovy脚本,需要确保…/jsr223-engines/groovy/build/groovy-engine.jar在我们的classpath下。
- MixingJavaAndGroovy/CallingScript.java
// Java代码
package com.agiledeveloper;
import javax.script.*;
public class CallingScript {
public static void main(String[] args) {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("groovy");
System.out.println("Calling script from Java");
try {
engine.eval("println 'Hello from Groovy'");
} catch(ScriptException ex) {
System.out.println(ex);
}
}
}
输出如下:
Calling script from Java
Hello from Groovy
不同于使用ScriptEngineManager ,从Java内还可以使用GroovyScriptEngine ,这和从Groovy内使用GroovyShell 很像
动态地添加方法和行为,代码会变得更灵活
元对象协议(MetaObject Protocol,MOP
在Groovy中,使用MOP可以动态调用方法,甚至在运行时合成类和方法
Groovy应用中,我们会使用三类对象:POJO、POGO和Groovy拦截器
POJO(Plain Old Java Object)就是普通的Java对象,可以使用Java或JVM上的其他语言来创建
POGO(Plain Old Groovy Object)是用Groovy编写的对象,扩展了java.lang.Object ,同时也实现了groovy.lang.GroovyObject 接口
Groovy拦截器是扩展了GroovyInterceptable 的Groovy对象,具有方法拦截功能
一旦一个类被加载到JVM中,我们就不能修改它的元对象Class 了。不过我们可以通过调用setMetaClass() 修改它的MetaClass
GroovyInterceptable 接口。它是一个扩展了GroovyObject 的标记接口,对于实现了该接口的对象而言,其上的所有方法调用,不管是存在的还是不存在的,都会被它的invokeMethod() 方法拦截
Groovy支持对POJO和POGO进行元编程。对于POJO,Groovy维护了MetaClass 的一个MetaClassRegistry ,如图11-1所示。另一方面,POGO有一个到其MetaClass 的直接引用
当我们调用一个方法时,Groovy会检查目标对象是一个POJO还是一个POGO。对于不同的对象类型,Groovy的方法处理是不同的
对于一个POJO,Groovy会去应用类(application-wide)的MetaClassRegistry 取它的MetaClass ,并将方法调用委托给它。因此,我们在它的MetaClass 上定义的任何拦截器或方法,都优先于POJO原来的方法。
对于一个POGO,Groovy会采取一些额外的步骤,如图11-2所示。如果对象实现了GroovyInterceptable ,那么所有的 调用都会被路由给它的invokeMethod() 。在这个拦截器内,我们可以把调用路由给实际的方法,使类AOP(Aspect-Oriented Programming,面向方面编程)的操作成为可能。
如果该POGO没有实现GroovyInterceptable ,那么Groovy会先查找它的MetaClass 中的方法,之后,如果没有找到,则查找POGO自身上的方法。 如果该POGO没有这样的方法,Groovy会以方法名查找属性或字段。如果属性或字段是Closure 类型的,Groovy会调用它,替代方法调用。如果Groovy没有找到这样的属性或字段,它会做最后两次尝试。如果POGO有一个名为methodMissing() 的方法,则调用该方法。否则调用POGO的invokeMethod() 。如果我们在POGO上实现了这个方法,它会被调用。invokeMethod() 的默认实现会抛出一个MissingMethodException 异常,说明调用失败
请研究代码,并尝试确定每种情况下Groovy会执行哪个方法
笔记内容:尝试过了,
- class TestMethodInvocation extends GroovyTestCase {
void testInterceptedMethodCallonPOJO() {
def val = new Integer(3)
Integer.metaClass.toString = {-> 'intercepted' }
assertEquals "intercepted", val.toString()
}
void testInterceptableCalled() {
def obj = new AnInterceptable()
assertEquals 'intercepted', obj.existingMethod()
assertEquals 'intercepted', obj.nonExistingMethod()
}
void testInterceptedExistingMethodCalled() {
AGroovyObject.metaClass.existingMethod2 = {-> 'intercepted' }
def obj = new AGroovyObject()
assertEquals 'intercepted', obj.existingMethod2()
}
void testUnInterceptedExistingMethodCalled() {
def obj = new AGroovyObject()
assertEquals 'existingMethod', obj.existingMethod()
}
void testPropertyThatIsClosureCalled() {
def obj = new AGroovyObject()
assertEquals 'closure called', obj.closureProp()
}
void testMethodMissingCalledOnlyForNonExistent() {
def obj = new ClassWithInvokeAndMissingMethod()
assertEquals 'existingMethod', obj.existingMethod()
assertEquals 'missing called', obj.nonExistingMethod()
}
void testInvokeMethodCalledForOnlyNonExistent() {
def obj = new ClassWithInvokeOnly()
assertEquals 'existingMethod', obj.existingMethod()
assertEquals 'invoke called', obj.nonExistingMethod()
}
void testMethodFailsOnNonExistent() {
def obj = new TestMethodInvocation()
shouldFail (MissingMethodException) { obj.nonExistingMethod() }
}
}
class AnInterceptable implements GroovyInterceptable {
def existingMethod() {}
def invokeMethod(String name, args) { 'intercepted' }
}
class AGroovyObject {
def existingMethod() { 'existingMethod' }
def existingMethod2() { 'existingMethod2' }
def closureProp = { 'closure called' }
}
class ClassWithInvokeAndMissingMethod {
def existingMethod() { 'existingMethod' }
def invokeMethod(String name, args) { 'invoke called' }
def methodMissing(String name, args) { 'missing called' }
}
class ClassWithInvokeOnly {
def existingMethod() { 'existingMethod' }
def invokeMethod(String name, args) { 'invoke called' }
}
不仅可以向类添加行为,还可以向类的一些实例添加行为
可以使用MetaObjectProtocol 的getMetaMethod() (MetaClass 扩展了MetaObjectProtocol )来获得一个元方法(metamethod)。如果要找的是一个static 方法,可以使用getStaticMetaMethod() 。要获得重载方法的列表,则使用这些方法的复数形式——getMetaMethods() 和getStaticMetaMethods()
类似地,使用getMetaProperty() 和getStaticMetaProperty() 可以获得一个元属性(metaproperty)
如果只是想简单地检查是否存在,而非获得元方法和元属性,我们使用respondsTo() 检查方法,使用hasProperty() 检查属性。
- ExploringMOP/UsingMetaMethod.groovy
str = "hello"
methodName = 'toUpperCase'
// 名字可能来自输入,而不是硬编码的
methodOfInterest = str.metaClass.getMetaMethod(methodName)
println methodOfInterest.invoke(str)
动态调用的方法产生如下输出:
HELLO
- ExploringMOP/AccessingObject.groovy
def printInfo(obj) {
// 假定用户从标准输入键入这些值
usrRequestedProperty = 'bytes'
usrRequestedMethod = 'toUpperCase'
println obj[usrRequestedProperty]
//或
println obj."$usrRequestedProperty"
println obj."$usrRequestedMethod"()
//或
println obj.invokeMethod(usrRequestedMethod, null)
}
printInfo('hello')
下面是这段代码的输出:
[104, 101, 108, 108, 111]
[104, 101, 108, 108, 111]
HELLO
HELLO
使用MOP拦截方法
这里我们将讨论Groovy中拦截方法调用的两种方式
要么让对象拦截,要么让MetaClass 拦截。让对象处理的话,需要实现GroovyInterceptable 接口
当我们不是类的作者,或者该类是一个Java类,再或者我们想动态地引入拦截,则第二种方式更适合。
GroovyInterceptable 的invokeMethod() 方法劫持了该对象上的所有方法调用
如果我们在一个对象的MetaClass 上实现了invokeMethod() ,不管方法存在与否,都会调用到该方法
这是因为println() 是Groovy在Object 上注入的一个方法
使用GroovyInterceptable 拦截了方法调用,这种方式适用于拦截作者是我们自己的类中的方法。然而,如果我们无权修改类的源代码,或者这个类是个Java类,就行不通了
我们也有可能在运行时决定基于某些条件或应用状态开始拦截调用。对于这几种情况,我们可以在MetaClass 上实现invokeMethod() 方法,并以此来拦截方法
我们前面看过,使用MetaClass 拦截调用有一个好处, 那就是也可以拦截POJO上的调用
为了看看实际效果,我们拦截一下Integer 上的方法调用,并执行类AOP的建议:
- InterceptingMethodsUsingMOP/InterceptInteger.groovy
Integer.metaClass.invokeMethod = { String name, args ->
System.out.println("Call to $name intercepted on $delegate... ")
def validMethod = Integer.metaClass.getMetaMethod(name, args)
if (validMethod == null) {
Integer.metaClass.invokeMissingMethod(delegate, name, args)
} else {
System.out.println("running pre-filter... ")
result = validMethod.invoke(delegate, args) // 如果要实现环绕建议,则去掉这条语句
System.out.println("running post-filter... ")
result
}
}
println 5.floatValue()
println 5.intValue()
try {
println 5.empty()
} catch(Exception ex) {
println ex
}
Groovy的MOP支持以下3种技术注入行为中的任何一种
分类(category)
ExpandoMetaClass
Mixin
分类 (category)是一种能够修改类的MetaClass 的对象,而且这种修改仅在代码块的作用域和执行线程内有效,当退出代码块时,一切会恢复原状
代码如下:
InjectionAndSynthesisWithMOP/UsingCategories.groovy
class StringUtil {
def static toSSN(self) { //如果想将参数限制为String类型,则使用toSSN(String self)
if (self.size() == 9) {
"${self[0..2]}-${self[3..4]}-${self[5..8]}"
}
}
}
use(StringUtil) {
println "123456789".toSSN()
println new StringBuilder("987654321").toSSN()
}
try {
println "123456789".toSSN()
} catch(MissingMethodException ex) {
println ex.message
}
这里注入的方法仅在use 块内有效,当在其外调用toSSN() 时,会出现MissingMethodException 异常
我们没有定义self 参数的类型,其类型默认为Object ,toSSN() 可以在任何对象上使用。如果想限制该方法仅支持String 和StringBuilder ,则必须使用显式的参数类型创建两个版本的toSSN() ,一个是String self ,一个是StringBuilder self 。
Groovy的分类要求注入的方法是静态的,而且至少接受一个参数。第一个参数(这个例子中叫作self )指向的是方法调用的目标。要注入的方法所需的参数都放在后面。参数可以是任何合法的Groovy参数,包括对象和闭包
@Category(String)
class StringUtilAnnotated {
def toSSN() {
if (size() == 9) {
"${this[0..2]}-${this[3..4]}-${this[5..8]}"
}
}
}
use(StringUtilAnnotated) {
println "123456789".toSSN()
}
@Category 注解会根据我们传入的String 参数将新定义的StringUtilAnnotated 类的toSSN() 转变为public static toSSN(String self) {…}
当调用use() 时,背后到底有何魔法。Groovy将在脚本中调用的use() 方法路由到了GroovyCategorySupport 类的public static Object use(Class categoryClass, Closure closure) 方法。该方法定义了一个新的作用域,其中包括栈上的一个新的属性/方法列表,用于目标对象的MetaClass 。之后它会检查给定分类中的每个静态方法,并将静态方法及其参数(至少有一个)加入到属性/方法列表中
- InjectionAndSynthesisWithMOP/UsingCategories.groovy
class FindUtil {
def static extractOnly(String self, closure) {
def result = ''
self.each {
if (closure(it)) { result += it }
}
result
}
}
use(FindUtil) {
println "121254123".extractOnly { it == '4' || it == '5' }
}
前面调用的结果如下:
54
可以同时使用多个分类,带入多组方法。use() 可以接受一个分类,也可以接受一个由分类组成的列表。下面的例子同时使用了前面创建的两个分类:
- InjectionAndSynthesisWithMOP/UsingCategories.groovy
use(StringUtil, FindUtil) {
str = "123487651"
println str.toSSN()
println str.extractOnly { it == '8' || it == '1' }
}
- 假设我们想拦截对toString() 的调用,然后在响应内容的每一侧加上两个感叹号。下面是使用分类的实现方式:
InjectionAndSynthesisWithMOP/UsingCategories.groovy
class Helper {
def static toString(String self) {
def method = self.metaClass.methods.find { it.name == 'toString' }
'!!' + method.invoke(self, null) + '!!'
}
}
use(Helper) {
println 'hello'.toString()
}
这段代码的输出如下:
!!hello!!
使用ExpandoMetaClass注入方法
通过向类的MetaClass 添加方法可以实现向类中注入方法。不同于分类中的局限于一个块,这种注入方法是全局可用的
下面这个例子使用ExpandoMetaClass 向Integer 中注入一个名为EdaysFromNow() 的方法
- InjectionAndSynthesisWithMOP/UsingExpandoMetaClass.groovy
Integer.metaClass.daysFromNow = { ->
Calendar today = Calendar.instance
today.add(Calendar.DAY_OF_MONTH, delegate)
today.time
}
println 5.daysFromNow()
->
去掉方法调用尾部的括号,会看上去更流畅(参见19.2节),接着就可以调用5.daysFromNow 了。然而这需要一个小窍门(参见19.9节)。如果没有括号,Groovy会将方法当成一个属性,所以需要设置一个属性,而非方法。要定义一个名为daysFromNow 的属性,必须创建一个名为getDaysFromNow() 的方法,像下面这样:
// InjectionAndSynthesisWithMOP/UsingExpandoMetaClass.groovy
Integer.metaClass.getDaysFromNow = { ->
Calendar today = Calendar.instance
today.add(Calendar.DAY_OF_MONTH, delegate)
today.time
}
println 5.daysFromNow
前面代码的输出如下。对daysFromNow 属性的调用现在被路由到了getDaysFromNow() 方法。
可以选择在Integer 的基类Number 上提供这个方法。我们在Number 上添加一个名为someMethod() 的方法,看看在Integer 和Long 上面是不是可以调用:
InjectionAndSynthesisWithMOP/MethodOnHierarchy.groovy
Number.metaClass.someMethod = { ->
println "someMethod called"
}
2.someMethod()
2L.someMethod()
向类中注入静态方法也是可以的,只需要将其加入到MetaClass 的static 属性中
下面向Integer 中加入一个静态方法:
InjectionAndSynthesisWithMOP/UsingExpandoMetaClass.groovy
Integer.metaClass.'static'.isEven = { val -> val % 2 == 0 }
println "Is 2 even? " + Integer.isEven(2)
println "Is 3 even? " + Integer.isEven(3)
执行这段代码,将得到如下输出:
Is 2 even? true
Is 3 even? false
类中还可以有第三类方法,即构造器。通过定义一个名为constructor 的特殊属性可以加入构造器。因为我们是要添加一个构造器,而不是替换一个现有的,所以使用了<< 操作符。注意:使用<< 来覆盖现有的构造器或方法,Groovy会报错。下面例子为Integer 引入一个构造器,它接受一个Calendar ,这样该实例就可以保存从这一年的开始到指定日期的天数了。
InjectionAndSynthesisWithMOP/UsingExpandoMetaClass.groovy
Integer.metaClass.constructor << { Calendar calendar ->
new Integer(calendar.get(Calendar.DAY_OF_YEAR))
}
println new Integer(Calendar.instance)
前面代码的输出如下:
349
如果不是想添加一个新的构造器,而是想替换(或者说覆盖,尽管严格讲构造器是不可覆盖的)一个原来的,可以使用= 操作符代替<< 操作符
如果想添加一两个方法,使用ClassName.metaClass.method = {…} 这样的语法向metaClass 中添加,既简单又方便。如果想添加一堆方法,这样声明和设置很快就会感觉费劲了。Groovy提供了一种可以将这些方法分组的方式,组织成一种叫作ExpandoMetaClass(EMC) DSL的方便的语法。前面的例子逐一地向Integer 的metaClass 中添加了一些方法。作为替代,可以将这些方法分组,如下所示:
InjectionAndSynthesisWithMOP/UsingEMCDSL.groovy
Integer.metaClass {
daysFromNow = { ->
Calendar today = Calendar.instance
today.add(Calendar.DAY_OF_MONTH, delegate)
today.time
}
getDaysFromNow = { ->
Calendar today = Calendar.instance
today.add(Calendar.DAY_OF_MONTH, delegate)
today.time
}
'static' {
isEven = { val -> val % 2 == 0 }
}
constructor = { Calendar calendar ->
new Integer(calendar.get(Calendar.DAY_OF_YEAR))
}
constructor = { int val ->
println "Intercepting constructor call"
constructor = Integer.class.getConstructor(Integer.TYPE)
constructor.newInstance(val)
}
}
然而,ExpandoMetaClass 也有些限制。注入的方法只能从Groovy代码内调用,不能从编译过的Java代码中调用,也不能从Java代码中通过反射来使用。不过要从Java中调用它们,有一个变通方案,参见10.6节。
向具体的实例中注入方法
每个实例都有一个MetaClass
可以将方法注入到从这个具体的实例获得的metaClass 中
也可以选择创建一个ExpandoMetaClass 实例,将指定方法加入其中
- InjectionAndSynthesisWithMOP/InjectInstance.groovy
class Person {
def play() { println 'playing...' }
}
def emc = new ExpandoMetaClass(Person)
emc.sing = { ->
'oh baby baby...'
}
emc.initialize()
def jack = new Person()
def paul = new Person()
jack.metaClass = emc
println jack.sing()
try {
paul.sing()
} catch(ex) {
println ex
}
Groovy提供了一种方便的方式来从实例中去掉这些注入的方法——只需要将metaClass 属性设置为null 。
前面用了几个步骤,来创建ExpandoMetaClass 、向其中加入方法以及初始化。其实不必这么麻烦。我们可以简单地将方法设置到该实例的metaClass 属性上,如下所示
- InjectionAndSynthesisWithMOP/InjectInstanceMetaClass.groovy
class Person {
def play() { println 'playing...' }
}
def jack = new Person()
def paul = new Person()
jack.metaClass.sing = { ->
'oh baby baby...'
}
println jack.sing()
try {
paul.sing()
} catch(ex) {
println ex
}
jack.metaClass = null
try {
jack.play()
jack.sing()
} catch(ex) {
println ex
}
这个版本的注入方法代码中少了很多噪音,而输出与之前的版本相同。
注入多个方法,也可以像13.2节所做的那样,使用EMC DSL将方法分组。将方法分组的语法如下:
jack.metaClass {
sing = { ->
'oh baby baby...'
}
dance = { ->
'start the music...'
}
}
使用Mixin注入方法
如果将一个类混入到另一个类中,Groovy会在内存中把这些类的类实例链接起来。当调用一个方法时,Groovy首先将调用路由到混入的类中,如果该方法存在于这个类中,则在此处理。否则由主类处理。可以将多个类混入到一个类中,最后加入的Mixin优先级最高
Mixin这个词由mix和in组合而来,顾名思义,即“混入进来
- InjectionAndSynthesisWithMOP/mixin.groovy
class Friend {
def listen() {
"$name is listening as a friend"
}
}
可以使用@Mixin 注解语法
- InjectionAndSynthesisWithMOP/mixin.groovy
@Mixin(Friend)
class Person {
String firstName
String lastName
String getName() { "$firstName $lastName"}
}
john = new Person(firstName: "John", lastName: "Smith")
println john.listen()
通过向注解提供由多个类名组成的列表,可以混入多个类,就像这样:@Mixin([Friend, Teacher])
Mixin的语法非常优雅,而且简洁,但是注解本身限制了这种方式只能由类的作者使用。如果没有类的源代码,或者不想修改源代码,就不能使用这种方式
下面看一下在运行时动态实现混入的语法
- InjectionAndSynthesisWithMOP/mixin.groovy
class Dog {
String name
}
Dog.mixin Friend
buddy = new Dog(name: "Buddy")
println buddy.listen()
这里没有使用注解,而是在Dog 类上调用了mixin() 方法,并将想混入到这个类中的类的名字传给了它
也可以有选择地向一个类的具体实例中混入类
使用methodMissing合成方法
在Groovy中,通过实现methodMissing() ,可以拦截对不存在的方法的调用。同样,通过实现propertyMissing() ,也可以拦截对不存在的属性的访问
在这些方法内,可以动态地为这些不存在的方法或属性实现相应逻辑。我们会基于所定义的约定推断其语义
使用ExpandoMetaClass合成方法
上一节介绍了如何合成方法。然而,如果我们无权编辑类的源文件,或者该类并非一个POGO,那种方法就行不通了。对于这类情况,可以使用ExpandoMetaClass 来合成方法。
这里将在MetaClass 上实现methodMissing() 方法。
- InjectionAndSynthesisWithMOP/MethodSynthesisUsingEMC.groovy
class Person {
def work() { "working..." }
}
Person.metaClass.methodMissing = { String name, args ->
def plays = ['Tennis', 'VolleyBall', 'BasketBall']
System.out.println "methodMissing called for $name"
def methodInList = plays.find { it == name.split('play')[1]}
if (methodInList) {
def impl = { Object[] vargs ->
"playing ${name.split('play')[1]}..."
}
Person.metaClass."$name" = impl //以后再调用就会使用它
impl(args)
} else {
throw new MissingMethodException(name, Person.class, args)
}
}
jack = new Person()
println jack.work()
println jack.playTennis()
println jack.playTennis()
try {
jack.playPolitics()
} catch(ex) {
println ex
}
前面代码的输出如下:
working...
methodMissing called for playTennis
playing Tennis...
playing Tennis...
methodMissing called for playPolitics
groovy.lang.MissingMethodException:
No signature of method: Person.playPolitics()
is applicable for argument types: () values: []
为具体的实例合成方法
通过向具体的实例提供专用的MetaClass ,也可以将方法合成到这些实例中
Groovy中可以完全在运行时创建一个类
Groovy的Expando 类提供了动态合成类的能力
- MOPpingUp/UsingExpando.groovy
carA = new Expando()
carB = new Expando(year: 2012, miles: 0)
carA.year = 2012
carA.miles = 10
println "carA: " + carA
println "carB: " + carB
输出如下:
carA: {year=2012, miles=10}
carB: {year=2012, miles=0}
- 还可以定义方法,并像调用任何方法那样调用它们
- MOPpingUp/UsingExpando.groovy
car = new Expando(year: 2012, miles: 0, turn: { println 'turning...' })
car.drive = {
miles += 10
println "$miles miles driven"
}
car.drive()
car.turn()
这段代码的输出如下:
10 miles driven
turning...
Groovy编译器允许我们进入其编译阶段,一窥其所处理的AST(抽象语法树)
Groovy生成器
Groovy可以用于很多日常任务,包括处理XML、JSON、HTML、DOM、SA
在Groovy中使用生成器创建XML文档:
- UsingBuilders/UsingXMLBuilder.groovy
bldr = new groovy.xml.MarkupBuilder()
bldr.languages {
language(name: 'C++') { author('Stroustrup')}
language(name: 'Java') { author('Gosling')}
language(name: 'Lisp') { author('McCarthy')}
}
这段代码使用groovy.xml.MarkupBuilder 来创建XML文档
<languages>
<language name='C++'>
<author>Stroustrupauthor>
language>
<language name='Java'>
<author>Goslingauthor>
language>
<language name='Lisp'>
<author>McCarthyauthor>
language>
languages>
我们调用了一个名为languages() 的方法,但该方法在MarkupBuilder 类的实例上并不存在。不过生成器并没有拒绝它,而是聪明地假定这次调用其实是想定义XML文档的一个根元素,这种假定可真不错
MarkupBuilder 十分适合小到中型的文档。然而,如果文档非常大(若干兆字节),我们可以使用StreamingMarkupBuilder ,它的内存占用情况更好一些
- UsingBuilders/BuildUsingStreamingBuilder.groovy
langs = ['C++' : 'Stroustrup', 'Java' : 'Gosling', 'Lisp' : 'McCarthy']
xmlDocument = new groovy.xml.StreamingMarkupBuilder().bind {
mkp.xmlDeclaration()
mkp.declareNamespace(computer: "Computer")
languages {
comment << "Created using StreamingMarkupBuilder"
langs.each { key, value ->
computer.language(name: key) {
author (value)
}
}
}
}
println xmlDocument
新版本的代码产生的输出如下:
<languages xmlns:computer='Computer'>
<computer:language name='C++'>
<author>Stroustrupauthor>
computer:language>
<computer:language name='Java'>
<author>Goslingauthor>
computer:language>
<computer:language name='Lisp'>
<author>McCarthyauthor>
构建JSON
groovy.json.JsonBuilder 的构造器
- UsingBuilders/BuildJSON.groovy
class Person {
String first
String last
def sigs
def tools
}
john = new Person(first: "John", last: "Smith",
sigs: ['Java', 'Groovy'], tools: ['script': 'Groovy', 'test': 'Spock'])
bldr = new groovy.json.JsonBuilder(john)
writer = new StringWriter()
bldr.writeTo(writer)
println writer
该生成器使用字段的名字以及它们的值作为JSON格式的键和值,如下所示:
{"first":"John","last":"Smith","tools":{"script":"Groovy","test":"Spock"},
"sigs":["Java","Groovy"]}
利用Groovy提供的JsonSlurper ,从JSON数据创建HashMap
- def sluper = new JsonSlurper()
def person = sluper.parse(new FileReader('person.json'))
println "$person.first $person.last is interested in ${person.sigs.join(', ')}"
使用元编程定制生成器
在安装Groovy时,已经自动获得了一个构建于JUnit之上的单元测试框架。可以使用它来测试Java虚拟机上的任何代码,包括Java代码、Groovy代码
只需要从GroovyTestCase 扩展出自己的测试类,并实现测试方法,然后准备运行测试就可以了
在Groovy中创建DSL
领域特定语言(Domain-Specific Language,DSL)针对的是“某一特定类型的问题”
其语法聚焦于指定的领域或问题。
DSL有两大特点:上下文驱动,非常流畅。
Ant和Gant(参见附录A)也是DSL的例子
- CreatingDSLs/OrderPizza.groovy
import com.agiledeveloper.*
PizzaShop joesPizza = new PizzaShop()
joesPizza.with {
setSize(Size.LARGE)
setCrust(Crust.THIN)
setTopping("Olives", "Onions", "Bell Pepper")
setAddress("101 Main St., ...")
int time = setCard(CardType.VISA, "1234-1234-1234-1234")
printf("Pizza will arrive in %d minutes\n", time)
}
- 因为在Groovy中类型是可选的,括号也几乎总是可选的(参见19.9节),所以前面的代码可以更轻巧一些
- CreatingDSLs/OrderPizza2.groovy
import com.agiledeveloper.*
PizzaShop joesPizza = new PizzaShop()
joesPizza.with {
setSize Size.LARGE
setCrust Crust.THIN
setTopping "Olives", "Onions", "Bell Pepper"
setAddress "101 Main St., ..."
time = setCard(CardType.VISA, "1234-1234-1234-1234")
printf "Pizza will arrive in %d minutes\n", time
}
Groovy对括号的灵活处理,让人喜忧参半。调用需要参数的方法时,Groovy不要求括号;但如果调用的方法没有参数,则括号不可或缺。
19.9节介绍了一个简单的技巧,用于解决这一烦恼。
如果方法会返回一个结果,不使用点符号(. ),就能在返回的这个实例上进行连续的调用
笔记内容:这个只能在方法返回值是this 的情况下才可以
且一个语句的开始不能这么做, 必须是中间才可以
闭包与DSL
再来看一下订购比萨的例子。假如想创建一种自然流动的语法,但是不想创建一个PizzaShop 实例
- CreatingDSLs/ClosureHelp.groovy
time = getPizza {
setSize Size.LARGE
setCrust Crust.THIN
setTopping "Olives", "Onions", "Bell Pepper"
setAddress "101 Main St., ..."
setCard(CardType.VISA, "1234-1234-1234-1234")
}
printf "Pizza will arrive in %d minutes\n", time
- time = getPizza
笔记内容:还可以这样, 直接将闭包调用, 且结果存起来.
- CreatingDSLs/ClosureHelp.groovy
def getPizza(closure) {
PizzaShop pizzaShop = new PizzaShop()
closure.delegate = pizzaShop
closure()
}
执行调用getPizza() 的代码,输出如下:
Pizza will arrive in 25 minutes
- closure.delegate = pizzaShop
笔记内容:注意, 他是直接指向了实例对象, 而不是类,
- 设计自己的DSL。
- CreatingDSLs/GroovyPizzaDSL.groovy
def large = 'large'
def thin = 'thin'
def visa = 'Visa'
def Olives = 'Olives'
def Onions = 'Onions'
def Bell_Pepper = 'Bell Pepper'
orderInfo = [:]
def methodMissing(String name, args) {
orderInfo[name] = args
}
def acceptOrder(closure) {
closure.delegate = this
closure()
println "Validation and processing performed here for order received:"
orderInfo.each { key, value ->
println "${key} -> ${value.join(', ')}"
}
}
必须想办法把这两个脚本放到一起执行。这可以非常简单地实现(参见10.8节),如下所示。调用GroovyShell ,加载前面的两个脚本,将它们聚合到一起,形成一个脚本,然后计算处理。
CreatingDSLs/GroovyPizzaOrderProcess.groovy
def dslDef = new File('GroovyPizzaDSL.groovy').text
def dsl = new File('orderPizza.dsl').text
def script = """
${dslDef}
acceptOrder {
${dsl}
}
"""
new GroovyShell().evaluate(script)
前面代码的输出如下:
Validation and processing performed here for order received:
size -> large
crust -> thin
topping -> Olives, Onions, Bell Pepper
address -> 101 Main St., ...
card -> Visa, 1234-1234-1234-1234
- CreatingDSLs/Total.groovy
value = 0
def clear() { value = 0 }
def add(number) { value += number }
def total() { println "Total is $value" }
clear()
add 2
add 5
add 7
total()
前面代码的输出如下:
Total is 14
这段代码中写的是total() 和clear() ,而不是total 和clear 。下面去掉括号,尝试调用total :
CreatingDSLs/Total.groovy
try {
total
} catch(Exception ex) {
println ex
}
执行这段代码,得到如下结果:
groovy.lang.MissingPropertyException:
No such property: total for class: Total
Groovy认为对total 的调用引用了一个(不存在的)属性。使用一门语言来设计DSL就像陪两岁的孩子玩耍,当孩子发脾气时,不要和他争,得让着他点。因此,在这种情况下,告诉Groovy一切正常,然后把问题处理掉。简单地创建它想要的属性即可:
value = 0
def getClear() { value = 0 }
def add(number) { value += number }
def getTotal() { println "Total is $value" }
通过编写getTotal() 和getClear() 方法,实现了名为total 和clear 的属性。现在Groovy会非常高兴(像个孩子一样)地跟我们玩了,我们也可以不用括号调用这些属性了:
clear
add 2
add 5
add 7
total
clear
total
输出如下:
Total is 14
Total is 0
现在想办法实现下面这种流畅的调用:2.days.ago.at(4.30)
- class DateUtil {
static int getDays(Integer self) { self }
static Calendar getAgo(Integer self) {
def date = Calendar.instance
date.add(Calendar.DAY_OF_MONTH, -self)
date
}
static Date at(Calendar self, Double time) {
def hour = (int)(time.doubleValue())
def minute = (int)(Math.round((time.doubleValue() - hour) * 100))
self.set(Calendar.HOUR_OF_DAY, hour)
self.set(Calendar.MINUTE, minute)
self.set(Calendar.SECOND, 0)
self.time
}
}
use(DateUtil) {
println 2.days.ago.at(4.30)
}
分类只能应用于use 块内,而且其效果被限制在了作用域内。如果希望方法注入在整个应用内都有效果,可以使用ExpandoMetaClass 来代替分类
下面使用ExpandoMetaClass 实现上一节介绍的DSL语法
- CreatingDSLs/DSLUsingExpandoMetaClass.groovy
Integer.metaClass{
getDays = { ->
delegate
}
getAgo = { ->
def date = Calendar.instance
date.add(Calendar.DAY_OF_MONTH, -delegate)
date
}
}
Calendar.metaClass.at = { Map time ->
def hour = 0
def minute = 0
time.each {key, value ->
hour = key.toInteger()
minute = value.toInteger()
}
delegate.set(Calendar.HOUR_OF_DAY, hour)
delegate.set(Calendar.MINUTE, minute)
delegate.set(Calendar.SECOND, 0)
delegate.time
}
println 2.days.ago.at(4:30)