Groovy学习笔记

1、Groovy中的所有事物都是对象实例。

2、Groovy支持整数和浮点数。整数是Integer类的实例,浮点数是BigDecimal类的实例。

3、Groovy支持的算术运算符包括加法、减法、乘法、除法。还有取模运算符(%),两个整数相除的结果是一个浮点数,对有个浮点数求模,或者对一个含有浮点数参数的整数求模都是非法的。

4、当在脚本中第一次使用某个变量时,需要使用Groovy关键字def,其用途是声明变量。

5、Groovy的动态类型(dynamic typing)功能,决定赋值是在运行时确定类型的。变量与对象之间的关联关系被称为引用。

6、Groovy支持关系运算符,(< 、<= 、> 、>=、== 、!=、<=>),其中<=>是用来替换compareTo方法的,例如5.compareTo(3) 表达式可以写成“5 <=> 3”,其结果是:1,通常a.compareTo(b),当a小于b时compareTo方法返回-1,当a大于b时返回+1,在两者相等的情况下返回0。另外,== 运算符相当于调用equals方法(==就是通过调用equals方法实现的)。

7、Groovy为表示字符串提供了数种不同的方式。例如可以使用单引号(')、双引号(")或者三引号(" " ")来封装字符串。其中在Groovy中使用三引号表示的字符串可以包含多行文本。

例子:

println 'He said "Hello"!'; // 单引号(嵌套双引号)
println "He said 'hello'!"; // 双引号(嵌套单引号)
println """one two three"""; // 三引号
// 多行文本使用三引号
println """Spared  // 
over
four
lines """;

结果:

He said "Hello"!
He said 'hello'!
one two three
Spared  // 
over
four
lines 


8、任何包含在解释型字符串中的$(expression)都将被求值,其结果是字符串的一部分。

例如:

def age = 25;
println 'My age is ${age}' // 注意:单引号中的${age}没有被解释
println "My age is ${age}"
println """My age is ${age}"""
println "My age is \${age}" // 注意:这里使用了双引号,但是$符号被转义,所以${age}也没有被解释

结果:

My age is ${age}
My age is 25
My age is 25
My age is ${age}


字符串通用的原则是:只在字符串需要被解释的时候使用双引号,在其他情况下字符串使用单引号。

9、关于字符串索引和索引段

      字符串的索引从零开始,止于该字符串的长度值减一。Groovy同样也支持负数索引,但是其顺序是从字符串的末尾开始往前计算的。另外子字符串通常可以用索引段(slicing)来表示。

例如:

def greeting = 'hello world'
println greeting[4]  // 返回索引位置
println greeting[-1]
println greeting[1..2]
println greeting[1..<3]
println greeting[4..2]
println greeting[4,1,6]

结果:

o
d
el
el
oll
oew


10、Groovy字符串是不可变的,它们在任何地方都不可能改变。可以通过索引、索引段以及合并其他字符串的方式创建一个新String对象。

11、Groovy使用Unicode字符集

12、正则表达式

        (1)、Groovy支持使用~“regex”来定义正则表达式。双引号中的文本表示正则表达式。例如:def regex =~‘cheese’

        (2)、当Groovy操作符“=~”作为一个谓词(表达式返回一个布尔值)出现时,左边的操作数String对象将和右边的正则表达式匹配。例如 ‘cheesecake’ =~ 'cheese'

        (3)、精确匹配符 “==~”需要精确匹配

        (4)、位置标识符 :脱字符号(^):表示某行的开始, 美元符号($):表示某行的结尾。

        (5)、加符号(+)表示在表达式中位于它前面的字符出现一次或者多次;

                星号(*)表示出现零次或者多次。

                “{”和“}”用来匹配位于“{”符号之前指定次数的字符。

        (6)、点符号(.)能代表任意字符,因而称其为通配符;

                正则表达式中的实际的点字符用“\\.”来表示。 例如:‘3.14’ =~‘3\\.14’

         (7)、在正则表达式中需要使用“\\\\”来表示一个反斜杠字符。

         (8)、一组字符集可以通过使用[和]元字符来表示。例如[aeiou]。对于字符和数字,可以使用破折号分隔符,如使用[a-z]或者[a-mA-M]表示。字符集合的余数可以通过脱字符来表示,如[^a-z]表示除指定字符之外的任意字符。

         (9)、可以通过组合正则表达式,生成更复杂的正则表达式。组合方式使用“(”和“)”元字符。例如正则表达式“(ab)*”表示ab出现任意次。使用选择符(用|表示)来组合一个或者多个可能的正则表达式。例如"(a|b)*"表示a、b或者ab混合出现任意次。

13、列表

        (1)、列表的初始化

        列表字面值是一系列包含在方括号中的对象集合,这些对象用逗号分隔。例如:

[11, 12, 13, 14] // 整型值列表
['Ken', 'John', 'Andrew'] // 字符串列表
[1, 2, [3, 4], 5] // 嵌套列表
['Ken', 21, 1.69] // 含有不同类型对象的列表
[] // 空列表

        (2)、访问列表

      *  可以使用索引操作符[]来标识元素值,例如def numbers = [11, 12, 13, 14] ;  numbers[0] 、numbers[3]、numbers[-1]

      *  索引端也同样可以用于列表,例如 [11, 12, 13, 14][2]  // 值为13

        注意:“[]”操作符就是List类中定义的getAt方法。

      * 也可以通过索引范围来操作列表。它将产生一个新的列表对象。例如:

def numbers = [11, 12, 13, 14]
println numbers[0..2] // 返回列表[11, 12, 13]
println numbers[1..<3] // 返回列表[12, 13]

      * 为列表元素赋值(putAt方法提供了赋值语句的功能)

      例如:numbers[1] = 22 // 这时列表变成了[11, 22, 13, 14]

                 numbers[1] = [33, 44] // 这时列表变成了[11, [33, 44], 13, 14]

      * 通过<<运算符(即leftShift方法) ,可以把一个新元素值追加到列表的最后

      例如:numbers << 15 // 这时列表变成了[11, [33, 44], 13, 14, 15]

      * 通过+操作符(即plus方法)可以连接两个列表:

      例如:numbers = [11, 12, 13, 14] ,  numbers + [15, 16] // 这时列表变成了[11, 12, 13, 14, 15, 16]

      * 通过-操作符(即minus方法)可以从列表中删除元素:

      例如:numbers = [11, 12, 13, 14], numbers - [13] // 这时列表变成了[11, 12, 14]    

14、映射

      把关键字(值对)放在方括号中,并使用逗号分隔。

      在映射字面值中,如果某个元素的关键字是一个变量名,那么它将被解释成一个String值。例如:

def x = 1
def y = 2
def m = [x:y, y:x]
println m  //结果: [x:2, y:1]

15、范围

      范围是指定特定序列值的一种简略方法。

      包含边界的范围通过“..”表示,而不包含边界的范围则在下边界和上边界之间使用“..<”。例如:

println 1990..1999 
println 2090..<2100
println 'A'..'D'
println 10..1
println 'Z'..'X'

结果:

[1990, 1991, 1992, 1993, 1994, 1995, 1996, 1997, 1998, 1999]
[2090, 2091, 2092, 2093, 2094, 2095, 2096, 2097, 2098, 2099]
[A, B, C, D]
[10, 9, 8, 7, 6, 5, 4, 3, 2, 1]
[Z, Y, X]


      范围的上标和下标同样可以通过整型表达式表示:例如:

def start = 10
def finish = 20
println start..finish + 1 //结果: [10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21]


注意:sort()和reverse()的顺序:

例如:

def z = [2, 1, 6, 5, 8]
println z.sort().reverse() // [8, 6, 5, 2, 1]
println z.reverse().sort() // [1, 2, 5, 6, 8]

注意:列表方法intersect返回两个列表共有的元素,例如:

def softwareHouse = ['Groovy':['Ken', 'John', 'Jon'],
'Java':['Ken', 'John'], 'C#':['Andrew']]

softwareHouse.get('Java').intersect( softwareHouse.get('Groovy')) // Result: [Ken, John]


16、基本输入输出

      1、严格地说,输入输出工具并不是Groovy语言的一部分,在Groovy中,为了实现基本的文本输出功能,可以使用下面形式的语句:

print xxx 或 print(xxx) 或println xxx 或 println(xxx)

例如:

// 输出字符串
print "My name is "
print("Ken")
println()

println "My first program"
println("This is fun")

// 输出一个数值
def age = 25;
print "My age is: "
println age
println "My age is: ${age}"

// 输出列表和映射值
def numbers=[11, 12, 13, 14]
def staffTel = ['Ken':2745, 'John':2746, 'Jessie': 2772]
println "Numbers:${numbers}" 
println "Staff telephones:${staffTel}"

结果:

My name is Ken
My first program
This is fun
My age is: 25
My age is: 25
Numbers:[11, 12, 13, 14]
Staff telephones:[Ken:2745, John:2746, Jessie:2772]

      2、格式化输出

      格式化输出通过调用printf方法实现,printf方法的调用形式为:printf(String format, List values)。例如:

// 转换规范
def a = 10
def b = 15
printf('The sum of %d and %d is %d \n', [a, b, a+b])  // The sum of 10 and 15 is 25 

注意:格式化字符串时,%s是原样输出字符串,%20s是在字符串右对齐,并且宽度为20;%-20s是字符串左对齐,且宽度为20

      3、基本输入

17、方法

       方法通常需要用关键字def声明,最简单的不含参数的方法的声明形式如下:

// 声明greeting方法
def greeting() {
    println 'Hello and welcome'
}

// 调用greeting方法
greeting()

注意:如果需要在同一行中使用两个或多个语句,则必须使用分号隔开。

       带参数的方法的声明格式如下:

def methodName(para1, para2, para3) {
    // 方法代码从这里开始
}

注意:方法的参数以形参列表的方式紧跟在方法名后面的括号中,各参数名必须互不相同

       默认参数:方法中的形参可以指定为默认参数,默认参数可以通过赋值语句来指定,默认参数仅能出现在非默认参数之后,例如:

def someMethod(para1, para2 = 0, para3 = 0) {
    // Method code goes here
}

       方法返回值:通过return语句的返回表达式。例如return expression

注意:return关键字是可选的。如果省略return关键字,则代码的最后一条语句的值就是方法的返回值。

       参数传递:在Groovy中,方法的参数传递策略为传值方式。在实参向形参传值时,Groovy会为形参创建一个新的对象以供使用,实参不受影响。

        作用域:run方法会将Groovy脚本编译成Java类。因此使用关键字def且在方法之外声明的变量对run方法来说是有效的局部变量,但是所有别的Groovy方法却不能引用它。

注意:Groovy中的方法也支持集合参数,如列表,并返回一个集合值。

18、流程控制

     顺序、选择、迭代

     while循环语法:

while(condition) {
    statement #1
    statement #2
    ...
}

     在groovy中,for语句通常用于循环处理某个范围、集合(列表、映射或者数组)、或者字符串

     for循环语法:

for(variable in range) | for(variable in collection) | for(variable in string) {
    statement #1
    statement #2
    ...
}

     注意,循环处理map的例子:

def staff = ['Ken':21, 'John':25, 'Sally':22]
def totalAge = 0
for(staffEntry in staff) 
   totalAge += staffEntry.value
   
println "Total staff age: ${totalAge}"

     注意,循环处理字符串的例子:

def name = 'Kenneth'
def listOfCharacters = []

for(letter in name) 
    listOfCharacters << letter
    
println "listOfCharacters: ${listOfCharacters}"


    注意:switch语句中的case表达式:

    case表达式既可以表示某个整型值,也可以表示整型值的范围。实际上,case表达式还可以是一个字符串、列表、范围、正则表达式或者某些类的对象。

19、闭包

      Groovy闭包是一种表示可执行代码块的方法。闭包也是对象,可以像方法一样传递参数。

      闭包的语法如下:{comma-separated-formal-parameter-list -> statement-list}

例如:无参数的闭包:

def clos = {println 'Hello world'}
clos.call()
     参数化的闭包:

def clos1 = {param -> println "Hello ${param}"}

clos1.call('world') 
clos1.call('again')
clos1('shortcut')
     使用闭包的隐参数it,例子如下:

def clos2 = {println "Hello ${it}"}

clos2.call('world')
clos2.call('again')
clos2('shortcut')

      注意:当且仅当闭包中没有显式地参数声明,it具有唯一参数引用的作用,一旦闭包包含了显式地参数声明,且声明中没有it,那么闭包逻辑代码块中it就降级为普通的变量。


     对于groovy Script,用def定义的变量对binding.variables不可见,没有def等任何定义的变量可被binding.variable参数名所访问,例如:

def c = 5
assert c == 5
d = 6
assert d == 6
assert binding.variables.c == null
assert binding.variables.d == 6
       注:当一个变量没有def等任何定义时,该变量全局有效。

      调用闭包的方法等于创建一个闭包实例。对于相同闭包创建出来的不同实例。它们的对象是不同的。例如:

c = {def e = {'milk'}; e}
d = c;
assert c == d;
v1 = c();
v2 = c()
assert v1 != v2
       闭包的快捷写法,当闭包作为闭包或方法的最后一个参数。可以将闭包从参数圆括号中提取出来接在括号后面。
       闭包与each方法的结合:

[1, 2, 3, 4].each {println it}
['Ken':21, 'John':22, 'Sally':25].each {println it}
['Ken':21, 'John':22, 'Sally':25].each {println "${it.key} maps to : ${it.value}"}
     通过if语句在闭包中过滤元素:

[1, 2, 3, 4].each {num -> if(num %2 == 0) println num}
    find方法、findAll方法与闭包的结合:

def value1 = [1, 3, 5, 7, 9].find {element -> element > 6}
println "Found:${value1}" // Found:7

def value2 = [1, 3, 5, 7, 9].findAll {element -> element > 6}
println "Found:${value2}" // Found:[7, 9]

     注:find方法在其需要匹配值的集合中定位第一个条目(如果有的话);而findAll方法将遍历所有元素,并返回一个符号条件的列表。
             
        只有在闭包被定义且存在,而不是在被调用时,可以访问其状态值。例如:

def greeting = 'Hello'
def clos = {param -> println "${greeting} ${param}"}
clos.call('world') // Hello world

greeting = 'Welcome'
clos.call('world') // Welcome world

def demo(clo) {
    def greeting = 'Bonjour'
    clo.call('Ken')
}

demo(clos) // Welcome Ken  // 在demo方法中定义的greeting并没有影响到闭包属性

20、文件

      在Groovy脚本中,命令行参数是一个字符串队列,可以通过args变量访问。通过:groovy [文件名.groovy] [参数表]  就可以运行groovy脚本。

      File类

      借助文件类的方法,可以判断某个文件对象是否存在、是否表示文件或目录、文件是可读还是可写、文件长度等。

(1)、以一次一行的方式读文件,例如:

import java.io.File

new File('E:\\GroovyTest\\staff.xml').eachLine { line ->
    println "Line: ${line}"
}

(2)、写文件,例如:

def outFile = new File('E:\\GroovyTest\\staff_copy.xml')
if(outFile.exists())
    outFile.delete()
    
def printWriter = outFile.newPrintWriter()

//write
printWriter.println('hello groovy!')

// Close up
printWriter.flush()
printWriter.close()

21、类

      Groovy类与Java类类似。可以拥有属性和方法

      Groovy类隐含了属性的getter/setter方法,即可以通过XX.X方式访问,也可以通过XX.getX()或XX.setX()方式访问。

      Groovy类可以含有构造方法,也可以不用编写构造方法,只需使用new操作符和指定的参数即可替代,例如 new Account(number:'ABC123', balance : 1200)。注意:一旦用户自定义了构造器后,系统就不再提供默认构造器。通过“new + 指定参数”的方式就不能使用,因为这需要默认构造器。

 22、继承

         在Groovy中,子类可以继承超类的所有属性和方法。Groovy类中没有权限修饰符修饰的属性或方法,都可以认为都是public的权限。

         抽象类、抽象方法、接口,与Java相同。

         由于Groovy支持动态类型,接口概念的价值并不大,在Groovy中,多态性只不过是匹配方法名和原型的一种方式。

23、单元测试(JUNIT)

       单元测试在迭代、递增的应用程序开发过程中有很大的作用。

       在面向对象系统中,类是最基本的组成单元,因此通常将一个类作为单元测试的基本单元。通常并不需要测试类的所有方法(这样做通常也是不可能完成的),测试的目的只是发现和纠正在应用程序中国部署类时可能出现的问题。

        实现单元测试的最显而易见的方法是为每个类编写测试脚本。但是,随着测试代码增加,测试人员的工作量也随之增加,这种测试方法过于浪费时间,并不合适;使用断言检查方式是另一种单元测试方法,但是该方法提升了代码的复杂度,对程序执行速度会有负面影响;另外,使用商用的测试工具也可行,但通常这些工具价格高昂,适用于大型应用程序的用户用例(功能性)测试。

        JUnit框架可以非常容易地用于测试Groovy类,只需要扩展GroovyTestCase类,它是基于JUnitTestCase类开发的标准Groovy环境的一部分。例如:

import groovy.util.GroovyTestCase

class BookTest extends GroovyTestCase {
    def void testToString() {
        def bk1 = new Book(catalogNumber : '111', title : 'Groovy', author : 'Ken')
        def expected = 'Book: 111 : Groovy by : Ken'
        
        assertToString(bk1, expected);
    }
}

     GroovyTestSuite类可以非常容易地使用JUnit TestSuite类(设计用来管理多个JUnitTestCase).例如:

import groovy.util.GroovyTestSuite
import junit.framework.Test
import junit.textui.TestRunner

class AllTests {
    
    static Test suite() {
        def allTests = new GroovyTestSuite()
        
        allTests.addTestSuite(BookTest.class)
        allTests.addTestSuite(LibraryTest.class)
        
        return allTests
    }
}

TestRunner.run(AllTests.suite())

     单元测试时迭代、递增式软件开发过程的一个组成部分。单元测试案例要在每次迭代完成时更新,以验证所有改动都是通过的。

24、持久性

       在Groovy中访问数据库:例如

import groovy.sql.*
import java.sql.Connection;

def DB = 'jdbc:oracle:thin:@191.168.0.52:1521/JTRDSERVICESTESTDB'
//def DB = 'jdbc:oracle:thin:@191.168.0.52:1521/ORCL'
def USER = 'xir_trd'
def PASSWORD = 'xpar'
def DIRVER = 'oracle.jdbc.driver.OracleDriver'

// Connect to database
def sql = Sql.newInstance(DB, USER, PASSWORD, DIRVER)

// Iterate over the result set
println 'Accounts'
println ' Number Balance '
println '+------------+--------------+'
sql.eachRow('select * from dual') { acc ->
    printf('---')
}
println '+------------+--------------+'

      注意:Groovy访问数据库,需要提前将数据库驱动包放置在Groovy安装目录下的lib文件夹下。

格式化输出sql结果的写法:

println '+------------------------------+----------------------------------+'
sql.eachRow('select * from XXXX t') { acc ->
    printf('| %-28s | %-28s\n', acc.ACCID, acc.ACCNAME)
    //println("ACCID:'${acc.ACCID}'            ,ACCNAME:'${acc.ACCNAME}")
}
println '+------------------------------+----------------------------------+'

条件查询的写法L:

sql.eachRow('select * from accounts where bankIDD = ?', ['RBS']) ...

更新数据库的写法:

* 插入单行数据:

def sql = Sql.newInstance(DB, USER, PASSWORD, DRIVER)

// insert a new row
sql.execute("insert into accounts(number, balance) values('GH1789', 600)")

// 或者
def newNumber = 'AAA111'
def newBalance = 1600
sql.execute("insert into accounts(number, balance) values(${newNumber}, ${newBalance})")

// 恢复原始状态
['GH1789', 'AAA111'].each {accNumber ->
    sql.execute('delete from accounts where number = ?', [accNumber])
}

* 使用DataSet更新数据库

// Connect to database
def sql = Sql.newInstance(DB, USER, PASSWORD, DRIVER)
def accounts = sql.dataSet('accounts') // 其中accounts为数据库中的一张表或试图

// now insert a new row
accounts.add(number:'GH1789', balance : 600)

// insert another
def newNumber = 'AAA111'
def newBalance = 1600

accounts.add(number : newNumber, balance : newBalance)

// show
accounts.each {acc ->
    println "Account:${acc.number}  ${acc.balance}"
}
println()

// 恢复数据
['GH1789', 'AAA111'].each {accNumber ->
    accounts.execute('delete from accounts where number = ?', [accNumber ])
}

// show again
accounts.each {acc ->
    println "Account:${acc.number}  ${acc.balance}"
}
println()


     下面是一个如何将关系型数据表中数据映射到对象的一个部分框架。

abstract class SqlQuery {
    
    def SqlQuery(sql, query) {
        this.sql = sql
        this.query = query
    }
    
    def execute() {
        def rowsList = sql.rows(query)
        def results = []
        def size rowsList.size()
        0.upto(size-1) {index ->
            results << this.mapRow(rowsList[index ])
        }
        return results
    }
    
    def abstract mapRow(row)
    
    //------properties-------------------------
    
    def sql
    def query
}

class AccountQuery extends SqlQuery {

    def AccountQuery(sql) {
        super(sql, 'select * from accounts')
    }
    
    def mapRow(row) {
        def acc = new Account(number : row.getProperty('number'), 
            balance : row.getProperty('balance'))
        return acc
    }
}

// 使用示例
// Connect to database
def sql = Sql.newInstance(DB, USER, PASSWORD, DRIVER)

// Prepare the query object
def accQuery = new AccountQuery(sql)

// Get the Accounts
def accs = accQuery.execute()

accs.each {acc ->
    println "${acc}"
}


     使用Spring框架

// Mapping SqlQuery类执行查询,并从查询结果中获取对象
// 父类MappingSqlQuery的核心思想是,让用户指定通过execute方法执行的SQL查询,这个查询在子类AccountQuery的构造器中指定。
// 子类必须包含mapRow方法的实现,把查询结果的每个行映射为一个对象,因此,execute方法将返回Account对象的一个列表

import java.sql.*
import org.springframework.jdbc.object.*

class AccountQuery extends MappingSqlQuery {
    def AccountQuery(ds) {
        super(ds, 'select * from accounts')
        this.compile()
    }
    
    protected Object mapRow(ResultSet rs, int rowNumber) {
        def acc = new Account(number : rs.getString('number'), balance : rs.getInt('balance'))
        return acc
    }
}

class AccountInsert extends SqlUpdate {
    def AccountInsert(ds) {
        super(ds, 'insert into accounts(number, balance) values(?, ?)')
        this.declareParameter(new SqlParameter(Types.VARCHAR))
        this.declareParameter(new SqlParameter(Types.INTEGER))
        this.compile()
    }
}

// 功能性需求描述接口
interface BankDaoIF {
    def abstract getAccounts()
    def abstract addAccount(acc)
}

//
class BankDaoJdbc implements BankDaoIF {
    def getAccounts() {
        def aQuery = new AccountQuery(dataSource)
        return aQuery.execute()
    }
    
    def addAccount(acc) {
        def params = [acc.number, acc.balance ]
        def aInsert = new AccountInsert(dataSource)
        aInsert.update(params as Object [])
    }
    
    //------properties---------------
    def dataSource
}

// 使用
class Bank {
    def Bank(name, dao) {
        this.name = name
        this.dao = dao
        
        accounts = dao.getAccounts()
    }
    
    def openAccount(account) {
        dao.addAccount(account)
        accounts << account
    }
    
    //------properties---------------
    def name
    def accounts
    def dao
}

// 完整使用示例
def DB = 'jdbc:derby:accountDB'
def USER = ''
def PASSWORD = ''
def DRIVER = 'org.apache.derby.jdbc.EmbeddedDriver'

def displayBank(bk) {
    println "Bank:${bk.name}"
    println '======================'
    
    bk.accounts.each {account -> println "${account}"}
    println()
}

def ds = new DriverManagerDataSource(driverClassName : DRIVER, url : DB, 
                username : USER, password : PASSWORD)
def dao = new BankDaoJdbc(dataSource : ds)
def bk = new Bank('Napier', dao)

def da = new Account(number : 'DDD444', balance : 5000)
bk.openAccount(da)

//now display everything
displayBank(bk)


       使用Spring的反向控制模式(Ioc):例如可以使用以下配置文件:

<?xml version="1.0" encoding=="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
                            "http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <constructor-arg index="0">
            <value>org.apache.derby.jdbc.EmbeddedDriver</value>
        </constructor-arg>
        <constructor-arg index="1">
            <value>jdbc:derby:libraryDB</value>
        </constructor-arg>
        <constructor-arg index="2">
            <value></value>
        </constructor-arg>
        <constructor-arg index="3">
            <value></value>
        </constructor-arg>
    </bean>
    <bean id="libDao" class="LibraryDaoJdbc">
        <property name="dataSource">
            <ref local="dataSource"/>
        </property>
    </bean>
    
    <bean id="lib" class="Library">
        <constructor-arg index="0">
            <value>Napier</value>
        </constructor-arg>
        <constructor-arg index="1">
            <ref local="libDao"/>
        </constructor-arg>
    </bean>
    
    <bean id="act" class="Action">
        <property name="library">
            <ref local="lib"/>
        </property>
    </bean>
</beans>

然后,创建一个合适的应用程序上下文,并从中获取对象,例如:

def applicationContext = new ClassPathXmlApplicationContext('config.xml')
def action = applicationContext.getBean('act')


另外注意:对于使用了Spring框架的应用程序,单元测试也可以使用上述的applicationContext来创建对象,并测试。

另外,一个优秀的基于JUnit的框架——DbUnit(参看:http://dbunit.sourceforge.net)非常适合测试数据库应用程序。

25、XML构造器和解析器

       用来表示任意嵌套的标记都是Groovy自身的语法。

(1)、MarkupBuilder的使用

       下面示例演示了使用MarkupBuilder构造一个XML文档。例如伪方法author('Ken Barclay')把参数变成XML元素的内容:<author>Ken Barclay</author>:

import groovy.xml.MarkupBuilder

// Create a builder
//def mB = new MarkupBuilder()
def mB = new MarkupBuilder(new File('E:\\GroovyTest\\book.xml').newPrintWriter())

// Compose the builder
mB.book() {
    author('Ken Barclay')  // producing <author>Ken Barclay</author>
    title('Groovy')
    publisher('Elsevier')
    isbn(number : '1234567890') // producing <isbn number='1234567890'/>
}

会在文件book.xml中输出如下信息:

<book>
  <author>Ken Barclay</author>
  <title>Groovy</title>
  <publisher>Elsevier</publisher>
  <isbn number='1234567890' />
</book>


      同时,也可以批量地填充xml文件数据。如下:使用一个映射存放将被填充的数据,然后将所有的数据填充到xml中:

注意:mB.book()中一定要使用mB前缀,以分辨出这是更深层次的标记,而不是作为闭包的一部分Groovy脚本。

import groovy.xml.MarkupBuilder
import java.io.*

def data = ['1111111111' : ['Groovy',                 'Ken Barclay',  'Elsevier'],
            '2222222222' : ['Object Oriented Design', 'John Savage',  'Elsevier'],
            '3333333333' : ['C Programming',          'Ken Barclay',  'Prentice Hall']
           ]

//Create a builder
def mB = new MarkupBuilder(new File('E:\\GroovyTest\\library.xml').newPrintWriter())

//Compose the builder
def lib = mB.library() {
    data.each {bk ->
        mB.book() {
            title(bk.value[0])
            author(bk.value[1])
            publisher(bk.value[2])
            isbn(number : bk.key)
        }
    }
}


结果如下:

<library>
  <book>
    <title>Groovy</title>
    <author>Ken Barclay</author>
    <publisher>Elsevier</publisher>
    <isbn number='1111111111' />
  </book>
  <book>
    <title>Object Oriented Design</title>
    <author>John Savage</author>
    <publisher>Elsevier</publisher>
    <isbn number='2222222222' />
  </book>
  <book>
    <title>C Programming</title>
    <author>Ken Barclay</author>
    <publisher>Prentice Hall</publisher>
    <isbn number='3333333333' />
  </book>
</library>


(2)、XML解析

使用Groovy类XmlParser可以将XML文档解析成树的节点实例。例如:

import groovy.util.*

def parser = new XmlParser()
def doc = parser.parse('E:\\GroovyTest\\library.xml')

// 输出book列表中的第一个book元素的第一个title元素的值
println "${doc.book[0].title[0].text()}"

// 使用迭代器方法和闭包处理<library>中的所有<book>元素
doc.book.each { bk ->
    println "${bk.title[0].text()}"
}

// 使用导航器功能简化上述代码
doc.book.title.each {title ->
    println "${title.text()}"
}

      符合“['@attributeName']”可以获取某个元素的属性值。例如['@number']可以获取元素的number属性。例如:

//<staff>
//    <lecturer name='Ken Barclay'>
//        <student name='David' grade='55'/>
//        <student name='Angus' grade='75'/>
//    </lecturer>
//    <lecturer name='John Savage'>
//        <student name='Jack' grade='60'/>
//        <student name='Todd' grade='44'/>
//        <student name='Mary' grade='62'/>
//    </lecturer>
//    <lecturer name='Jessie Kennedy'>
//        <student name='Mike' grade='50'/>
//        <student name='Ruth' grade='70'/>
//    </lecturer>
//</staff>
import groovy.util.*

def parser = new XmlParser()
def doc = parser.parse('E:\\GroovyTest\\staff.xml')

println doc.lecturer.student ['@name']

println doc.lecturer.student.findAll {stu ->
    stu['@grade'].toInteger() >= 65
}['@name']

doc.lecturer.student.each {stu ->
    if (stu['@grade'].toInteger() >= 65)
        println stu ['@name']
}

结果为:

[David, Angus, Jack, Todd, Mary, Mike, Ruth]
[Angus, Ruth]
Angus
Ruth

下面列举xml解析与基于MarkupBuilder的XML结构导航功能结合的一些应用:

(3)、将使用XML文档描述的数据库表转换为SQL指令

例如,原始的数据库表的XML描述如下,每个<table>元素都描述了一个关系型数据库表。<field>子元素通过其name和type表示表的字段。该示例仅包含几个基本类型。

<?xml version="1.0" encoding="UTF-8"?>

<tables>
    <table neme='Book'>
        <field name="title" type="text"/>
        <field name="isbn" type="text"/>
        <field name="price" type="integer"/>
        <field name="author" type="id"/>
        <field name="publisher" type="id"/>
    </table>
     <table neme='Author'>
        <field name="surname" type="text"/>
        <field name="forename" type="text"/>
    </table>
     <table neme='Publisher'>
        <field name="name" type="text"/>
        <field name="url" type="text"/>
    </table>
</tables>

转换的Groovy脚本如下:

import groovy.util.*

def typeToSQL = ['text' : 'TEXT NOT NULL',
                 'id'   : 'INTEGER NOT NULL',
                 'integer' : 'INTEGER NOT NULL'
                ]
def parser = new XmlParser()
def doc = parser.parse('E:\\GroovyTest\\tables.xml')

def outFile = new File('E:\\GroovyTest\\tables.sql')
def printWriter = outFile.newPrintWriter()

doc.table.each {tab ->
    printWriter.println "DROP TABLE IF EXISTS ${tab['@name']};"
    printWriter.println "CREATE TABLE ${tab['@name']}("
    printWriter.println " ${tab['@name']}_ID ${typeToSQL['id']}, "
    tab.field.each {col ->
        printWriter.println " ${col['@name']} ${typeToSQL[col['@type']]},"
    }
    printWriter.println " PRIMARK KEY ((${tab['@name']}_ID))"
    printWriter.println ");"
}

printWriter.flush()
printWriter.close()

结果:tables.sql文件的内容如下:

DROP TABLE IF EXISTS null;
CREATE TABLE null(
 null_ID INTEGER NOT NULL, 
 title TEXT NOT NULL,
 isbn TEXT NOT NULL,
 price INTEGER NOT NULL,
 author INTEGER NOT NULL,
 publisher INTEGER NOT NULL,
 PRIMARK KEY ((null_ID))
);
DROP TABLE IF EXISTS null;
CREATE TABLE null(
 null_ID INTEGER NOT NULL, 
 surname TEXT NOT NULL,
 forename TEXT NOT NULL,
 PRIMARK KEY ((null_ID))
);
DROP TABLE IF EXISTS null;
CREATE TABLE null(
 null_ID INTEGER NOT NULL, 
 name TEXT NOT NULL,
 url TEXT NOT NULL,
 PRIMARK KEY ((null_ID))
);


(4)、通过XML进行某种转换

可以将某些XML输入实施某种转换,并产生新的输出,新的输出可能是XML形式,或者其他形式。大部分的转换在Groovy中都是非常容易实现的,有些XSLT转换非常难以表示,对于这些转换来说,还必须求助于非常不简单的XSLT扩展。Groovy支持使用类似于XPath的符号转换复杂的结构,因而实现起来通常比使用XSLT简单的多。

例如,原始的xml描述如下,是按cd分组的:

<catalog>
    <cd>
        <title>Empire Burlesque</title>
        <artist>Bob Dylan</artist>
        <country>USA</country>
        <company>Columbia</company>
        <price>10.90</price>
        <year>1985</year>
    </cd>
     <cd>
        <title>Still got the blues</title>
        <artist>Gary More</artist>
        <country>UK</country>
        <company>Virgin Records</company>
        <price>10.20</price>
        <year>1990</year>
    </cd>
     <cd>
        <title>Hide you heart</title>
        <artist>Bonnie Tyler</artist>
        <country>UK</country>
        <company>CBS Records</company>
        <price>9.90</price>
        <year>1988</year>
    </cd>
</catalog>


我们的目标是以这些CD的原产地国家为标准分组,然后通过发行年份进一步分组。可以通过如下代码转换:

import groovy.util.*
import groovy.xml.*

def countryGrouping(catalog) {
    countryMap = [:]
    
    catalog.cd.each {cd ->
        if (countryMap.containsKey(cd.country[0].text())) {
            def yearMap = countryMap[cd.country[0].text()]
            if (yearMap.containsKey(cd.year[0].text()))
                yearMap[cd.year[0].text()] << cd.title[0].text()
            else 
                yearMap[cd.year[0].text()] =[cd.title[0].text()]
        } else {
            countryMap[cd.country[0].text()]=[(cd.year[0].text()):[cd.title[0].text()]]
        }
    }
    
    return countryMap
}

def parser = new XmlParser()
def doc = parser.parse('E:\\GroovyTest\\catalog.xml')

// Create a builder
def mB = new MarkupBuilder(new File('E:\\GroovyTest\\countries.xml').newPrintWriter())
def groupings = countryGrouping(doc)
mB.grouping() {
    groupings.each {country, yearMap ->
        mB.country(name : country) {
            yearMap.each {year, titleList ->
                mB.year(year : year) {
                    titleList.each {title ->
                        mB.title(title)
                    }
                }
            }
        }
    }
}

转换后,生成的countries.xml的内容如下:

<grouping>
  <country name='USA'>
    <year year='1985'>
      <title>Empire Burlesque</title>
    </year>
  </country>
  <country name='UK'>
    <year year='1990'>
      <title>Still got the blues</title>
    </year>
    <year year='1988'>
      <title>Hide you heart</title>
    </year>
  </country>
</grouping>


26、GUI构造器

      图形化应用程序是一些Swing组件的集合。组件可以使用层次方式嵌套在另一个组件中。因此,Groovy标记自带的语法也能在Swing应用程序中使用。

      Swing框架大而复杂,借助于类SwingBuilder,使用伪方法就可以表示Swing组件,注意,通常为方法不以J为前缀,并且首字母必须小写。伪方法参数用来初始化这些组件,闭包则用来定义子组件。

例如:

import groovy.swing.SwingBuilder
import javax.swing.*

def sB = new SwingBuilder()

def frame = sB.frame(title : 'Examlple01', location:[100, 100],
    size:[400, 300], defaultCloseOperation:WindowConstants.DISPOSE_ON_CLOSE) {
     label(text : 'Hello world')  
}

frame.pack()
frame.setVisible(true)

结果:



使用layout管理器:

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

// Create a builder
def sB = new SwingBuilder()

// Compose the builder
def frame = sB.frame(title : 'Examlple01', location:[100, 100],
    size:[400, 300], defaultCloseOperation:WindowConstants.DISPOSE_ON_CLOSE) {
    panel(layout : new GridLayout(3,2,5,5)) {
        label(text : 'Last Name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
        label(text : 'Middle Name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
        label(text : 'First Name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
    }
}

frame.pack()
frame.setVisible(true)

结果:


针对上述代码,可以使用递增式组合,来优化程序结构,我们可以先构件一个mainPanel组件,然后在frame组件中添加这个mainPanel组件。这样就分别定义各个子面板,然后将它们组合成框架。

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

// Create a builder
def sB = new SwingBuilder()

// Build the panel
def mainPanel = {
    sB.panel(layout:new GridLayout(3,2,5,5)) {
        label(text:'Last name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
        label(text:'Middle name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
        label(text:'First name:', horizontalAlignment : JLabel.RIGHT)
        textField(text : '', columns : 10)
    }
}

// Composite the frame
def frame = sB.frame(title:'Example03', location: [100, 100],
    size:[400, 300], defaultCloseOperation : WindowConstants.DISPOSE_ON_CLOSE) {
    mainPanel()
}

// Now show it
frame.pack()
frame.setVisible(true)


同样,可以创建含有按钮的面板,并给按钮添加事件处理器(event handler)。事件处理器表示按钮被单击之后将要执行的动作。每个button伪方法都有参数actionPerformed。例如:

// Build the button panel...
def buttonPanel = {
    sB.panel(constraints : BorderLayout.SOUTH) {
        button(text : 'OK', actionPerformed: {
            println 'OK pressed'
        })
        button(text : 'Cancel', actionPerformed: {
            println 'Cancel pressed'
        })
    }
}

针对上述代码,可以使用单独的闭包作为按钮的时间处理器,例如:

// Handlers
def okHandler = {
    println 'OK pressed'
}

def cancelHandler = {
    println 'Cancel pressed'
}

// Buttons
def buttons = [['OK', okHandler], ['Cancel', cancelHandler]]

// Create a builder
def sB = new SwingBuilder()

// Build the button panel...
def buttonPanel = {
    sB.panel(constraints : BorderLayout.SOUTH) {
       buttons.each {but ->
           sB.button(text : but[0], actionPerformed : but[1])
       }
    }
}


GUI构造器的应用

下面的程序将某个以英寸单位的数值,转换为厘米数值。

import groovy.swing.SwingBuilder
import javax.swing.*
import java.awt.*

// Create a builder 
def sB = new SwingBuilder()

// Properties
def inputText = null
def outputText = null

// Handlers
def doConvert = {
    def text = inputText.getText()
    def inches = text.toInteger()
    def centimetres = 2.54 * inches
    outputText.setText(centimetres.toString())
}

// Builder the input panel..
def inputPanel = {
    sB.panel() {
        label(text : 'Input the length in inches:', horizontalAlignment : JLabel.RIGHT)
        inputText = textField(text : '', columns : 10)
    }
}

// ...then the output panel...
def outputPanel = {
    sB.panel() {
        label(text : 'Converted length in centimeters:', horizontalAlignment : JLabel.RIGHT)
        outputText = textField(text : '', columns : 10, enabled : false)
        button(text : 'Convert', actionPerformed : doConvert)
    }
}

// ...and now the main panel
def mainPanel = {
    sB.panel(layout : new GridLayout(2, 3, 5, 5)) {
        inputPanel()
        outputPanel()
    }
}

// ...and the frame
def frame = sB.frame(title : 'Example07', location:[100, 100],
    size : [400, 300], defaultCloseOperation : WindowConstants.DISPOSE_ON_CLOSE) {
    mainPanel()
}

// Now show it
frame.pack()
frame.setVisible(true)


组件的使用示例:

1、List

import groovy.swing.SwingBuilder
import javax.swing.*
import javax.swing.event.*
import javax.swing.text.*
import java.awt.*

// properties
def staffList = null
def removeButton = null
def staffNameTextField = null

// Event handler for the Remove button
def doRemove = {
    def listModel = staffList.getModel()
    def index = staffList.getSelectedIndex()
    def size = listModel.size()
    
    listModel.remove(index)
    if (size == 0) 
        removeButton.setEnabled(false)
    else {
        if (index == listModel.getSize())
            index --
        staffList.setSelectedIndex(index)
        staffList.ensureIndexIsVisible(index)
    }
}

// Event handler for the Add button
def doAdd = {
    def listModel = staffList.getModel()
    def staffName = staffNameTextField.getText()
    
    if (staffName == "" || listModel.contains(staffName)) {
        Toolkit.getDefaultToolkit().beep()
        staffNameTextField.requestFocusInWindow()
        staffNameTextField.selectAll()
        return
    }
    
    def index = staffList.getSelectedIndex()
    index = (index == -1) ? 0 : 1 + index
    
    listModel.insertElementAt(staffName, index)
    staffNameTextField.requestFocusInWindow()
    staffNameTextField.setText('')
    
    staffList.setSelectedIndex(index)
    staffList.ensureIndexIsVisible(index)
}

//----------------------------------------------
// Implementation for an observer to register to receive
// notifications of changes to a text document.
class StaffDocumentListener implements DocumentListener {
    void changedUpdate(DocumentEvent event) {
        if (event.document.length <= 0) 
            button.setEnabled(false)
    }
    
    void insertUpdate(DocumentEvent event) {
        button.setEnabled(true)
    }
    
    void removeUpdate(DocumentEvent event) {
        if (event.document.length <= 0)
            button.setEnabled(false)
    }
    
    //-----properties--------------
    def button
}

//-------------------------------------------------
// Specialized DefaultListModel with a parameterized
// constructor
class StaffListModel extends DefaultListModel {
    StaffListModel(list) {
        super()
        list.each {item ->
        this.addElement(item)
        }
    }
}

// Create a builder
def sB = new SwingBuilder()

// Panel carrying the staff list
def listPanel = {
    sB.panel(constraints : BorderLayout.CENTER) {
        scrollPane() {
            def sList = ['Ken Barclay', 'John Savage',
                         'Sally Smith', 'Peter Thomson',
                         'John Owens', 'Neil Urquhart',
                         'Jessie Kennedy', 'Jon Kerridge'
                        ]
            staffList = list(model : new StaffListModel(sList),
                    selectionMode : ListSelectionModel.SINGLE_SELECTION,
                    selectedIndex : 0, visibleRowCount  : 4)
        }
    }
}

def buttonPanel = {
    sB.panel(constraints : BorderLayout.SOUTH) {
        removeButton = button(text : 'Remove', actionPerformed : doRemove)
        def plainDocument = new PlainDocument()
        staffNameTextField = textField(text : '', columns : 20,
            document : plainDocument, actionPerformed : doAdd)
        def addButton = button(text : 'Add', enabled : false, actionPerformed : doAdd)
        def documentListener = new StaffDocumentListener(button : addButton)
        plainDocument.addDocumentListener(documentListener)
    }
}

// Now the main panel...
def mainPanel = {
    sB.panel(layout : new BorderLayout()) {
        listPanel()
        buttonPanel()
    }
}

//...add the frame
def frame = sB.frame(title : 'Example08', location : [100, 100],
    size : [400, 300], defaultCloseOperation : WindowConstants.DISPOSE_ON_CLOSE) {
    mainPanel()
}

// Now show it
frame.pack()
frame.setVisible(true)


2、Table

// Panel carrying the staff list
def tablePanel = {
    sB.panel(constraints : BorderLayout.CENTER) {
        scrollPane() {
            table(selectionMode : ListSelectionModel.SINGLE_SELECTION) {
                def staffList = [[forename : 'Ken', surname : 'Barclay', room : 'C48', telephone : 2745],
                                 [forename : 'Sally', surname : 'Savage', room : 'C48', telephone : 2746],
                                 [forename : 'Peter', surname : 'Smith', room : 'C46', telephone : 2742],
                                 [forename : 'John', surname : 'Owens', room : 'C47', telephone : 2744],
                                 [forename : 'Neil', surname : 'Urquhart', room : 'C66', telephone : 2655] 
                                ]
                tableModel(list :staffList) {
                    closureColumn(header : 'First name', read : {row -> return row.forename})
                    closureColumn(header : 'Last naem', read : {row -> return row.surname})
                    closureColumn(header : 'Room', read : {row -> return row.room})
                    closureColumn(header : 'Tel extension', read : {row ->return row.telephone})
                }
                
            }
        }
    }
}


3、Box类和BoxLayout类

      类BoxLayout是一个管理组件的行或者列位置的布局管理器,特别适合管理带状按钮。类Box则是为BoxLayout管理器准备的一个轻量级容器。

// Button of set size
class FixedButton extends JButton {
    
    Dimension getMinimumSize() {return BUTTONSIZE }
    Dimension getMaximumSize() {return BUTTONSIZE }
    Dimension getPreferredSize() {return BUTTONSIZE }
    
    private static final BUTTONSIZE = new Dimension(80, 30)
}

// Text area of set size
class FixedTextArea extends JTextArea {
    
    Dimension getMinimumSize() {return TEXTAREASIZE}
    Dimension getMaximumSize() {return TEXTAREASIZE}
    Dimension getPreferredSize() {return TEXTAREASIZE}
    
    private  static final TEXTAREASIZE = new Dimension(400, 400)
}

def mainPanel = {
    sB.panel(layout : new BorderLayout()) {
        vbox(constraints : BorderLayout.WEST) {
            def buttons = ['One', 'Two', 'Three', 'Four']
                buttons.each {but ->
                    sB.widget(new FixedButton(text : but))
                }
        }
        
        panel(constraints : BorderLayout.CENTER) {
            widget(new FixedTextArea(enabled : false))
        }
    }
}


27、模板引擎
      Groovy的模板引擎(template engine),可以生成各种格式化的文件。
以下程序,展示了模板引擎的简单用法:

例如模板如下:

<book>
    <author>${author}</author>
    <title>${title}</title>
    <publisher>${publisher}</publisher>
    <isbn number="${isbn}"/>
</book>

程序如下:

import groovy.text.*
import java.io.*

def file = new File('E:\\GroovyTest\\book.template')
def binding = ['author'    : 'Ken Barclay',
               'title'     : 'Groovy',
               'publisher' : 'Elsevier',
               'isbn'      : '1234567890'
              ]
def engine = new SimpleTemplateEngine()
def template = engine.createTemplate(file)
def writeable = template.make(binding)

println writeable

结果:

<book>
    <author>Ken Barclay</author>
    <title>Groovy</title>
    <publisher>Elsevier</publisher>
    <isbn number="1234567890"/>
</book>

此外,也可以使用JSP的脚本语法,来实现绑定;而且,还可以实现模板文件与数据库的绑定。

28、服务器编程

        Groovy中有与Java servlet和JSP等效的Groolet和GSP。

附录:

1、消除[Module[groovy-nio]]警告信息

(1)          如何消除〖Module [groovy-nio]〗警告信息
                警告信息如下:
                D:\groovy\bin>groovy -v
                2014-11-23 14:55:14 org.codehaus.groovy.runtime.m12n.MetaInfExtensionModule newModule
                警告: Module [groovy-nio] - Unable to load extension class [org.codehaus.groovy.runtime.NioGroovyMethods]
                Groovy Version: 2.3.7 JVM: 1.6.0_45 Vendor: Sun Microsystems Inc. OS: Windows 8

方法一:    修改〖%JAVA_HOME%/jre/lib/logging.properties〗日志属性文件
                使用文本编辑器打开〖%JAVA_HOME%/jre/lib/logging.properties〗日志属性文件,
                将〖java.util.logging.ConsoleHandler.level〗默认值〖INFO〗改为〖SEVERE〗,即:
                〖java.util.logging.ConsoleHandler.level = SEVERE〗
                然后,在命令行窗口,输入〖groovy -v〗(或〖groovy --version〗)命令,显示结果如下:
                C:\Windows\System32>groovy -v
                Groovy Version: 2.3.7 JVM: 1.6.0_45 Vendor: Sun Microsystems Inc. OS: Windows 8

方法二:    删除〖%GROOVY_HOME%\lib\〗目录下的〖groovy-nio-2.3.7.jar〗文件


2、Groovy中访问数据库:

Groovy访问数据库,间接,方便。 需要提前将ojdbc5.jar放到groovy'的lib文件夹下面,或者确保你Groovy能去读该驱动,例如-cp  .;xxx.jar;  等等

参看:http://blog.csdn.net/russle/article/details/45248847













 








评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值