SPI mechanism and jdbc break parental delegation

Posted by Bounty on Sat, 08 Jan 2022 14:26:50 +0100


The article has been included in my warehouse: Java learning notes

SPI mechanism and JDBC break parental delegation

This paper is based on jdk 11

SPI mechanism

brief introduction

What is SPI mechanism?

The full name of SPI in Java is Service Provider Interface. It is a built-in service provision discovery mechanism in JDK. It is a set of API s provided by java to be implemented or extended by third parties. It can be used to enable framework extension and replace components.

For example, in the central jdbc of this article, we can use the unified interface Connection to manipulate various databases, but have you ever thought that jdk really has all database drivers built in?

Obviously, this is impossible. We usually need to import the corresponding jar package to use different databases, such as MySQL and SqlServer databases. However, we use connection conn = drivermanager Getconnection (URL, user, pass) is indeed a built-in interface of jdk. In fact, this is an SPI mechanism, that is, an interface is officially defined and implemented by different third-party service providers.

So the question is, how is the SPI mechanism implemented?

The answer is very simple, abide by the official agreement!

jdk SPI principle

SPI can be implemented in different ways, but the core idea is to abide by official rules anyway. The official jdk is not necessary. For example, SpringBoot defines a set of rules, but we still talk about the implementation principle of jdk.

The key point of implementing SPI mechanism is to know the specific implementation classes of the interface. These implementation classes are provided by third-party services. There must be a method to let the JVM know which implementation class to use. Therefore, the following rules are officially defined:

  • The class of the service provider must implement the officially provided interface (or inherit a class).
  • Place the fully qualified name of the concrete implementation class in the META-INF/services/${interfaceClassName} file under the resource file, where ${interfaceClassName} is the fully qualified name of the interface.
  • If multiple implementation classes are scanned, the jdk initializes all of them.

This is the jdk convention. Since the class name provided by the service provider is required to be placed under the META-INF/services/${interfaceClassName} file, the official must scan this file. The official provides an implementation: serviceloader Load method.

The pseudo code for obtaining a specific class instance is as follows:

ServiceLoader load = ServiceLoader.load(XXX.class);
for (XXX x : load) {
    // o is the instance we want to obtain. XXX is an officially defined interface
    System.out.println(x);
}

ServiceLoader. The load method returns an instance of serviceloader. We need to traverse its iterator to get all possible instances, and the core code is in the iterator.

Let's look at the source code of this iterator. We mainly look at the hasNext method, because the next method itself depends on the hasNext method:

public Iterator<S> iterator() {
    return new Iterator<S>() {
        int index;
        @Override
        public boolean hasNext() {
            if (index < instantiatedProviders.size())
                return true;
            return lookupIterator1.hasNext();
        }
    };
}

Skip to lookupIterator1 In the hasnext () method, lookupIterator1 is returned by calling the newLookupIterator() method. Take a look at this method:

private Iterator<Provider<S>> newLookupIterator() {
    Iterator<Provider<S>> first = new ModuleServicesLookupIterator<>();
    Iterator<Provider<S>> second = new LazyClassPathLookupIterator<>();
    return new Iterator<Provider<S>>() {
        @Override
        public boolean hasNext() {
            return (first.hasNext() || second.hasNext());
        }
        @Override
        public Provider<S> next() {
            if (first.hasNext()) {
                return first.next();
            } else if (second.hasNext()) {
                return second.next();
            } else {
                throw new NoSuchElementException();
            }
        }
    };
}

It can be found that there are two main loading methods, one is modular loading, and the other is ordinary lazy loading. We should go to second Hasnext() method:

@Override
public boolean hasNext() {
    if (acc == null) {
        return hasNextService();
    } else {
        PrivilegedAction<Boolean> action = new PrivilegedAction<>() {
            public Boolean run() { return hasNextService(); }
        };
        return AccessController.doPrivileged(action, acc);
    }
}

second. The hasnext () method calls the hasNextService() method again. Let's see the logic of this method:

