这篇博客我们继续梳理一下Groovy的知识,
主要记录一下Groovy编写的脚本与Java类之间的联系。
1 Groovy定义类及import类
Groovy可以像Java那样定义类,例如:
package com.zhangjian.groovy
class Test {
String mName
String mTitle
Test(name, title) {
mName = name
mTitle = title
}
def print() {
println mName + ' ' + mTitle
}
}
与Java不同的是,如果不声明public/private等访问权限,
Groovy中定义类的方法及成员变量均默认是public的。
与java一样,其它文件如果需要使用这个类时,
需要使用import关键字导入。
例如,在Test类的根目录下创建一个测试文件test.groovy时,
可以这么使用Test.groovy:
import com.zhangjian.groovy.Test
def test = new Test('superman', 'hero')
test.print()
现在,我们可以这么运行test.groovy:
结合上图,需要注意的是:
调用groovy命令时,它会自动加载当前路径对应目录及子目录下的xxx.groovy文件。
因此,当test.groovy中进行import的操作时,
groovy会认为当前目录下存在com/zhangjian/groovy/Test.java的文件。
我们测试一下,回到当前文件的上级目录,
直接运行test.groovy,会发现如下结果:
容易看出,如果我们在workspace的上级目录运行test.groovy时,
groovy无法从当前目录加载到com/zhangjian/groovy/Test.groovy文件。
此时,如果想加载到Test.groovy文件,就需要利用–classpath或-cp参数,
指定类所在路径,例如:
2 脚本的编译结果
我们知道Groovy编程可以像写脚本一样,将要做的事都写在xxx.groovy中,
然后,利用groovy xxx.groovy直接运行。
但在之前的博客中我们已经提到过,Groovy是基于Java的面向对象的编程语言。
Groovy到底如何将脚本与Java对象联系在一起的呢?
为了找到这个问题的答案,我们可以看一下Groovy文件编译的结果。
我们先创建一个简单的文件test.groovy,其内容如下:
def fun () {
println 'have fun'
}
println 'Hello world'
容易看出,上面的代码定义了一个函数,并执行了一条命令。
接着,我们执行下列命令:
//-d后的参数指定的是:编译后文件的存放目录
//此处编译完的文件名是test.class
groovyc -d '/home/zhangjian/Desktop/temp' '/home/zhangjian/Desktop/test.groovy'
编译完成后,我们可以利用jd-gui反编译test.class,得到下面的结果(此处保留了反编译后的原始式样):
import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
public class test
extends Script
{
public Object fun()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[2].callCurrent(this, "have fun");return null;
}
public Object run()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[1].callCurrent(this, "Hello world");return null;
}
public static void main(String... args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, test.class, args);
}
public test(Binding context)
{
super(context);
}
public test() {}
}
从上面反编译后的代码可以看出:
1、test.groovy被转化成了一个test类,继承于Script类;
2、每一个Groovy脚本被编译后都会生成main函数,
当调用groovy xxx.groovy命令时,实际上就是利用java执行这个main函数;
3、脚本中的所有直接执行的代码都会放到run函数中,
可以看出当对象的main函数被调用后,会触发其run函数的运行;
4、如果脚本中定义了函数,那么这个函数将作为类的函数。
这里需要注意的是,假设我们这样写groovy脚本:
//注意这里定义函数,使用的是闭包
def fun = {
println 'have fun'
}
println 'Hello world'
然后,同样利用groovyc来编译,我们发现生成了两个文件:
test.class和test$_run_closure1.class。
熟悉Java编译结果的朋友,应该能看出来,
此时生成了test内部类对应的class文件。
我们同样利用jd-gui观察一下test.class:
import groovy.lang.Binding;
import groovy.lang.Closure;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.GeneratedClosure;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
public class test
extends Script
{
public test() {}
public test(Binding context)
{
super(context);
}
public static void main(String... args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, test.class, args);
}
//fun闭包对应一个内部类
//这部分内容就是test$_run_closure1.class中的内容
public class _run_closure1
extends Closure
implements GeneratedClosure
{
public _run_closure1(Object _thisObject)
{
super(_thisObject);
}
public Object doCall(Object it)
{
//包含闭包中的执行代码
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[0].callCurrent(this, "have fun");return null;
}
public Object doCall()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
return doCall(null);
return null;
}
}
public Object run()
{
//在run中创建了闭包
CallSite[] arrayOfCallSite = $getCallSiteArray();Object fun = new test._run_closure1(this);
return arrayOfCallSite[1].callCurrent(this, "Hello world");return null;
}
}
通过这个例子就可以看出,闭包虽然可以像函数一样被调用,
但在groovy中,它实际上是被当作一个内部类对象的。
这也印证了我们之前提到的,闭包同时具有函数和对象的特征。
3 脚本中的变量作用域
在本篇博客的最后,我们来研究一下脚本中定义变量的作用域。
定义一个简单的脚本文件,内容如下:
def x = 1
def printx() {
println x
}
printx()
当我们运行脚本时,却出现如下错误:
Caught: groovy.lang.MissingPropertyException: No such property: x for class: test
groovy.lang.MissingPropertyException: No such property: x for class: test
at test.printx(test.groovy:4)
at test.run(test.groovy:7)
乍一看,这个异常让人有点难以理解,毕竟在脚本中我们已经显示定义了变量x。
为了理解这个问题,我们同样利用groovyc编译该脚本,
然后利用jd-gui看看编译后的结果:
import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
public class test
extends Script
{
//printx被定义为一个函数
public Object printx()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[2].callCurrent(this, arrayOfCallSite[3].callGroovyObjectGetProperty(this));return null;
}
public Object run()
{
//定义变量x的操作被放在run中进行, 是一个局部变量
CallSite[] arrayOfCallSite = $getCallSiteArray();Object x = Integer.valueOf(1);
if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass())) {
return arrayOfCallSite[1].callCurrent(this);
} else {
return printx();
}
return null;
}
public static void main(String... args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, test.class, args);
}
public test(Binding context)
{
super(context);
}
public test() {}
}
从上面反编译的结果可以看出,出现异常的原因是x是一个局部变量。
与前文利用def定义闭包一样,定义的动作均被放至到run函数中进行。
此时,为了得到我们预期的结果,脚本必须这么写:
x = 1
def printx() {
println x
}
printx()
我们看看这种写法的反编译结果:
import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.ScriptBytecodeAdapter;
import org.codehaus.groovy.runtime.callsite.CallSite;
public class test
extends Script
{
public Object printx()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[2].callCurrent(this, arrayOfCallSite[3].callGroovyObjectGetProperty(this));return null;
}
public Object run()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();int i =
//注意这个地方
1;ScriptBytecodeAdapter.setGroovyObjectProperty(Integer.valueOf(i), test.class, this, (String)"x");
if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass())) {
return arrayOfCallSite[1].callCurrent(this);
} else {
return printx();
}
return null;
}
public static void main(String... args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, test.class, args);
}
public test(Binding context)
{
super(context);
}
public test() {}
}
从反编译的结果来看,当按照上述脚本的方式定义x时,
x将作为一个属性,在run方法创建test对象时,被添加到test中。
于是,test对象中的printx方法可以获取到x的值。
注意到,此时x并不是test的成员变量。
我们创建一个新的脚本user.groovy,其内容如下:
// 创建之前的test对象
def user = new test()
//调用其printx函数
user.printx()
运行user.groovy时,仍然会抛出无法找到x的异常。
正如前文的分析,我们知道x是test.groovy脚本被调用时,
在其run方法中被动态添加到test对象的属性中。
如果脚本没有运行,即run方法没被调用时(没有调用脚本的main方法),
仅调用test对象的printx函数是获取不到x属性的。
说了这么多,当我们需要时,
到底该如何创建test对象的成员变量了?
类似的解决方案如下:
import groovy.transform.Field
//在定义的变量前,增加@Field标注
//这样x才成为test的成员变量
@Field x = 1
def printx() {
println x
}
printx()
我们看看反编译的结果:
import groovy.lang.Binding;
import groovy.lang.Script;
import org.codehaus.groovy.runtime.BytecodeInterface8;
import org.codehaus.groovy.runtime.InvokerHelper;
import org.codehaus.groovy.runtime.callsite.CallSite;
public class test
extends Script
{
//成为成员变量
Object x;
public Object printx()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();return arrayOfCallSite[2].callCurrent(this, this.x);return null;
}
public Object run()
{
CallSite[] arrayOfCallSite = $getCallSiteArray();null;
if ((__$stMC) || (BytecodeInterface8.disabledStandardMetaClass())) {
return arrayOfCallSite[1].callCurrent(this);
} else {
return printx();
}
return null;
}
public static void main(String... args)
{
CallSite[] arrayOfCallSite = $getCallSiteArray();
arrayOfCallSite[0].call(InvokerHelper.class, test.class, args);
}
public test(Binding context)
{
super(context);
int i = 1;
this.x = Integer.valueOf(i);
}
public test() {}
}
这样其它脚本创建出test对象后,就可以直接访问x了。