6. GDK to expand

This topic will focus on how Groovy can sweeten the JDK’s “boring” base — first, it’s best to take a look at the unified access principles, and second, a brief look at GDK’s extensions to the JDK. Third, manually implement extensions to the JDK based on SPI to increase Groovy development efficiency.

6.1 Unified Access Rules

Returning to a recent topic, we now know that we can retrieve the flow information of a process directly by calling the execute().text property:

// Access the text property of the Process
println "...".execute().text
Copy the code

However, when I tried to understand the internal details of the.text property, I discovered that text is not an attribute at all, and is actually linked to a.gettext () method.

// from decompiler.
public static String getText(File file) throws IOException {
    return IOGroovyMethods.getText(newReader(file));
}
Copy the code

Groovy provides syntactic candy that allows any method named get[Name] to be called externally as a.[Name] property. Something like this:

class Apple{
    def getWeight(){
        return 1000}}// Apple has no weight attribute.
new Apple().weight
Copy the code

In fact, the design was inspired by the Uniform Access Principle. The reason for this principle is that the supply side and the demand side view products differently. In application delivery, data providers often go through a series of (even complex) processes to provide reliable data, but this process is completely transparent to the demand side. This is why programming languages introduce functions — in addition to code reuse, another purpose is to hide internal processing details that callers (i.e., data demanders) don’t care about, or shouldn’t touch.

The access consistency principle goes one step further: the demand side of the data may not care at all whether the data is obtained by accessing fields or by evaluating functions. The only thing the demander needs to remember is to access a variable name when it needs to, as we did earlier with the process flow directly from.text.

A counterexample to this design principle is the Size() method of Java’s List interface (poor Java, which has been the villain of almost my entire Groovy project, in order to make Groovy cool). This is equivalent to telling the user: “If you want to get the length of the sequence, you must get it through a method call.” Perhaps we are used to it and curl our lips and say, “It’s no big deal to unify the access principle.” But Groovy believes that a few white lies can make life better for you and me.

The concept of the unified access principle was also mentioned in my Scala learning article: Scala: The Beginning of Functional Programming (juejin. Cn)

6.2 Expansion method of GDK

Groovy’s presence not only gives the JVM the benefit of a dynamic language, it also enhances the performance of the long-established JDK, allowing users to enjoy a lighter, more elegant Java API. Groovy’s extension to the JDK is called the Groovy JDK Development Pack, the GDK we installed to use Groovy.

This article won’t cover all the SDK extensions in detail, but just a few simple examples to get a feel for how efficient development can be through closures + GDK. In particular, the with() and indirect access methods described below will be useful in future dynamic programming.

6.2.1 Context Binding: with method

This approach was mentioned in method closures: “mount” a closure to an object, and all method calls within the closure are preferentially routed to the object’s instance method, which acts as the context for the closure.

class Context{
    def func1(){
        println "func1 of ${this.getClass()}@${this.hashCode()}"
    }

    def func2(){
        println "func2 of ${this.getClass()}@${this.hashCode()}"}}// These two methods will not be called first, as indicated by this.
def func1(){println "func1 of ${this.getClass()}@${this.hashCode()}"}
def func2(){println "func2 of ${this.getClass()}@${this.hashCode()}"}

new Context().with {
    func1()
    func2()
}
Copy the code

Again, it differs from setting a closure delegate in that the routing methods are in reverse order.

6.2.2 Indirectly accessing properties and methods

Suppose a Student class Student has two types of information: ID and name. When we make it clear that the user wants to access the name field, we simply call.name. But if you don’t know in advance what fields the user is requesting, because they might come from a form submitted on the other side of the network, you can’t hardcode each of the fields based on all of the user’s possible inputs. One solution is to use reflection, so let’s introduce a Java.lang.reflect.* bucket first.

Fortunately, Groovy is inherently built on Java’s reflection mechanism, which makes things easy: you can use the [] operator to directly and dynamically access a property of a user. This is essentially Groovy binding the getAt() method to the object and then doing it through operator overloading.

class Student_ {
    int id
    String name
    def info(){println "student ${id} : ${name}"}}def s = new Student_(id:1.name:"Wang Fang")

// This key may come from an external input.
def key = 'id'
println(s[key])
Copy the code

Likewise, since Groovy supports indirect property access while shielding a long list of reflection calls, indirect method access is similar. In Groovy, any object supports calling a method dynamically via invokeMethod() :

student.invokeMethod('info'.null)
Copy the code

Consider what we did before: we first got the meta-object Class, then called the getMethod Method to access the Method instance, then called the Invoke Method on that instance, and of course threw a bunch of exceptions we didn’t know what to do with.

6.2.3 Expansion of Thread

In Groovy, you can start a Thread with Thread.start, and then tell each Thread what its task is directly inside the closure.

// Whether two threads can execute in parallel depends on whether the machine has multiple logic cores and whether there are data races between the two threads.
// Note that start simply indicates that the thread is ready, and the actual time to run depends on the JVM's scheduling.
Thread.start {
    def name = Thread.currentThread().name
    for(;;) { println("thread:${name} : is running....")
        Object.sleep(1000)
    }
}

