Handwriting breaking parental delegation (Part 4 of class loading mechanism)

We know that when loading a class, we will delegate up level by level to determine whether it has been loaded. Start the class loader from user-defined class loader - application class loader - extended class loader - start the class loader. If the class is not loaded in the end, go back and load your own class.

Parental entrustment has one drawback:

Can't delegate down, can't delegate without

How to break the parental delegation mechanism: (i.e. can delegate downward or not)

Custom class loader (no delegation)

spi mechanism (delegation down)

Using spi to break parental delegation

What is SPI?

SPI, the full name of Service Provider Interface, is a set of interfaces provided by Java to be implemented or extended by third parties. It can be used to enable framework extension and replace components. The role of SPI is to find service implementations for these extended API s.

Scenarios used by SPI

API (Application Programming Interface) in most cases, the implementer formulates the interface and completes the implementation of the interface. The caller only depends on the interface call and has no right to choose different implementations. In terms of users, API is directly used by application developers.

SPI (Service Provider Interface) is an interface specification formulated by the caller and provided to the external implementation. When calling, the caller selects the external implementation he needs. In terms of users, SPI is used by framework extension personnel.

Because in some cases, the parent class loader needs to entrust the child class loader to load the class file. Due to the limitation of loading range, the parent loader cannot load the required file.

Take the Driver interface as an example. The Driver manager is loaded through Bootstrap ClassLoader, while com.mysql.jdbc.Driver is loaded through Application ClassLoader. Due to the parent delegation model, the parent loader cannot get the class loaded by the child loader. At this time, you need to start the class loader to delegate subclasses to load the Driver implementation, thus breaking the parental delegation.

Write an SPI case

Define an interface HelloSpi

package com.shendu.spi;

public interface HelloSpi {

    public String getName();
}

Define two implementation classes, HelloSpiImpl01 and HelloSpiImpl02

public class HelloSpiImpl01 implements HelloSpi {
    @Override
    public String getName() {
        return "helloSpiImp01";
    }
}

public class HelloSpiImpl02 implements HelloSpi {
    @Override
    public String getName() {
        return "helloSpiImp02";
    }
}

Define the full path file of the interface in META-INF\services \ as com.shendu.spi.HelloSpi, and write the full path of the implementation class in the file

com.shendu.spi.HelloSpiImpl01
com.shendu.spi.HelloSpiImpl02

Define test class as

public class SPITest {
    public static void main(String[] args) {
        ServiceLoader<HelloSpi> helloSpis = ServiceLoader.load(HelloSpi.class);

        for (HelloSpi hello :
                helloSpis) {
            System.out.println(hello.getName());
        }
    }
}

Printout:

helloSpiImp01
helloSpiImp02

Process finished with exit code 0

Here is just a small case to experience the charm of SPI. It allows the parent class to entrust the child class to load classes that are not in its own directory, thus breaking the conventional parent delegation mechanism.

The famous dubbo is enhanced based on SPI native. In the future, the author will give a source level SPI explanation. Please look forward to it.

Override the loadClass() method to break the parental delegation

Let's continue to review the source code of the loader method

//Working process source code of parent delegation model
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
    // First, check if the class has already been loaded
    Class c = findLoadedClass(name);
    if (c == null) {
        try {
            if (parent != null) {
                c = parent.loadClass(name, false);
            } else {
                c = findBootstrapClassOrNull(name);
            }
        } 
        catch (ClassNotFoundException e) {
            //The parent class loader could not complete the class load request
        }
 
        if (c == null) {
            //Class loading by child loader 
            c = findClass(name);
        }
    } 
    if (resolve) {
        //Judge whether the link process is required, and pass in the parameters
        resolveClass(c);
    }
    return c;
}

This method is the core method of parent delegation. We only need to override this method and add our own loading logic to break the parent delegation.

Case of overriding loadClass method

1 define custom loader

package com.shendu;

import sun.misc.PerfCounter;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.WritableByteChannel;

public class MyClassLoader02 extends ClassLoader {

    private String classPath;

    public MyClassLoader02(String classPath) {
        this.classPath = classPath;
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        File file = new File(classPath);
        try {
            byte[] bytes = getClassBytes(file);
            //The defineClass method can convert a file composed of binary stream bytes into a java.lang.Class
            Class<?> c = this.defineClass(name, bytes, 0, bytes.length);
            return c;
        } catch (Exception e) {
            e.printStackTrace();
        }

        return super.findClass(name);
    }


    private byte[] getClassBytes(File file) throws IOException {

        FileInputStream fis = new FileInputStream(file);
        FileChannel fc = fis.getChannel();
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        WritableByteChannel wbc = Channels.newChannel(baos);
        ByteBuffer by = ByteBuffer.allocate(1024);

        while (true) {
            int i = fc.read(by);
            if (i == 0 || i == -1)
                break;
            by.flip();
            wbc.write(by);
            by.clear();
        }
        fis.close();
        return baos.toByteArray();
    }


    @Override
    public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    //Key point: This is to add my own logic. As long as the classes under com.shendu are loaded through my custom loader
                    if (name.startsWith("com.shendu")) {
                        c = findClass(name);
                    } else {
                        c = this.getParent().loadClass(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
}

Define user class

package com.shendu;
public class User {

    private String name ;

    private Integer age ;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "User{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

Define test class

   
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException {
        //Instantiate custom loader
        MyClassLoader02 myClassLoader = new          MyClassLoader02("D:\\tcl_ouyang\\demo_coding\\jvmclassload\\target\\classes\\com\\shendu\\User.class");
		//Load user class
        Class<?> user = myClassLoader.loadClass("com.shendu.User");

    	//Assign a value to the user attribute through reflection
        Method setName = user.getMethod("setName", String.class);
        Method setAge = user.getMethod("setAge", Integer.class);
        Object o = user.newInstance();
        setName.invoke(o, "shendu");
        setAge.invoke(o, 18);
		
        System.out.println(o);
        System.out.println(o.getClass().getClassLoader());

    }

Print as follows:

User{name='shendu', age=18}
com.shendu.MyClassLoader02@4554617c

From the results, it is indeed loaded with our custom class loader.

Keywords: Java Front-end bootstrap

Added by acidbox on Thu, 04 Nov 2021 14:36:40 +0200