我是靠谱客的博主 背后黑米,最近开发中收集的这篇文章主要介绍gradle入门笔记Groovy快速入门gradle参考为什么要学习gradleGradle基础-Gradle构建机制-Gradle任务,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

Groovy快速入门

Groovy介绍

Groovy是一种基于JVM(Java虚拟机)的敏捷开发语言,它结合了Python、Ruby和Smalltalk的许多强大的特性,Groovy代码能够与Java代码很好地结合,也能用于扩展现有代码。由于其运行在 JVM 上的特性,Groovy也可以使用其它非Java语言编写的库

Groovy & Java & Kotlin

  • Groovy、Java及Kotlin都是基于JVM的开发语言
  • Groovy基于Java,在语法上基本相似,但也做了很多自己的扩展。Groovy除了可以面向对象编程,还可以用作纯粹的脚本语言,这一点和Kotlin是一样的
  • Goovy 和 Kotlin都有自己支持的DSL,两者有许多共通之处

Groovy特性

  • 同时支持静态类型和动态类型
  • 支持运算符重载
  • 支持DSL语法特性
  • 本地语法列表和关联数组
  • 各种标记语言,如XML和HTML原生支持
  • 对正则表达式的本地支持
  • Groovy和Java语法非常相似,可以无缝衔接使用Java
  • 支持使用现有的Java库,并做了一定的扩展

Hello, Groovy!

  • 编写Groovy,使用Java写法基本是没有问题的。但是Groovy有自己的特性,而且又是一种领域特定语言,会发现Groovy的写法就变得非常的灵活了
  • Groovy与Java写法上的一些差异
    • 分号可选,除非会导致语法歧义时才使用分号
    • return语句可选(默认返回最后一个语句)
    • 方法,属性默认都是public的
    • Groovy不强迫捕获异常,看使用者需求
    • ?.安全导航操作符的使用(kotlin借鉴了这个)
    • 闭包的使用结合DSL语法特性等等
// 1、以java的方式写groovy
class HelloGroovy{
    static void main(String[] args) {
        for(int i = 0; i< 3 ;i ++){
            println(test())
        }
    }

    // 会自动返回最后一个语句,类型会自动判别
    def test(){
        // return "hello world"
        "hello world"
    }
}

// 2、以脚本(kotlin)的方式写groovy
3.times {
    println("hello world")
}

Groovy基础语法

数据类型

  • 基本数据类型
    • byte、short、int、long、float、double、char、boolean
  • 包装类 (装箱拆箱)
    • String、Byte、Short、Integer、Long、Float、Double、Char、Boolean
  • 自动装箱
    • 因为Groovy具有动态类型特性,所以它从一开始就支持自动装箱。实际上,必要时Groovy会自动将基本类型视作对象

定义变量-动态类型

  • Java是一门静态类型的语言,但是也有自己的多态
  • 动态类型是一种更高级的多态
  • 动态类型放低了对类型的要求,使语言能够根据上下文来判定变量类型
  • 使用def关键字定义变量,不过已使用了final (不可变), priviate这样修饰符,def可以省略
class Two {
    // 自动变量
    def mile
    // 只读变量
    final year = 2027
    // def 可以省略
    private month = 12
}

def two = new Two(mile: 1000)
// 因为 year只读,下面这种方式报错
// def twotwo = new Two(mile: 1000, year: 1111)

// 获取自动装箱的类型
println two.mile.class
println two.mile.getClass()

结果
class java.lang.Integer
class java.lang.Integer

字符串

  • 单引号字符串是java.lang.String类型,同时不支持插值
  • 双引号字符串在没有使用插值表达式情况下是java.lang.String类型, 但如果有插值表达式使用的话,就是groovy.lang.GString类型
  • 三引号字符串表示多行的字符串。不必将字符串分割成几块,也不必用连接符或换行符转义字符来将字符串跨行
  • 字符串的使用
    • 单引号单个字符要表示char类型,需要使用as转换
    • ${…} 表达式进行插值,去掉花括号不引起歧义的话,可以去掉
    • 可以通过+=, -=操作符添加/减少字符 (会自动匹配)
def name = "carName"
def realName = "$name:AAAA"
println realName
println name.getClass()
println realName.getClass()

结果
carName:AAAA
class java.lang.String
class org.codehaus.groovy.runtime.GStringImpl
// 关于自动装箱

def method(Object obj){
    1
}

def method(String str){
    2
}

Object o = new Object()
println "method${method(o)} is invoked"

Object obj = "A" // 推断是String类型
println "method${method(obj)} is invoked"

结果
method1 is invoked
method2 is invoked

数组和列表

  • 数组和列表都是使用逗号分割列表的值,使用方括号括起来表示
  • Groovy中的数组和列可以随意转换
  • def定义的变量会自动推断 [ ] 类型是列表
  • Groovy列表是普通的JDK java.util.List,因为Groovy中没有定义自己的集合类
// 看起来是数组,实际是ArrayList类型
def array = [1, 2, 3]
println array.getClass()

// 下面这个才是数组类型
int[] array2 = [1, 2, 3]
println array2.getClass()

// 数组的另一个写法
def array3 = [1, 2, 3] as int[]
println array3.getClass()