Thread.start {
    def name = Thread.currentThread().name
    for(;;) { println("thread:${name} : is running....")
        Object.sleep(1000)}}Copy the code

Thread.startDaemon Starts a daemon Thread, which is equivalent to a background Thread. The daemon thread exits automatically when all other non-daemon threads (including the main thread) of the program have finished executing.

Thread.start {
    def name = Thread.currentThread().name
    println("thread:${name} : is running....")
    Object.sleep(3000)
    println("thread:${name} has done.")
}

Thread.start {

    def name = Thread.currentThread().name
    println("thread:${name} : is running....")
    Object.sleep(4000)
    println("thread:${name} has done.")}// The daemon thread exits automatically after the preceding two threads finish executing.
Thread.startDaemon {
    def name = Thread.currentThread().name
    for (;;)
    {
        println "thread:${name} is waiting... ${Thread.activeCount()} is alive."
        Object.sleep(500)}}// The main thread exits after the three threads are started.
Copy the code

Now that we’re talking about threads, let’s take a look at Groovy’s sleep() method for binding objects. This method puts the thread executing at this point into a state of sleep — or, to be more precise, soundSleep. When another Thread interrupts its sleep with an interrupt(), the InterurptedException is suppressed by default, which differs from Thread.sleep.

import static java.lang.System.currentTimeMillis as now

// The actual running time is ~ 2000 ms, indicating that the interrupt has no effect.
def t = Thread.start {
    def l1 = now()
    new Object().sleep(2000)
    println now() - l1
}

// The main thread tries to interrupt t after sleeping for one second.
new Object().sleep(1000)
t.interrupt()
Copy the code

If you want the thread to be allowed to interrupt, you need to add a closure after the sleep method is called that indicates what to do if the interrupt exception is caught, and that the closure eventually returns false (or void) if you intend to continue ignoring the interrupt, or true otherwise.

def t = Thread.start {
    def l1 = now()
    new Object().sleep(2000) {It means that InterruptedException is caught.
        println "Who woke me up?? ${it}"
        true
    }
    println now() - l1
}

new Object().sleep(1000)
t.interrupt()
println "Interrupting sleep..."
Copy the code

6.2.4 Extensions to java.io

Groovy adds a number of useful extensions to the java.io class (we’ve already experimented with some of these methods before, withWriter, withStream… , etc.). For some lightweight IO requirements, Groovy allows you to do this directly in files (how does Groovy extend methods without modifying the JDK? “Manual extension of JDK methods” below may provide some ideas).

For example, call eachFile() in a File instance to view all files in the current path, or call eachDir() to view all folders in the current path. All operations are passed in as closures, and the following code demonstrates how to traverse everything under a folder in Groovy using recursion and the eachFile() method.

def seek(File file, int layout = 0) {
    if (file.file) {
        println "\t" * layout + file.name
    } else if (file.directory) {
        println "\t" * layout + file.name
        file.eachFile { seek(it, layout + 1)}}}Copy the code

Or maybe we want to load the text content of a file into the program at once (text has 8 MB of cache space inside it, combined with StringBuilder, although string constants are only supported at 65535, a string object can take up 2 gb of space).

// Unified access principle.text =>.gettext () method
prtinln new File(/C:\Users\ I \Desktop\ swordsword.txt /).text
Copy the code

Of course, you can also choose to process text content line by line.

new File(/C:\Users\ I \Desktop\ swordsword.txt /).eachLine {
    // Any processing for each row can be inserted here.
    println it
}
Copy the code

If you want to filter text content, you can use the fitlerLine method to filter content with regular expressions:

println new File("C:\ Users\ liJunhu\ Desktop\ group orders.txt").filterLine {
    // Filter out comment lines starting with #! (it =~/ ^ # /)}Copy the code

For writing files, Groovy is almost as concise as possible:

// Append is supported, or write is overridden.
// When writing binary files, the majority of cases are overwrites.
new File("C:\\Users\\liJunhu\\Desktop\\aa.txt").append(""" Hello Groovy, this is the 5th day of learning groovy. I wouldn't miss Java anymore.s """."UTF-8"
)

// Equivalent to the append method.
new File("C:\\Users\\liJunhu\\Desktop\\aa.txt") < <"..."
Copy the code

Even if we want to follow traditional Java patterns (such as manually resize the buffered stream), the withXXXX method gives us great convenience by simply writing the logic in a closure. For ioexceptions or flush(), close(), etc., Groovy takes care of all this trivia based on the Execute Around Method pattern.

new BufferedOutputStream(new FileOutputStream(new File("...")),4096).withStream {
    // It means BufferedOutputStream.
    it.write(...)
}
Copy the code

6.3 Manually extend JDK class methods

One day, we might find java.lang.String a little lacking in functionality and want to bind it to some custom static or instance methods based on project requirements. Unfortunately, String is marked as final, which means the JDK doesn’t want us to mess with String.

We might want to enforce a decorator without any inheritance, which means that whenever we need to call String’s native methods, we either unbox them, such as new StringHandler(“long long code”).getBind().toupperCase (); Or you could just manually wrap all the native String methods up front (too expensive to accept).

public class MainTest {
    public static void main(String[] args) throws IOException {
        // We need a seamless approach to integration.
        System.out.println(StringHandler.bracket("hello"));
        new StringHandler("looooog loooog code").writeTo(new File("C:\\Uses\\liJunhu\\Desktop\\aa.txt"),"UTF-8"); }}class StringHandler {

    private String bind;

    /** * The following thick code only wants to say one thing: write the string directly into a text, encapsulating the process as a method. *@paramTarget Indicates the file passed to the target. The file type must meet the requirement; otherwise, the assertion fails. *@paramCharset A string representation of the incoming character set. * /
    public void writeTo(File target,String charset) throws IOException {
        assert target.isFile();
        try(FileOutputStream fos = new FileOutputStream(target,true)) {// There is no FileWriter here, because I think it is a bit hard to adjust its charset.
            OutputStreamWriter osw = new OutputStreamWriter(fos,charset);
            BufferedWriter bw = new BufferedWriter(osw);
            bw.newLine();
            bw.append(bind);
            bw.flush();
        }catch(IOException ioe){ ioe.printStackTrace(); }}/** * adds a pair of parentheses to the string. *@paramSelf static method that requires a string passed in from outside. *@returnReturns the processed string. * /
    public static String bracket(String self){
        return "(" + self + ")";
    }
	
    // For reading purposes, GET, SET, and constructor methods are omitted here.
}
Copy the code

Despite my best efforts to convince myself that StringHandler is a wrapped String, I wish Java had introduced Scala’s implicit class features…… Isn’t there a way to seamlessly integrate extension methods into String? Groovy provides a solution. In simple terms, it’s like “keeping an elephant in your fridge” in three steps:

  1. Write container class hostingStringExtended instance method and static method. These containers can be written in any JVM language, including Groovy and Java.
  2. Pack these containers into onejar
  3. Find a way for Groovy to recognize thisjarPackage extensions (implemented via SPI, seeService Provider Interface+ Maven.

The first method is an instance of writing strings to text. The second method is a static method that adds brackets to strings. The Groovy class is chosen to do this, and the static and instance methods are stored separately in two container classes.

package com.i.extension
import groovy.transform.CompileStatic

@CompileStatic
class StringExtension {
    // "HELLO WORLD".writeTo(new File("..." ))
    // Where self refers to the String calling the instance method.
    WriteTo accepts only file and charset when called.
    static String writeTo(String self,File file,String charset){
        assert file.file
        file.withWriterAppend(charset){
            it.writeLine(self)
        }
    }
}


//------ two class files -----------//

package com.i.extension
import groovy.transform.CompileStatic

@CompileStatic
class StringStaticExtension {
    // String.brackt("hello") -> "(hello)"
    // The selfType here means that this method is bound to the String type.
    // When called, BRACKET only accepts the target argument.
    static String bracket(String selfType,String target){
        return "(${target})"}}Copy the code

Note that whether we bind instance methods or static methods to the target, they must all be declared static methods in the container. Second, if the method is to be bound to the instance method of the target, the first argument it receives represents the target itself (equivalent to this), whereas the first argument is used to bind static types.

That is, the first parameter of each method has a special meaning that will not appear directly in the method’s parameter list in the future (see comments for details).

These containers are then loaded into a JAR package. But! Groovy doesn’t just specify this JAR as an extension of String (or any other JDK class) for nothing. Before doing this, we need to register the extension service under META-INF/ Services using the SPI (service discovery Interface, used to break the traditional parent-delegate model on which JDBC is based) interface. This way, when this extension package is introduced into other projects in the future, Groovy will know that we have extended String.

Note: For Maven projects, meta-INF /services should be placed in the resoucre folder. Into the meta-inf/services create org. Codehaus. Groovy. Runtime. ExtensionModule file, leave the following information:

moduleName=GString_extension
moduleVersion=1.0
extensionClasses=com.i.extension.StringExtension
staticExtensionClasses=com.i.extension.StringStaticExtension
Copy the code

ModuleName is the logical name we gave the extension module, and moduleVersion is used to declare the version of the module (these two items are customizable). ExtensionClasses specifies the fully qualified name of our instance method extension container, separated by commas if there are multiple entries; StaticExtensionClasses specify the fully qualified name of our static method extension container.

Now you can export the entire project as a JAR package using the IDE or Maven plug-in. In other Groovy projects, we just import the jar package into the dependency, and then we can use our own extended methods for the String class.

// a String method "implemented by us" is created.
// All method calls really seem to come from String.
"hello".writeTo(new File(/C:\Users\i\Desktop\greet.txt/),"UTF-8")

// (hello)                
println String.bracket("hello")
Copy the code

Not only that, but the IDE is now fully capable of giving code hints based on the methods we bind. By now, the question of how GDK can extend the JDK without violating OCP principles should have been resolved……