Android Gradle - Groovy dynamic features and metaprogramming

Keywords: Java Android Gradle

preface

In the last article, I mainly explained Detailed explanation of Groovy classes, methods and closures, In this article, we will explain Groovy's dynamic features and metaprogramming in detail.

1. Groovy dynamic features

Speaking of dynamic characteristics, in the first article Understand Gradle and automated build In, it is just a brief introduction without detailed explanation. Here we should start with the difference between dynamic language and static language:

  • Dynamically typed language
    Dynamic type language refers to the language that does data type checking only during operation, that is, when programming with dynamic type language, you don't need to specify the data type for the variable. The language will record the data type internally when you assign a value to the variable for the first time. Python and Ruby are typical dynamically typed languages

  • Statically typed language
    Static type language is just the opposite of dynamic type language. Its data types are checked during compilation, that is, the data types of all variables should be declared when writing programs. C/C + + is a typical representative of static type language. Other static type languages include c#, Java, etc.

Various types of languages

After a lot of theories are finished, let's go straight to the happy code rolling session

Let's first look at the familiar java code

public class JavaTest2 {
    static class User {
        String userName = "hqk";
        int age;
        void setName(String name) {
            System.out.println("setName(String name)");
            this.userName = name;
        }
        void setName(Object name) {
            System.out.println("setName(Object name)");
            this.userName = name.toString();
        }
    }
    public static void main(String[] args) {
        User user=new User();
        user.setName(123);
        user.setName("zzz");
    }
}

This is a very simple piece of java code. We can see that two different types of data sources are passed in at the function entry. In order to ensure that the code does not report errors, the method setName is overloaded, but the final userName attribute type is String. In other words, when a variable is defined (before running), its type has been determined. If it is not satisfied, the compilation will not let you pass.

Then continue to look at Groovy we are learning now.

class User {
    def userName = 'hqk'
    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 "Before assignment"
println user.userName.class
println "\n"

println "Assign as object type"
user.userName = new Object()
println user.userName.class
println "\n"

println "Assign as int type"
user.userName = 123
println user.userName.class
println "\n"

println "Assign as User type"
user.userName = new User()
println user.userName.class
println "\n"

We can see that the same logic is also implemented here in Groovy. After different values are assigned, the type of the variable is printed in turn. In this way, there is no error message when writing the code. Run to see the effect.

Before assignment
class java.lang.String


Assign as object type
class java.lang.Object


Assign as int type
class java.lang.Integer


Assign as User type
class com.zee.gradle.groovy.User

Because Groovy can be directly assigned through the object. Variable, it is not assigned through the defined setName method for the time being. However, it can be seen that the userName variable defined here through def will change with the change of value type, and it is dynamic. Here is the result of direct assignment. Let's call the setName method just like the java method.

println "The following will be passed setName Method assignment\n"
Object name = "hqk"
println "definition Object Variable with value String"
println name.class
user.setName(name)
println user.userName.class

println "\n"

println "definition Object Variable with value int"
name = 123
println name.class
user.setName(name)
println user.userName.class

Here, a variable name of Object type is defined. First, the value of String type is assigned, and the corresponding value is assigned through the setName method; Then assign the variable name of Object type to the value of int type, and assign the corresponding value again through the setName method. Let's see how it works.

The following will be passed setName Method assignment

definition Object Variable with value String
class java.lang.String
setName(String name)
class java.lang.String


definition Object Variable with value int
class java.lang.Integer
setName(Object name)
class java.lang.Integer

Now we see that the value of a Object type is changed when the different values are assigned, and then different setName methods are invoked.

Now the question arises. Since variables can be changed at will in Groovy, is Groovy, like JS, a weakly typed variable? With this problem in mind, let's look at Groovy code again.

class User {
    def userName = 'hqk'
    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()
user.age = "123"
println user.age.class

user.age = 123
println user.age.class

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

Here, I define the age variable in Class, and the type is fully confirmed as String type. However, when assigning values below, I assign values of different types, and finally print the assigned variable type. Next, let's see the operation effect.

class java.lang.String
class java.lang.String
class java.lang.String

Here we can see that when the type is determined, no matter how the assignment type changes, it will not change. This shows that Groovy is a strongly typed variable. When a variable is defined by def and Object, its own type will be automatically changed to the type of the corresponding assignment Object, not a weakly typed variable.

Here's a summary of Groovy's dynamic features:

  • Use def/Object to define variables, and the type is determined by the type class assigned to it at run time.
  • You can use MOP for metaprogramming

MOP metaprogramming is mentioned here, so it is necessary to explain MOP metaprogramming in detail.

2. Groovy metaprogramming

Speaking of metaprogramming, let's talk about what pure Java can do:

In Java, you can dynamically obtain class properties, methods and other information at runtime through reflection, and then reflect the call. But you can't add attributes, methods, and behaviors directly into it. (dynamic bytecode technologies such as ASM and javassist are needed to dynamically modify classes)

In Groovy, we can directly use MOP for meta programming. We can dynamically add or change class methods and behaviors based on the current state of the application. For example, a method is not implemented in a Groovy class. The specific operation of this method is controlled by the server. Use metaprogramming to dynamically add methods for this class, or replace the original implementation, and then call it.

In a word, Groovy can do what Java can do, but Java can't do what Groovy can do. What Groovy can do is dynamic modification and dynamic injection. Dynamic modification involves interception. So let's explain it in detail from the two aspects of Groovy interception and injection.

2.1 Groovy interception

2.1.1 interception through GroovyInterceptable

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) / / judge whether the method exists
        if (metaClass.invokeMethod(this, 'respondsTo', name, args)) {
            System.out.println "$name Method exists"
            System.out.println "$name Before execution.."
            metaClass.invokeMethod(this, name, args)
            System.out.println "$name After execution.."
        }
    }
}

new Person().func()

Code parsing

As soon as I see this code, isn't it what I explained before Detailed explanation of static agent and dynamic agent It's as like as two peas buddy, but only by Java. Let's continue to see the operation effect:

func invokeMethod
func Method exists
func Before execution..
I have a dream!
func After execution..

Note: if you want to intercept in this way, you must implement the GroovyInterceptable interface and override the invokeMethod method. This method is too troublesome. Is there a way to intercept without implementing this interface? So there is the second

2.1.2 method interception through MetaClass

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

def person3 = new Person3()
person3.func()
// Here is a method that intercepts an object
person3.metaClass.func = {
    println "I have a new dream !!!"
}
person3.func()

Operation effect

I have a dream!
I have a new dream !!!

It can be seen from here that the logic in the corresponding method can be modified directly through metaClass. It is equivalent to:

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

def person3 = new Person3()
// Equivalence and implementation of interception interface
person3.metaClass.invokeMethod = {
    String name, Object args ->// invokeMethod(String name, Object args)
        println "$name Intercepted"
}
person3.func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()

Operation effect

func Intercepted
I have a dream!
I have a dream!
I have a dream!
I have a dream!
I have a dream!

We found that it was only intercepted once and did not achieve global interception. What should we do to achieve global interception?

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

def person3=new Person3()
person3.func()

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

person3=new Person3()
person3.func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()
new Person3().func()

Here, we instantiate and call the corresponding method many times before and after interception. Now let's see the effect

I have a dream!
func Intercepted
func Intercepted
func Intercepted
func Intercepted
func Intercepted
func Intercepted

Here we can see that the code before interception and before operation will become the latest logic after interception. So far, we have intercepted the code created by ourselves. Can the system class intercept it?

String.metaClass.invokeMethod = {
    String name, Object args ->
        println "String.metaClass.invokeMethod"
        MetaMethod method = delegate.metaClass.getMetaMethod(name)
        if (method != null && name == 'toString') {
            "You were intercepted by me and wanted to toString"
        }
}
println "hqk".toString()

Here we see that the toString method of String is directly intercepted to see the running effect

String.metaClass.invokeMethod
 You were intercepted by me and wanted to toString

Here we see that instead of printing the string we want, we print the intercepted string. At this point, the Groovy interception method is almost finished. Now it's time to explain Groovy injection.

2.2 Groovy injection

2.2.1 using MetaClass to inject methods

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