结果
class java.util.ArrayList
class [I
class [I

数组的自动装箱

String[] strArray = ["1", 2, "3"]
println strArray.getClass()
println strArray[1].getClass()

结果
class [Ljava.lang.String;
class java.lang.String

范围

范围是一种特殊的列表,由序列中的第一个和最后一个值表示,Range可以是包含或排除。包含范围包括从第一个到最后一个的所有值,而独占范围包括除最后一个之外的所有值。也可以使用表达式来表示范围

  • 1…10 包含范围的示例
  • 1…<10 独占范围的示例 (开区间)
  • ‘a’…‘x’ 范围也可以由字符组成
  • 10…1 范围也可以按降序排列
  • ‘x’…‘a’ 范围也可以由字符组成并按降序排列。同步请求/异步请求实现
(1..3).forEach{
    println "hello world : " + it
    if(it == 2){
        println("2222")
    }
}

结果
hello world : 1
hello world : 2
2222
hello world : 3
class groovy.lang.IntRange

映射

  • 映射(也称为关联数组,字典,表和散列)是对象引用的无序集合
  • Map集合中的元素由键值访问
  • Map中使用的键可以是任何类,如果不能推断具体key类型,默认就是字符串

在Groovy中可以使用特定的表述方式来指定映射

  • [k1:v1,k2:v2] 具有键值对的集合
  • [:] 空映射
def map = [a: 11, b: 12]
map.forEach{k,v ->
    println "keyType=${k.class} key=$k, value=$v"
}

println "map['a'] = " + map['a']

结果
keyType=class java.lang.String key=a, value=11
keyType=class java.lang.String key=b, value=12
map['a'] = 11

运算符及控制语句

  • Groovy支持运算符重载
  • 循环语句
    • 除了和Java保持差不多的用法外,还支持结合范围的用来进行循环
    • 组合闭包来实现更简化的循环操作
  • 条件语句
    • 除了和Java保持差不多的用法外,还多了Groovy的一些扩展
    • 可以组合闭包实现更灵活的条件语句

Groovy稍微高级语法

Gradle中用到的Groovy最主要的语法

  • 闭包
  • 元编程(MOP编程)
Integer.metaClass.add1000{
    delegate+1000
}

println 250.add1000()

结果
1250

闭包,gradle中有很多这种写法

class Car{
    def miles
    private year = 2027

    def closureTest(def param1,  Closure closure){

    }
}

new Car().closureTest(1){
    
}

类与方法 - getter/setter

  • 默认会生成getter, setter方法
  • 并且可以直接像使用成员变量的方法来自动判断调用getter/setter
  • 当进行赋值时调用setter方法
  • 当直接访问值时调用的是getter方法
  • 使用’.@'才是真正的直接访问变量,跳过默认的getter/setter方法调用

groovy默认会为类中的变量生产getter/setter方法

在这里插入图片描述

car.miles = 2000
println car.miles

对变量的赋值和获取,默认是调用的getter/setter方法

Groovy对于private修饰的变量没有强制性

在这里插入图片描述

可以重写getter/setter方法(也可以看到privacy修饰符不起作用)

class Car{
    def miles
    private year = 2027

    def getMiles() {
        println "in getMiles"
        return miles
    }

    void setMiles(miles) {
        println "in setMiles"
        this.miles = miles
    }

    def getYear() {
        println "in getYear"
        return year
    }

    void setYear(year) {
        println "in setYear"
        this.year = year
    }
}

def car = new Car()
car.miles = 2000
println car.miles
car.year = 2028
println car.year

结果
in setMiles
in getMiles
2000
in setYear
in getYear
2028

Groovy中private不被限制

  • 所有的变量默认是public的
  • 如果要设置为私有禁止直接访问,仅申明private是不行的。依然可以使用’.'直接访问
  • 即使把这段代码放入到另外一个package下面也不行
  • 重载这个变量的getter/setter方法,并且在调用方法时抛出异常
  • Java中如果没有显式的指定访问修饰符(public、protected、private)那么默认是包访问权限,Groovy使用@PackageScope

如何让privacy修饰符起作用,通过抛出异常(groovy的语言不是为了安全,是为了便捷,解决这种没多大意义)

class Car{
    def miles
    private year = 2027

    def getMiles() {
        println "in getMiles"
        return miles
    }

    void setMiles(miles) {
        println "in setMiles"
        this.miles = miles
    }

    def getYear() {
        throw new IllegalAccessException("can not get")
        println "in getYear"
        return year
    }

    void setYear(year) {
        throw new IllegalAccessException("can not set")
        println "in setYear"
        this.year = year
    }
}

def car = new Car()
car.miles = 2000
println car.miles
car.year = 2028
println car.year

不通过getter/setter,直接操作变量

car.@mails = 2000

多种访问get/set方式

Car car = new Car()
car.'miles' = 10000

car['miles'] = 30000
println car.'miles'

def str = 'miles'
car."$str" = 20000

类-创建对象-使用具名参数的坑

构造方法

  • 构造方法重载规则跟Java一样
  • 但是要注意,如果没有指定具体参数的类型时,默认推断类型是Object
  • 在构造方法中传入具名参数,但是要注意:传入的参数都是键值对,实则就是一个Map类型
  • 这种方式传入的参数会自动拆解Map并且调用setter方法对应的进行赋值
  • 如果参数中还有非键值对的传参,就会把这些键值对当成Map了不会再进行自动拆解赋值。所以要有对应的构造方法才行

正常创建一个对象

class Car{
    def miles
    private year = 2027

    def getMiles() {
        println "in getMiles"
        return miles
    }

    void setMiles(miles) {
        println "in setMiles"
        this.miles = miles
    }

    def getYear() {
        println "in getYear"
        return year
    }

    void setYear(year) {
        println "in setYear"
        this.year = year
    }
}

def car = new Car(miles: 1000, year: 2028)

结果
in setMiles
in setYear

添加一个构造函数,只接收miles

class Car{
    def miles
    private year = 2027

    def getMiles() {
        println "in getMiles"
        return miles
    }

    void setMiles(miles) {
        println "in setMiles"
        this.miles = miles
    }

    def getYear() {
        println "in getYear"
        return year
    }

    void setYear(year) {
        println "in setYear"
        this.year = year
    }

    Car(miles){
        this.miles = miles
    }
}

def car = new Car(miles: 1000, year: 2028)
println car.miles
println car.miles.getClass()

结果
in getMiles
[miles:1000, year:2028]
in getMiles
class java.util.LinkedHashMap

1、可以看到,创建对象时,new Car中传递参数是当做map进行传递的

2、由于 下面构造方式没有指明参数类型,就把miles当做Object对象,接受了传递进来的map对象

Car(miles){
    this.miles = miles
}

如果想让只具名参数和只接收一个参数的构造方法生效

  • 需要创建一个空的构造方法
  • 一个参数的构造方法,必须指定类型,否则还是识别为接收map的object对象
class Car{
    def miles
    private year = 2027

    def getMiles() {
        println "in getMiles"
        return miles
    }

    void setMiles(miles) {
        println "in setMiles"
        this.miles = miles
    }

    def getYear() {
        println "in getYear"
        return year
    }

    void setYear(year) {
        println "in setYear"
        this.year = year
    }

    Car(){

    }
    Car(int miles){
        this.miles = miles
    }
}

def car = new Car(miles: 1000, year: 2028)
println car.miles
println car.miles.getClass()

def car2 = new Car(miles: 2000)
println car2.miles
println car2.miles.getClass()

结果
in setMiles
in setYear
in getMiles
1000
in getMiles
class java.lang.Integer
in setMiles
in getMiles
2000
in getMiles
class java.lang.Integer

普通类中方法不支持具名参数 - 神经病

class Car {
    def miles
    def year


    void execute(x = 1, y = 2, z=3) {
        println "$x $y $z"
    }

}
Car c = new Car()
c.execute(x: 1, y: 2, z: 3) // 不支持具名参数,会把传递进来的方法当做map
c.execute(x: 1, y: 2, z: 3, 2222, 3333) // 这个是可以的,正常的(传递了3个参数,一个是map,和两个整数)
c.execute(x: 1, y: 2, z: 3, 2222, c: 111, 3333, a: 111, b: 222) // 这个也是可以的,多有的键值对合并,成为第一个参数

// 下面的结果,xyz结果都是1,2,3 没有用到变量进行赋值
c.execute(x = 1, y = 2, z = 3)
c.execute(x = 1, z = 2, y = 3)

默认参数

class Car {
    def miles
    def year


    void execute(x = 1, y = 2, z=3) {
        println "$x $y $z"
    }
  
  // 这个会报错,这和kotlin一样
  void execute() {
        println "$x $y $z"
    }

}

Groovy闭包

Groovy中可以理解为闭包就是可执行的代码块,或匿名函数。闭包在使用上与函数与许多共通之处,但是闭包可以作为一个函数的参数

  • 默认情况下,闭包能接收一个参数,且参数字段默认使用it
  • 在箭头前面没有定义参数,这时候闭包不能传入参数
  • 可以定义多个接收的参数
  • 使用参数默认值,跟方法使用规则一样!

一个最简单的闭包

def closure = {
    // 这个里面和方法体一样
  	// 默认接收一个参数,参数直接使用it
}

// 有一个默认参数的闭包
def closure = {
    println it
}
closure("show this")

// 没有默认参数的闭包
def closure = {
    -> 
    println "not arguments"
}

closure()


// 定义多参数的闭包,支持默认值
def closure = {
    arg1="ccc", arg2 ->
    println "$arg1 and $arg2"
}

closure("aaa", "bbb")

闭包就是Closure对象

def abc = {

}

println abc.class

结果 - 动态运行时生成的
class com.study.class1.TwoThis$_run_closure2 

闭包可以作为方法的参数

def func(closure) {
   closure()
}

func {
   // 闭包执行的代码
   println "call"
}

柯里化闭包

  • 使用curry方法给闭包参数加默认值,生成的新的闭包,就等于设置了参数默认值的闭包
  • def curriedClosure = closure2.curry(“20”) // curry从左到右设置参数默认值

闭包与接口/类进行转换

  • 即使定义的方法传入的是闭包,但是如果传入的对象的类型也有call方法,那么,是可以执行这个对象的call方法的,实际上,闭包执行的也是call方法
  • 在一个对象上调用(),表示调用这个对象的call方法
    • def action = new Action()
    • action()
interface Action {
   void aaa()
}
def closure = {
   println "aaa"
}
Action action = closure
action.aaa()

println closure.class
println action.class // 动态代理对象
interface Action {
   void aaa()
   void bbb()
   void ccc()
}

def addTextWatcher(Action action) {
   action.aaa()
}
addTextWatcher({
   println "aaa"
} as Action)
// 当有接口有多个方法时,传入的闭包要强转为Action,使用as关键字
// 现在看起来就像是gradle中的内容了

闭包中的内容是通过Closure中的call方法进行调用

interface Action {
   void call()
}

def func(closure) {
   closure()
}

Action closure = new Action() {
   @Override
   void call() {
       println "call"
   }
} as Closure

func(closure)

类当中如果定义了call方法,可以直接使用对象()去调用call方法

class Action {
   void call(a) {
       println "$a"
   }
}

def a = new Action()
a(111)

Closure比较重要的成员变量

private Object delegate;
private Object owner; 
private Object thisObject;

// delegate、owner、thisObject 
// 在普通闭包中,都是定义这个普通闭包的对象
// 在静态闭包中,都是定义这个静态闭包的类

private int resolveStrategy = OWNER_FIRST;
protected Class[] parameterTypes; // 参数类型
protected int maximumNumberOfParameters; // 参数个数
def closure = {
   int a, int b ->
       println a.getClass()
       println b.getClass()
}
closure(1, 2)
println closure.parameterTypes
println closure.maximumNumberOfParameters

def closure2 = {
   println "this is" + this
   println "owner is" + owner
   println "delegate is" + delegate
}
closure2()
class Test {
   def closure2 = {
       println "this is" + this
       println "owner is" + owner
       println "delegate is" + delegate
   }
}

class Test {
   static def closure2 = {
       println "this is" + this
       println "owner is" + owner
       println "delegate is" + delegate
   }
}


class Test {
  
   def closure1 = {
       def closure2 = {
           // this:Test@2b76ff4e,定义它的时候的类的this,当它是static时就是class对象
           println "this is" + this
           // owner:Test$_closure1@8c3619e,定义它的时候的类的对象
           println "owner is" + owner
           // delegate:Test$_closure1@8c3619e,默认就是owner, 但是代理可以修改
           println "delegate is" + delegate
       }
       closure2()
   }
}

闭包中代理策略

  • delegate默认就是owner,在执行脚本中可以直接调用方法
  • 但是如果方法放入类中就不可以了,可以通过修改代理的方式,让它能够调用
  • 代理策略,默认的是选择owner
  • 闭包有以下代理策略
    • groovy.lang.Closure#DELEGATE_FIRST // delegate优先
    • groovy.lang.Closure#DELEGATE_ONLY // 只在delegate中找
    • groovy.lang.Closure#OWNER_FIRST // owner优先
    • groovy.lang.Closure#OWNER_ONLY // 只在owner找
    • groovy.lang.Closure#TO_SELF // 只在自身找(闭包内部),意义不大

在脚本中,func不在类中,下面的脚本可以直接访问

def func(){
	println "func"
}

def closure = {
	func()
}

closure()

当func在类中时,下面闭包访问就报错


class Test2 {
   def func() {
       println "Test2 func"
   }
}

def closure = {
   func()
}
closure()

修改代理,就能访问了


class Test2 {
   def func() {
       println "Test2 func"
   }
}

def closure = {
   func()
}

closure.delegate = new Test2()
closure()

但是下面的代码结果是Script func

class Test2 {
   def func() {
       println "Test2 func"
   }
}

def func() {
   println "Script func"
}

def closure = {
   func()
}

closure.delegate = new Test2()
closure()

修改代理,修改策略

class Test2 {
   def func() {
       println "Test2 func"
   }
}

def func() {
   println "Script func"
}
def closure = {
   func()
}
closure()

closure.delegate = new Test2()// 优先选择的是owner
//* @see groovy.lang.Closure#DELEGATE_FIRST
//* @see groovy.lang.Closure#DELEGATE_ONLY
//* @see groovy.lang.Closure#OWNER_FIRST
//* @see groovy.lang.Closure#OWNER_ONLY
//* @see groovy.lang.Closure#TO_SELF
closure.resolveStrategy=Closure.DELEGATE_ONLY
closure()

动态/静态类型语言

  • 动态类型语言:动态类型语言是指在运行期间才去做数据类型检查的语言,也就是说,在用动态类型的语言编程时,可以不用给变量指定数据类型,该语言会在你第一次赋值给变量时,在内部将数据类型记录下来。Python和Ruby这些就是一种典型的动态类型语言
  • 静态类型语言:静态类型语言与动态类型语言刚好相反,它的数据类型是在编译其间检查的,也就是说在写程序时要声明所有变量的数据类型,C/C++是静态类型语言的典型代表,其他的静态类型语言还有C#、Java等

groovy的动态特性

class User {
    def username = 'zee'
    String age

    void setName(String name) {
        println "setName(String name)"
        this.username = name
    }

    void setName(Object name) {
        println "setName(Object name)"
        this.username = name
    }
}

def user = new User()
println user.username.class

user.username = new Object()
println user.username.class

user.username = 123
println user.username.class

user.username = new User()
println user.username.class




def user1 = new User()
Object name = "zee"
println name.class

user1.setName(name)
name = 123
println name.class
user1.setName(name)

def user3 = new User()
user3.age = "123"
user3.age = 123 // groovy是强类型的,动态的,这里int可以转成 String
println user3.age

结果
class java.lang.String
class java.lang.Object
class java.lang.Integer
class com.study.class1.User
class java.lang.String
setName(String name)
class java.lang.Integer
setName(Object name)
123

groovy不会进行安全检查,在运行时候才抛出异常

class Person1 {
    def dream() {
        println "I have a dream!"
    }
}

class Person2 {
    def dream() {
        println "I have a dream!"
    }
}

def func(person){
    person.dream()
}

def person1 = new Person1()
def person2 = new Person2()
func(person1)
func(person2)

使用@TypeChecked强制编译检查,但是会损失动态特性

class Person1 {
    def dream() {
        println "I have a dream!"
    }
}

class Person2 {
    def dream() {
        println "I have a dream!"
    }
}

@TypeChecked
def func(Person1 person) { // 这里必须指定类型
    person.dream()
}

def person1 = new Person1()
def person2 = new Person2()

func(person1)
func(person2)


@TypeChecked
class Test {

    @TypeChecked(TypeCheckingMode.SKIP) // 跳过检查
    def func(person) {
        person.dream()
    }
}

动态特性及元编程

动态特性

  • 根据上下文推断具体类型
    • def file = new File(“”);
    • file.getParent()
  • CallSite动态调用节点
  • java中要使用继承才能实现多态,而Groovy轻而易举
    • 优势:灵活
    • 缺点:编译时不会检查类型,运行时报错

元编程

  • Java中可以通过反射,可以在运行时动态的获取类的属性,方法等信息,然后反射调用。但是没法直接做到往内中注入变量、方法;不过Java也有动态字节码技术:ASM,JVM TI,javassist等等
  • MOP(元对象协议):Meta Object Protocol
  • Groovy直接可以使用MOP进行元编程,我们可以基于应用当前的状态,动态的添加或者改变类的方法和行为。比如在某个Groovy类中并没有实现某个方法,这个方法的具体操作由服务器来控制,使用元编程,为这个类动态添加方法,或者替换原来的实现,然后可以进行调用

MOP方法拦截

  • 实现GroovyInterceptable接口,重写invokeMethod来进行拦截
  • 使用MetaClass拦截方法,覆盖invokeMethod方法
    • 使用类的MetaClass,针对的是class对象,所有实例都会被影响
    • 使用具体实例的MetaClass,只影响当前对象实例
class Person implements GroovyInterceptable {

    def func() {
        System.out.println "I have a dream!"
    }

    @Override
    Object invokeMethod(String name, Object args) {
        //println "invokeMethod"
        System.out.println "$name invokeMethod"
        //respondsTo(name) //判断方法是否存在
        if (metaClass.invokeMethod(this, 'respondsTo', name, args)) {
            System.out.println "$name 方法存在"
            System.out.println "$name 执行前.."
            metaClass.invokeMethod(this, name, args)
            System.out.println "$name 执行后.."
        }
    }
}

new Person().func()


结果
func invokeMethod
func 方法存在
func 执行前..
I have a dream!
func 执行后..

这里拦截某个对象的某一个方法

class Person3 {

    def func() {
        System.out.println "I have a dream!"
    }
}

def person3 = new Person3()
// 这里拦截某个对象的某一个方法
person3.metaClass.func = {
   println "I have a new dream !!!"
}
person3.func()

// 拦截所有的方法
person3.metaClass.invokeMethod = {
    String name, Object args ->// invokeMethod(String name, Object args)
        println "$name 被拦截"
}
person3.func()

拦截所有实例的方法

Person3.metaClass.invokeMethod = {
    String name, Object args ->// invokeMethod(String name, Object args)
        println "$name 被拦截"
}

对内置String进行拦截

String.metaClass.invokeMethod = {
    String name, Object args ->
        println "String.metaClass.invokeMethod"
        MetaMethod method = delegate.metaClass.getMetaMethod(name)
        if (method != null && name == 'toString') {
            "showme"
        }
}
println "jason".toString()

MOP方法注入

方法注入:编写代码时知道想要添加到一个或多个类中的方法的名字。利用方法注入,可以动态地向类中添加行为。也可以向任意数目的类中注入一组实现某一特定功能的可服用方法,就像工具函数。有以下几种方式

  • 使用分类注入方法
  • 使用ExpandoMetaClass注入方法
    • 直接使用类或实例的MetaClass注入方法,实际上最终操作的类型是ExpandoMetaClass。手动创建ExpandoMetaClass来进行注入
  • 使用Mixin注入方法。( 在类中可以使用多个Mixin )

使用MetaClass来注入方法

class Person4 {
    def func() {
        System.out.println "I have a dream!"
    }
}

println Person4.metaClass // 注入前:org.codehaus.groovy.runtime.HandleMetaClass

Person4.metaClass.newFunc = {
   println "newFunc调用"
}
println Person4.metaClass // 注入后:groovy.lang.ExpandoMetaClass

使用groovy.lang.ExpandoMetaClass来注入方法

def emc = new ExpandoMetaClass(Person4)
emc.func2 = {
   println "func2"
}
emc.initialize()
Person4.metaClass = emc
new Person4().func2()

使用分类注入方法 - 场景,框架工具类

class StringUtils {

//    public static String isEmpty(){
//
//    }

   static def isNotEmpty(String self) {
       println "isNotEmpty"
       self != null && self.length() > 0
   }

}

class StringUtils2 {

   static def isNotEmpty(Integer self) {
       println "isNotEmpty2"
       //self != null && self.length() > 0
   }
}

use(StringUtils, StringUtils2) { // 从后往前找
   println "".isNotEmpty()
   println 1.isNotEmpty()
}

其他写法

@Category(String)
class StringUtils2 {
   def isNotEmpty() {
       println "isNotEmpty2"
       this != null && this.length() > 0
   }
}

use(StringUtils2) {
   println "".isNotEmpty()
}

操作不存在的变量、方法

class Person5 {
   def username

   // 对不存在的变量进行get操作
   def propertyMissing(String name) {
       println "propertyMissing"
       if (name == 'age') {
           "19" // 返回默认值
       }
   }

   // 对不存在的变量进行set操作
   def propertyMissing(String name, def arg) {
       println "propertyMissing ${name} : arg${arg}"
       return "default"// 给与返回值
   }

   def methodMissing(String name, def arg) {
       println "$name methodMissing"
       if (name.startsWith 'getFather') {
           "zzzz"
       }
   }

}

def p = new Person5()
println p.age = 12
println p.age
println p.getFather()

运算符重载

运算符重载方法列表

a + b         a.plus(b)
a – b         a.minus(b)
a * b         a.multiply(b)
a ** b        a.power(b)
a / b         a.div(b)
a % b         a.mod(b)
a | b         a.or(b)
a & b         a.and(b)
a ^ b         a.xor(b)
a++ or ++a    a.next()
a– or –a      a.previous()
a[b]          a.getAt(b)
a[b] = c      a.putAt(b, c)
a << b        a.leftShift(b)
a >> b        a.rightShift(b)
~a            a.bitwiseNegate()
-a            a.negative()
+a            a.positive()

switch(a) { case(b) : }
b.isCase(a)

a == b
a.equals(b) or a.compareTo(b) == 0

a != b
! a.equals(b)

a <=> b
a.compareTo(b)

a > b
a.compareTo(b) > 0

a >= b
a.compareTo(b) >= 0

a < b
a.compareTo(b) < 0

a <= b
a.compareTo(b) <= 0
class TestA {
   def plus(TestA newTestA) {
       // 处理业务
   }
}
new TestA() + new TestA()

Integer.metaClass.plus = {
   int a ->
       "$delegate + $a"
}
println 1 + 2

Expando动态生成类

def e = new Expando(name: "Aaa", func: {
   println "func"
})

e.a = 123
println e.a

gradle参考

https://blog.csdn.net/nihaomabmt/article/details/112308851

为什么要学习gradle

gradle自身介绍

  • Gradle是一款基于Apache的Ant和Maven概念的项目自动化开源构建工具。Gradle的核心是基于Java来实现的,可以把Gradle看成就是一个轻量级的Java应用程序
  • Gradle使用Groovy、Kotlin等语言编写自定义脚本,取代了Ant和Maven使用xml配置文件的方式,很大程度简化了开发时对项目构建要做的配置,使用更加灵活和强大

学习gradle的目的

  • Gradle是目前Andorid最主流的构建工具
  • 不少技术领域如组件化、插件化、热修复,及构建系统 (很多优秀的框架都是在编译时或者打包之前做一些特殊的处理) ,都需要通过Gradle来实现
  • 不懂Gradle将无法完成上述事情,所以学习Gradle非常必要

关于项目构建

  • 对于Java应用程序,编写好的Java代码需要编译成.class文件才能够执行。所以任何的Java应用开发,最终都需要经过这一步
  • 编译好了这些class文件,还需要对其进行打包。打包不仅针对于这些class文件,还有所有的资源文件等。比如web工程打包成jar包或者war包就包含了自己的资源文件,然后放到服务器上运行
  • Andorid工程编译好的class文件还要被打包到dex包中,并且所有的资源文件进行合并处理,甚至还需要对最终打包出来的文件进行加密和签名处理等等

Android build

在这里插入图片描述

gradle使用安卓插件编译android项目

自动化构建工具的发展

  • “石器时代”:自己编写命令脚本,进行编译和打包
  • “蒸汽时代”:Make、Ant工具的出现
  • “电气时代”:Maven
  • “信息时代”:Gradle,更高级的自动构建工具出现

gradle提供了什么

  • 对多工程的构建支持非常出色,尤其是工程依赖问题,并支持局部构建
  • 多种方式的依赖管理:如远程Maven仓库,nexus私服,ivy仓库或者本地文件系统等
  • 支持传递性依赖管理
  • 轻松迁移项目工程
  • 基于Groovy等语言构建脚本,简便且灵活
  • 免费开源,并且整体设计是以作为一种语言为导向的,而非成为一个严格死板的框架

Gradle基础-Gradle构建机制-Gradle任务

win 10 下环境搭建

获取gradle

  • 从官网下载 https://gradle.org/releases/
  • 如果已经使用过android studio编译过项目,可使用本地用户文件夹下的gradle工具包 C:Usersshowme.gradlewrapperdists

这里使用本地缓存的gradle工具包

C:Usersshowme.gradlewrapperdistsgradle-7.4.2-bin48ivgl02cpt2ed3fh9dbalvx8gradle-7.4.2

系统属性配置

添加 GRADLE_HOME C:Usersshowme.gradlewrapperdistsgradle-7.4.2-bin48ivgl02cpt2ed3fh9dbalvx8gradle-7.4.2

添加 Path %GRADLE_HOME%bin

通过命令行终端输入gradle --version 查看是否配置成功

PS C:Usersshowme> gradle --version

------------------------------------------------------------
Gradle 7.4.2
------------------------------------------------------------

Build time:   2022-03-31 15:25:29 UTC
Revision:     540473b8118064efcc264694cbcaa4b677f61041

Kotlin:       1.5.31
Groovy:       3.0.9
Ant:          Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM:          17.0.2 (Oracle Corporation 17.0.2+8-LTS-86)
OS:           Windows 10 10.0 amd64

Hello Gradle

在工程文件夹下,创建一个build.gradle文件,包含内容

println 'Hello, Gradle!'

build.gradle是构建Project的核心文件,也是入口

  • 如果没有该文件,会出现not found in root project ‘xxxxx’ 提示异常
  • 必须要有一个可以运行的task,运行后自动生成.gradle文件夹下的内容

打开cmd终端,移动到工程目录下,执行命令:> gradle help

  • help是gradle的内置任务
$ gradle help                                                                                                                            [0:31:04]
Starting a Gradle Daemon (subsequent builds will be faster)

> Configure project :
Hello, Gradle!

> Task :help

Welcome to Gradle 7.4.2.

To run a build, run gradle <task> ...

To see a list of available tasks, run gradle tasks

To see more detail about a task, run gradle help --task <task>

To see a list of command-line options, run gradle --help

For more detail on using Gradle, see https://docs.gradle.org/7.4.2/userguide/command_line_interface.html

For troubleshooting, visit https://help.gradle.org

从输出可以看到

  • 生成了一个Gradle Daemon
  • 执行了gradle脚本中的输出语句
  • 打印了帮助信息

在看一下,目录下当前没有生产额外的文件

showme@showmedeMBP: ~/workspace/tmmmp/hello_gradle
$ tree .                                                                                                                                 [0:34:09]
.
└── build.gradle

0 directories, 1 file

Gradle Wrapper

  • Gradle Wrapper用来配置开发过程中用到的Gradle构建工具版本。避免因为Gradle不统一带来的不必要的问题
  • 在工程目录下使用cmd命令生成wrapper:> gradle wrapper
  • 标准的gradle工程目录
    • gradlew和gradlew.bat分别是Linux和Windows下的可执行脚本
    • 具体业务逻辑是在/gradle/wrapper/gradle-wrapper.jar中实现,gradlew最终还是使用Java执行这个jar包来执行相关的Gradle操作的
showme@showmedeMBP: ~/workspace/tmmmp/hello_gradle
$ gradle wrapper                                                                                                                         [0:37:24]

> Configure project :
Hello, Gradle!

BUILD SUCCESSFUL in 696ms
1 actionable task: 1 executed

showme@showmedeMBP: ~/workspace/tmmmp/hello_gradle
$ tree .                                                                                                                                 [0:37:31]
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

2 directories, 5 files

gradle/wrapper/gradle-wrapper.properties文件的目的:在不同的机器上快速部署gradle执行环境

distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https://services.gradle.org/distributions/gradle-7.4.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
  • distributionBase 下载的Gradle压缩包解压后存储的主目录

  • zipStorePath | distributionPath: 本地缓存的gradle可执行文件路径, 默认在 ~/.gradle/wrapper/dists

  • distributionUrl 下载特定gradle版本

    • https://services.gradle.org/distributions/gradle-7.4.2-bin.zip

    • https://services.gradle.org/distributions/gradle-7.4.2-all.zip

    • bin和all的区别,bin只包含了可执行文件相关的内容,all还包含了gradle的源码和帮助文档

GRADLE_USER_HOME

  • 默认路径在~/.gradle/ ,不建议使用本地maven的m2替代,因为原本的.gradle目录下的模块分的很清晰,功能明确
  • 如果启动时,指定参数,使用别的目录代替GradleUserHome ,后果是每次构建需要重新下载插件与依赖到新的目录
  • 默认情况下,gradle运行时,除了和项目打交道,还有当前项目构建的全新的GradleUserHome目录,所有jar包都得重新下载

gradlew命令行

  • gradlew -?/-h/-help 使用帮助
  • gradlew tasks 查看所有可执行Tasks
  • gradlew --refresh-dependencies assemble 强制刷新依赖
  • gradlew cBC 等价与执行Task cleanBuildCache,这种通过缩写名快速执行任务
  • gradlew :app:dependencies 查找app工程依赖树

在这里插入图片描述

Gradle构建机制

gradle标准工程

showme@showmedeMBP: ~/workspace/tmmmp/hello_gradle
$ tree .                                                                                                                                 [0:37:31]
.
├── build.gradle
├── gradle
│   └── wrapper
│       ├── gradle-wrapper.jar
│       └── gradle-wrapper.properties
├── gradlew
└── gradlew.bat

java中的标准工程

在这里插入图片描述

安卓中的标准工程:有一个app模块,在这个模块中包含也包含了一个build.gradle

  • 最外部的build.gradle适用于整个工程
  • app模块中的build.gradle适用于当前模块

在这里插入图片描述

Gradle DSL

  • DSL(Domain Specific Language) 领域特定语言,或领域专属语言。简单来说就是专门关注某一领域的语言,它在于专而不是全,最典型的比如HTML
  • Gradle可以使用Groovy DSL,专门用来开发Gradle的构建脚本。所以说Gradle整体设计是以作为一种语言为导向的,并非成为一个严格死板的框架

App->build.gradle文件 , 使用的就是groovy 语言

plugins {
    id 'com.android.application'
}

android {
    compileSdk 32

    defaultConfig {
        applicationId "com.example.studygradle"
        minSdk 26
        targetSdk 32
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
}

dependencies {

    implementation 'androidx.appcompat:appcompat:1.3.0'
    implementation 'com.google.android.material:material:1.4.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.0.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.3'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

settings.gradle

  • Gradle支持多工程构建,使用settings.gradle来配置添加子工程(模块)
  • settings文件在初始化阶段执行,创建Settings对象,在执行脚本时调用该对象的方法
  • Settings.include(String… projectPaths)
    • 将给定的目录添加到项目构建中,‘:app’表示文件相对路径,相当于’./app’文件夹
    • 多项目架构进行分层,把同层次的子工程放在同一文件夹下便于管理,使用’:xxx:yyy’表示

在android studio中新增java模块(多层次的模块写法 - 这个冒号就相当于文件夹的斜杆

在这里插入图片描述

可以看到setting.gradle通过include命令引入的模块,已经分层方式

模块不include进来,则该模块不会进行构建

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PPnqRKCX-1657964865416)(/Users/showme/我的云端硬盘/核心/android/android编译时技术/基础03-gradle/01-gradle基础-gradle构建机制-gradle任务/01-gradle基础-gradle构建机制-gradle任务.assets/image-20220630011124298.png)]

可以点进去看include细节,发现是gradle构建工具的一个方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7EKl0pQR-1657964865417)(/Users/showme/我的云端硬盘/核心/android/android编译时技术/基础03-gradle/01-gradle基础-gradle构建机制-gradle任务/01-gradle基础-gradle构建机制-gradle任务.assets/image-20220630011551817.png)]

rootProject.name 配置的是整个工程最顶层的名字

在这里插入图片描述

build.gradle

  • build.gradle是项目构建文件,每个工程都有一个build.gradle文件
  • build.gradle在配置阶段执行,并创建相应工程的Project对象,执行的代码可以直接调用该对象提供的方法或属性
  • 最顶层的build.gradle适用于整个项目,每个模块中的build.gradle适用于模块自身

Daemon(守护进程)

  • 项目启动时,会开启一个client,然后启动一个daemon,通过client向daemon收发请求,项目关闭,client关闭,daemon保持启动,有类似项目再次部署时,会直接通过新的client访问已经启动的daemon,所以速度很快,默认daemon不使用3小时后关闭;不同项目兼容性考虑,也可使用–no-daemon 启动项目,就没有速度优势了
  • 手动停止daemon:gradle wrapper --stop

Gradle生命周期

  • Initialization
    • Gradle支持单项目和多项目构建。在初始化阶段,Gradle确定哪些项目将参与构建,并为每个项目创建Project实例,一般我们不会接触到它。(比如解析settings.gradle)
  • Configuration
    • 配置阶段,解析每个工程的build.gradle文件,创建要执行的任务子集和确定各种任务之间的关系,并对任务的做一些初始化配置
  • Execution
    • 运行阶段,Gradle根据配置阶段创建和配置的要执行的任务子集,执行任务

在这里插入图片描述

配置阶段

  • 解析每个Project中的build.gradle,解析过程中并不会执行各个build.gradle中的task
  • 经过Configration阶段,Project之间及内部Task之间的关系就确定了。一个Project包含很多Task,每个Task之间有依赖关系。Configuration会建立一个有向图来描述Task之间的依赖关系,所有Project配置完成后,会有一个回调project.afterEvaluate(),表示所有的模块都已经配置完了

task

  • task是gardle中最小的任务单元,任务之间可以进行复杂的操作(如动态创建任务,多任务间依赖调用等等)。gradle的执行其实就是由各种任务组合执行,来对项目进行构建的
  • 使用gradlew help命令,任何gradle项目都有一个该task,可以执行此命令观察taks执行的流程是否如预期
  • 可以使用工具查看,还可以通过 gradlew tasks 命令查看可运行任务
    • 使用gradlew tasks --all 命令查看所有任务
    • 使用gradlew A B 命令表示执行任务A和B,支持驼峰简写

在build.gradle中自定义任务

  • task <任务名>{ … },在Gradle5.x以上已经删除<<操作符这种写法
  • { … }执行的是配置阶段的代码,执行阶段要处理的逻辑需要调用doFirst、doLast方法,在闭包中实现。doFirst{}表示任务执行开始时调用的方法,doLast{}表示任务执行结束调用的方法
  • task A(dependsOn:[B]){ … } 表示任务A依赖于任务B,那么B执行在A之前
  • 自定义的任务默认分组到other中

自定义task类

// 通过自定义任务类来实现自定义任务
class MyTask extends DefaultTask {

    @Input
    String filePath // 可以指定filePath来传入n个文件的路径

    @OutputFile
    File file

    MyTask() {
        group "自定义任务"
        description "自定义任务描述"
    }

    @TaskAction
    void action() {
        println "MyTask action()"
    }

    @TaskAction
    void action2() {
        println "MyTask action2()"
    }

}

// 继承自定义的task类,并创建新的task
task myTask2(type: MyTask) {
    filePath = 'xxx/xxx'
    file = file('test.txt')
}


// 继承已有的task
task clean(type: Delete) {
    delete rootProject.buildDir
}

task之间的集成和依赖关系

// Gradle3.0 << ,在4.x以上就废弃了
task A {
    println "configuration A.."

    doFirst {
        println "doFirst A.."
    }
   doLast {
       println "doLast A1.."
   }
   doLast {
       println "doLast A2.."
   }
   doLast {
       println "doLast A3.."
   }
}

A.doLast {
   println "doLast A4.."
}

task C {
   println "configuration C.."

   doLast {
       println "doLast C.."
   }
}
task B {
   println "configuration B.."

   doLast {
       println "doLast B.."
   }
}

task hello2(dependsOn: [A, C, B]) {
   doLast {
       println "doLast hello2"
   }
}

A.dependsOn B
A.mustRunAfter(B)
A.shouldRunAfter B

task finalized {
   doLast {
       println "清理任务"
   }
}

A.finalizedBy finalized

依赖其他build.gradle文件
1)创建一个gradle文件
config.gradle

task CCCC {
    doLast {
        println "doLast.."
    }
}

2)依赖创建的其他gradle文件