private boolean hasNextService() {
    while (nextProvider == null && nextError == null) {
        try {
            Class<?> clazz = nextProviderClass();
            if (clazz == null)
                return false;
            if (service.isAssignableFrom(clazz)) {
                Class<? extends S> type = (Class<? extends S>) clazz;
                Constructor<? extends S> ctor  = (Constructor<? extends S>)getConstructor(clazz);
                ProviderImpl<S> p = new ProviderImpl<S>(service, type, ctor, acc);
                nextProvider = (ProviderImpl<T>) p;
            } 
        } catch (ServiceConfigurationError e) {
            nextError = e;
        }
    }
    return true;
}

This method first determines whether the nextProvider is empty. If not, it indicates that there are already resources and returns directly; We are loading for the first time, and it must be empty. Therefore, when we enter the loop, we can see that the main method is nextProviderClass(). This method returns a constructor, then carries out real packaging, and sets nextProvider equal to the packaged ProviderImpl. With ProviderImpl, we can naturally obtain instances through reflection!

The core method here should be nextProviderClass():

static final String PREFIX = "META-INF/services/";
private Class<?> nextProviderClass() {
    if (configs == null) {
        // Pathname
        String fullName = PREFIX + service.getName();
        // The resource is obtained according to the pathname, and the loader is obtained from the thread context
        configs = loader.getResources(fullName);
    }
    // pending is a String iterator
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return null;
        }
        pending = parse(configs.nextElement());
    }
    String cn = pending.next();
    try {
        return Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        return null;
    }
}

You can see in this step through prefix + service Getname() gets the file name, which is what we call the META-INF/services/${interfaceClassName} convention file, and then obtains the corresponding resource. Pending is a String iterator, which contains the fully qualified name of the specific implementation class. If this iterator exists, it indicates that it has been resolved, then it directly returns class forName(pending.next(), false, loader).

If the pending iterator goes to the end, it enters the loop again and calls configs Nexterelement() reads again, * * which means that if there are multiple configuration files, all classes will be loaded** Until there is nothing left, throw an exception and return null. If NULL is returned here, the hashNext method will return false.

If it is the first parsing, it will enter pending = parse (configs. Nexterelement()); Method, take a look at this method:

private Iterator<String> parse(URL u) {
    Set<String> names = new LinkedHashSet<>(); // preserve insertion order
    URLConnection uc = u.openConnection();
    uc.setUseCaches(false);
    try (InputStream in = uc.getInputStream();
         BufferedReader r = new BufferedReader(new InputStreamReader(in, UTF_8.INSTANCE))) {
        int lc = 1;
        while ((lc = parseLine(u, r, lc, names)) >= 0);
    }
    return names.iterator();
}

This method is very simple, that is, read each row in the configuration, add it to the Set, and then return its iterator. So we will return all the fully qualified names in a configuration file!

This is the specific implementation principle of SPI mechanism provided by jdk!

Note: the above source code has been slightly simplified

Write a demo

If we have a HelloPrinter interface that needs to be provided by a third party, we can write it to obtain the specific implementation class:

public class Test {
    public static void main(String[] args) {
        ServiceLoader<HelloPrinter> load = ServiceLoader.load(HelloPrinter.class);
        Iterator<HelloPrinter> iterator = load.iterator();
        while (iterator.hasNext()) {
            HelloPrinter helloPrinter = iterator.next();
            helloPrinter.hello();
        }
    }
}

Do you have any information about specific implementation classes? No, The specific implementation class is completely transparent to the customer. The customer only knows the HelloPrinter interface! Nothing is done now, and this code will not output anything.

Then we start another project, write two implementation classes helloprrinterimpl1 and helloprrinterimpl2, and establish the following directories according to the Convention:

Fill in the fully qualified name of the implementer in this file:

com.demo.HelloPrinterImpl1
com.demo.HelloPrinterImpl2

Then maven packages and lets the client import the jar package and run it again. Now the output of the client is:

I am the first implementer!
I am the second implementer!

JDBC breaks the parental delegation model

Pre knowledge of this section: Class loading mechanism

JDBC is also an SPI mechanism. For example, when we introduce the jar package driven by MYSQL:

We can see that as like as two peas, we should use the driver's "com.mysql.cj.jdbc.Driver".

After introducing the jar package, you can use the following code:

Connection connection = DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306/xxx?serverTimezone=GMT", "root", "123456");

To obtain the database connection. Connection is the official interface. No matter what database, it can be operated the same, which is very convenient.

But why do we say that jdbc breaks the parental delegation model?

The reason is drivermanager The getconnection method actually loads com mysql. cj. jdbc. The driver class, and then the driver creates the connection.

The problem is that DriverManager belongs to the official class of JDK and should be loaded by the boot class loader (jdk8 was formerly called the boot class loader), while com mysql. cj. jdbc. The driver class obviously cannot be loaded by the boot class loader, but by the application class loader (also known as the system class loader).

public class Test {
    public static void main(String[] args) throws Exception {
        System.out.println(DriverManager.class.getClassLoader().getName());
        Class c = Class.forName("com.mysql.cj.jdbc.Driver");
        System.out.println(c.getClassLoader().getName());
    }
}

The output is:

platform
app

It can be seen that the platform class loader and the application class loader load these two classes. The problem is how the DriverManager loads com mysql. cj. jdbc. Driver driver, directly using class Is forname feasible?

infeasible! Because class By default, forname will be loaded by the caller's own class loader. The class loader of DriverManager is the platform class loader. Obviously, the driver class cannot be loaded.

Therefore, the DriveManager must use the secondary class loader to load. Here, the SPI mechanism is used. In the getConnection method:

SPI implementation has been mentioned above, but we deliberately didn't talk about serviceloader Load method, we call a line of code:

private static Connection getConnection(
    String url, java.util.Properties info, Class<?> caller) throws SQLException {
    // ellipsis
    ensureDriversInitialized();
    // ellipsis
    return driver.connect(url, info);
}

The method of ensureDriversInitialized():

private static void ensureDriversInitialized() {
    synchronized (lockForInitDrivers) {
        if (driversInitialized) {
            return;
        }
        String drivers;
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();
                while (driversIterator.hasNext()) {
                    driversIterator.next();
                }
                return null;
            }
        });
        driversInitialized = true;
    }
}

You can see that loadeddrivers = serviceloader is called in this method Load (driver. Class) method, and then traverse the iterator, driveriterator Next() is equivalent to instantiating Drive. Although it seems that it has done nothing, in fact, during the instantiation process, it triggers the call of the initialization statement block of Drive class:

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }
    
    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

It registers itself with DriverManager! Therefore, DriverManager can save this Driver!

The implementation principle of ServiceLoader loading has been mentioned above, but I deliberately didn't talk about ServiceLoader Load method:

@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}

In the load method, set the default loader as the thread context loader. This thread context class loader has been described in detail in the previous article and will not be repeated.

Because we call drivermanager in the main thread The getconnection method is obvious now. ServiceLoader uses the class loader of the main thread to load, which is of course the Application Loader!

Now let's summarize why jdbc broke the parental delegation. I think there are two points:

  1. The official class of the DriverManager data jdk uses the platform loader, but the application loader is used to load the Driver class, which is equivalent to that the high-level classes use the low-level class loader, which can be regarded as the reverse of the parental delegation model.
  2. In addition, ServiceLoader uses the thread context loader to directly obtain the class loader without calling up layer by layer, which must also violate the parental delegation model.

This thread context class loader has been described in detail in the previous article and will not be repeated.

Because we call drivermanager in the main thread The getconnection method is obvious now. ServiceLoader uses the class loader of the main thread to load, which is of course the Application Loader!

Now let's summarize why jdbc broke the parental delegation. I think there are two points:

  1. The official class of the DriverManager data jdk uses the platform loader, but the application loader is used to load the Driver class, which is equivalent to that the high-level classes use the low-level class loader, which can be regarded as the reverse of the parental delegation model.
  2. In addition, ServiceLoader uses the thread context loader to directly obtain the class loader without calling up layer by layer, which must also violate the parental delegation model.

Topics: Java Database