// Before injection: org.codehaus.groovy.runtime.HandleMetaClass
println Person4.metaClass 

Person4.metaClass.newFunc = {
    println "newFunc call"
    //Method returns a string
    "After injection, it can still be accessed later"
}
// After injection: groovy.lang.ExpandoMetaClass
println Person4.metaClass 
def person=new Person4()
//After injection, try whether the injected method can be called
println person.newFunc()

Here, we see that there is no newFunc method in person 4, so we inject a new method newFunc through metaClass and implement the corresponding logic. Let's see how it works

org.codehaus.groovy.runtime.HandleMetaClass@ea27e34[groovy.lang.MetaClassImpl@ea27e34[class com.zee.gradle.groovy.Person4]]
groovy.lang.ExpandoMetaClass@27a0a5a2[class com.zee.gradle.groovy.Person4]
newFunc call
 After injection, it can still be accessed later

Here, we can see that the Class type before and after injection has changed, and its Class type has changed to groovy.lang.ExpandoMetaClass. Can we implement injection directly through ExpandoMetaClass?

2.2.2 using groovy.lang.ExpandoMetaClass to inject methods

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

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

Here, the ExpandoMetaClass object is directly instantiated, and then a series of operations are carried out through the object. See how it works

func2

The injected method can still be called normally

2.2.3 using classified injection method (writing method I)

class StringUtils {
    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 
        //Because println does not return a value, it returns null here, so it will be printed as null below
    }
}

use(StringUtils, StringUtils2) {
    println "".isNotEmpty()
    println 1.isNotEmpty()
}

Operation effect

isNotEmpty
false
isNotEmpty2
null

Here, the corresponding tool class is defined, which has the corresponding non empty judgment logic. The methods in these two classes are directly injected into the corresponding system class (String/Integer) through use. Then you can directly call the corresponding injected method in the closure.

In addition, there are other ways?

2.2.4 using classified injection method (writing 2)

@Category(Object)
class StringUtils2 {
    def isNotEmpty() {
        println "isNotEmpty2"
        if (this instanceof Integer){
            println "Digital type judgment"
            return true
        }
        println "Non numeric type judgment"
        this != null && this.length() > 0
    }
}
use(StringUtils2) {
    println "".isNotEmpty()
    println "\n"
    println 1.isNotEmpty()
}

Here we see that the @ Category annotation is used, and an Object is placed in it, indicating that any type of variable has a new method isNotEmpty in the use closure. See how it works

isNotEmpty2
 Non numeric type judgment
false


isNotEmpty2
 Digital type judgment
true

Here we can see that annotation can still be successfully injected, but these two methods can only be used in the use closure, which is usually used for temporary judgment and thrown away when used up.

Because Groovy is a dynamic language, if there are no corresponding methods and member variables in the object during program operation, it will certainly report an error if you force access outside. So how to avoid it?

2.2.5 assignment of initial values to nonexistent methods and variables

class Person5 {
    def username
    // get a non-existent variable
    def propertyMissing(String name) {
        println "propertyMissing"
        if (name == 'age') {
            "19" // Return to default
        }
    }
    // set non-existent variables
    def propertyMissing(String name, def arg) {
        println "propertyMissing ${name} : arg${arg}"
        return "default"// Give return value
    }
    // Initializing a method that does not exist
    def methodMissing(String name, def arg) {
        println "$name methodMissing"
        if (name.startsWith 'getFather') {
            "zzzz"
        }
    }
}
def p = new Person5()
println p.age = 12
println "\n"
println p.age
println "\n"
println p.getFather()

Operation effect

propertyMissing age : arg12
12


propertyMissing
19


getFather methodMissing
zzzz

It can be seen here that when there are no corresponding methods and variables in the object, you can avoid error reporting in this way, and you can also do different logical processing for different situations.

3. Concluding remarks

Because most of Gradle is written in Groovy language, learning Gradle must master some Groovy syntax. So far, the whole Groovy grammar has been explained. In the next article, we'll really start Gradle's explanation.

Posted by cloudzilla on Fri, 22 Oct 2021 03:37:22 -0700