apply from: 'config.gradle'

DefaultTask

  • task定义的任务其实就是DefaultTask的一种具体实现类的对象
  • 可以使用自定义类继承DeaflutTask
    • 在方法上使用@TaskAction注解,表示任务运行时调用的方法
    • 使用@Input表示对任务的输入参数
    • 使用@OutputFile表示任务输出文件
    • 使用inputs,outputs直接设置任务输入/输出项
    • 一个任务的输出项可以作为另一个任务的输入项 (隐式依赖关系)
class ZipTask extends DefaultTask {

//    @Input
//    @Optional
    // 表示可选
    String from

//    @OutputFile
//    @Optional
    // 表示可选
    File out

    ZipTask() {
        outputs.upToDateWhen {
            false//  增量构建,每次都会开启,不会跳过任务
        }
    }

    @TaskAction
    void fun() {
        println " @TaskAction fun()"
        println from
        println out

        //文件进行操作
        //inputs.files.first()
        println inputs.files.singleFile
        def inFile = inputs.files.singleFile

        def file = outputs.files.singleFile
        file.createNewFile()
        file.text = inFile.text
    }
}

task myTask(type: ZipTask) {
    from = "a/b/c" // 输入
    out = file("test.txt") // 输出
    inputs.file file('build.gradle')
    outputs.file file('test.txt')
}

