background

As part of the JDK standard, the JDBC API defines a set of interfaces for Java to interact with databases. There are many kinds of databases, and the implementation of how to interact is provided by the database vendor. Take MySQL as an example. After version 4.0 of the JDBC API, we usually only need to import its driver mysql-connector-Java as our dependency. Then we can call the JDBC API to create a connection and execute SQL to interact with the database. In this process, how does our program locate the specific location of the driver and load it correctly? This brings us to the SPI mechanism.

Mechanism of SPI

Service Provider Interface (SPI) can be said to be an extension-oriented design mode. The core logic of the application is designed with the Interface oriented programming idea and extensible points, and the specific implementation of the extension points is loaded through the Service discovery mechanism at runtime, so as to achieve without modifying the core logic. Concrete implementation of flexible replacement effect.

Java’s SPI mechanism

Java 6 provides the SPI mechanism. Java’s SPI mechanism consists of two types of roles. The first is the service definer.

  • Service Provider Interface (SPI) : A Service Provider Interface, usually a set of interfaces or abstract classes, that uniformly defines how a Service is consumed.
  • ServiceLoader: a run-time loading mechanism for a service, based on the spIs defined to find the implementation.

Then there are service implementers, involving concepts such as:

  • Service Provider: The concrete implementation of the Service, which is the implementation of the SPI by the Service Provider.

A simple example of how SPI works

For example, our service needs to provide consumers with the ability to pay, but the actual ability to pay may be provided by Alipay or wechat. As a service definer, we first define the standard Interface of the service, which is itself a common Java Interface. For example, we define our SPI:

package me.leozdgao.demo.spi;

public interface Payment {
    void pay(Long amount, Long from, Long to);
}
Copy the code

This interface should be exposed for the service provider to implement. We can publish it in a separate package, such as my-system-spi, and then the service provider implements it. First we introduce the package containing the SPI definition:

<dependency>
    <groupId>me.leozdgao</groupId>
    <artifactId>my-system-spi</artifactId>
    <version>1.0.0 - the SNAPSHOT</version>
</dependency>
Copy the code

Then implement it:

package me.leozdgao.payment;

public class MyPayment implements Payment {

    @Override
    public void pay(Long amount, Long from, Long to) {
        System.out.println("Pay with " + amount + " from " + from + " to "+ to); }}Copy the code

At the same time, the service provider needs to inform itself that it has an implementation of an SPI. The convention is to define a file named SPI fully qualified in the META-INF/services folder of the Jar package. In the file, the fully qualified name of the SPI implementation class is defined. For example, we need to create a meta-inf/services/me. Leozdgao. Demo. Spi. The Payment documents, the contents of the file is as follows:

me.leozdgao.payment.MyPayment
Copy the code

If there are multiple implementations in a Jar package, they can all be listed and separated by a newline character.

After the service provider implements the Jar, it releases the Jar, and then we need to load it. This is where the key ServiceLoader comes in.

package me.leozdgao.demo.service;

import me.leozdgao.easyerp.spi.Payment;

import java.util.Iterator;
import java.util.ServiceLoader;

public class MyService {

    @Override
    public void doPay(Long amount, Long from, Long to) {
        ServiceLoader<Payment> loader = ServiceLoader.load(Payment.class);

        for (Driver driver : loader) {
                // ...}}}Copy the code

You can see that we created an instance of ServiceLoader by calling the Serviceloader.load method and passing in our SPI interface. Since ServiceLoader implements the Iterator interface, Services that implement SPI can be retrieved by accessing iterators. If there are multiple implementations of SPI, which one to use is a matter of discretion.

This allows us to load the service at run time without having to change our logic code at all if we want to replace the SPI implementation in the future.

ServiceLoader service loading mechanism

The serviceloader. load method creates an instance of the ServiceLoader. The Lazy Iterator is used to load the ServiceLoader.

See JDK source code: java.util.Serviceloader

Because it is a Lazy Iterator, a ServiceLoader instance does not start by finding all the implementations. Instead, it lazily loads the implementation classes, completes the instantiation, and caches the results of the implementation class instantiation during repeated Iterator calls. The initialization logic of the service also reflects two conventions:

  • Implement positioning dependencies for classesMETA-INF/services/*Statement (already mentioned)
  • The service implementation class needs to provide a no-argument constructor to instantiate

How does MySQL Driver implement SPI

With the implementation mechanism of SPI covered above, let’s answer the initial question: how does our program locate the specific driver location and load it correctly? This is the implementation of DriverManager.

An ensureDriversInitialized method is called in the static methods getDrivers, getDriver, drivers, and getConnection of DriverManager. This method ensures that the driver is initialized and executed only once. Let’s look at its implementation logic:

See JDK source code: java.sql.DriverManager

Drivers can be initialized using the system parameter jdbc.drivers, and Class. ForName can be reflected to initialize the driver. Another option is to use serviceloader.load (driver.class) to find the service provider and invoke an iterator to trigger initialization of the service.

The specific initialization of the service takes advantage of the static code block mechanism of the class. Take MySQL driver as an example:

package com.mysql.cj.jdbc;

import java.sql.DriverManager;
import java.sql.SQLException;

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

As you can see, the Driver implementation contains a static block of code that can be triggered to execute, either by reflection class.forname or when initialized by the ServiceLoader iterator. Here called the DriverManager. RegisterDriver was registered with.

So if multiple drivers are found, which specific implementation should be selected? DriverManager uses this convention until it finds the first connection that does not return null.

conclusion

This article discusses the design idea of SPI, analyzes the mechanism of Java SPI and the implementation principle of ServiceLoader service loading, and takes JDBC API as an example to see the specific application.

As you can see, Java provides a standard local service discovery mechanism that doesn’t rely on additional frameworks. We can use this mechanism to make our applications visible in either the server base library or the Android ecosystem based on flexibility and extensibility. However, in addition to local service discovery, Which service implementation you should choose is still an additional consideration when designing extension functionality.

In general, understanding Java SPI is more important than learning about SPI’s design philosophy. In fact, you can implement your own version of ServiceLoader (define your own service discovery mechanism, depending on the situation). Even Jar packages can be loaded remotely) to provide a unique extensibility mechanism for your application.