使用自定义任务,实现zip打包packageDebug输出的内容

task zip(type: Zip) {
    archiveName "outputs.zip"// 输出的文件名字
    destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
    from "${buildDir}/outputs"// 输入的文件
}

压缩packageDebug任务的输出文件

afterEvaluate {

    println tasks.getByName("packageDebug")

    task zip(type: Zip) {
        archiveName "outputs2.zip"// 输出的文件名字
        destinationDir file("${buildDir}/custom")// 输出的文件存放的文件夹
        from tasks.getByName("packageDebug").outputs.files// 输入的文件
        tasks.getByName("packageDebug").outputs.files.each {
            println it
        }
    }
}

gradle钩子函数

  • gradle在生命周期三个阶段都设置了相应的钩子函数调用
  • 使用钩子函数,处理自定义的构建
    • 初始化阶段:gradle.settingsEvaluated和gradle.projectsLoaded。(在settings.gradle中生效)
    • 配置阶段:project.beforeEvaluate和project.afterEvaluate;gradle.beforeProject、gradle.afterProject及gradle.taskGraph.taskGraph.whenReady
    • 执行阶段:gradle.taskGraph.beforeTask和gradle.taskGraph.afterTask

gradle构建监听

  • gradle可以设置监听,对各阶段都有相应的回调处理
    • gradle.addProjectEvaluationListener
    • gradle.addBuildListener
    • gradle.addListener:TaskExecutionGraphListener (任务执行图监听),TaskExecutionListener(任务执行监听),TaskExecutionListener、TaskActionListener、StandardOutputListener …

Daemon(守护进程)

  • 项目启动时,会开启一个client,然后启动一个daemon,通过client向daemon收发请求,项目关闭,client关闭,daemon保持启动,有类似项目再次部署时,会直接通过新的client访问已经启动的daemon,所以速度很快,默认daemon不使用3小时后关闭;不同项目兼容性考虑,也可使用–no-daemon 启动项目,就没有速度优势了
  • 手动停止daemon:gradle wrapper --stop

Project

  • build.gradle在配置阶段会生成project实例,在build.gradle中直接调用方法或属性,实则是调用当前工程project对象的方法或属性
  • 使用Project提供的Api,在多项目构建设置游刃有余
    • project(‘:app’){} 指定的project (这里是app) 配置
    • allprojects{} 所有的project配置
    • subprojects{} 所有的子project配置
    • buildscript {} 此项目配置构建脚本类路径

在根build.gradle文件中,给app模块进行配置

project(':app') {
//    plugins {
//        id 'com.android.application'
//        id 'kotlin-android'
//    }

    apply plugin: 'com.android.application'
    apply plugin: 'kotlin-android'

    android {
        compileSdkVersion 30
        buildToolsVersion "30.0.2"

        defaultConfig {
            applicationId "com.dongnaoedu.gradledemo"
            minSdkVersion 28
            targetSdkVersion 30
            versionCode 1
            versionName "1.0"

            testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        }

        buildTypes {
            release {
                minifyEnabled false
                proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
            }
        }
        compileOptions {
            sourceCompatibility JavaVersion.VERSION_1_8
            targetCompatibility JavaVersion.VERSION_1_8
        }
        kotlinOptions {
            jvmTarget = '1.8'
        }
    }

    dependencies {
        implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
        implementation 'androidx.core:core-ktx:1.2.0'
        implementation 'androidx.appcompat:appcompat:1.1.0'
        implementation 'com.google.android.material:material:1.1.0'
        implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
        testImplementation 'junit:junit:4.+'
        androidTestImplementation 'androidx.test.ext:junit:1.1.1'
        androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
    }

}

最后

以上就是背后黑米为你收集整理的gradle入门笔记Groovy快速入门gradle参考为什么要学习gradleGradle基础-Gradle构建机制-Gradle任务的全部内容,希望文章能够帮你解决gradle入门笔记Groovy快速入门gradle参考为什么要学习gradleGradle基础-Gradle构建机制-Gradle任务所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(41)

评论列表共有 0 条评论

立即
投稿
返回
顶部