What is SPI?

The Service Provider Interface (SPI) is a built-in Service Provider discovery mechanism in JDK. SPI is a mechanism for dynamically replacing discovery. For example, if you have an interface and want to dynamically add an implementation to it at runtime, you just add an implementation. The Driver interface can be implemented by different vendors. Mysql and PostgresQL both have different implementations for users. Java’s SPI mechanism can find a service implementation for a particular interface.

In the class diagram, the interface corresponds to the abstract SPI interface defined; The implementer implements the SPI interface; Callers rely on the SPI interface.

The SPI interface is defined by the caller and conceptually more dependent on the caller; Organizationally in the package of the caller; The implementation is in a separate package.

In the case where the interface belongs to the implementer, the implementer provides the interface and the implementation, this usage is very common and is an API call. We can invoke an implementation class by referring to an interface.

Java SPI application instance

When a service provider provides an implementation of an interface, you need to create a file named after the service interface in the meta-INF/Services/directory of your classpath. The file contains the specific implementation class of the interface. When other programs need this service, they can use it by looking for the configuration file in the meta-INF /services/ of the jar package. The configuration file contains the name of the class that implements the interface, and they can load the instantiation according to the name. The tool class for finding service implementations in the JDK is java.util.Serviceloader.

SPI interface

public interface ObjectSerializer {

    byte[] serialize(Object obj) throws ObjectSerializerException;

    <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException;

    String getSchemeName(a);
}
Copy the code

An object serialization interface is defined with three methods: serialization method, deserialization method, and serialization name.

SPI implementation

public class KryoSerializer implements ObjectSerializer {

    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        byte[] bytes;
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            // Get the kryo object
            Kryo kryo = new Kryo();
            Output output = new Output(outputStream);
            kryo.writeObject(output, obj);
            bytes = output.toBytes();
            output.flush();
        } catch (Exception ex) {
            throw new ObjectSerializerException("kryo serialize error" + ex.getMessage());
        } finally {
            try {
                outputStream.flush();
                outputStream.close();
            } catch (IOException e) {

            }
        }
        return bytes;
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        T object;
        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(param)) {
            Kryo kryo = new Kryo();
            Input input = new Input(inputStream);
            object = kryo.readObject(input, clazz);
            input.close();
        } catch (Exception e) {
            throw new ObjectSerializerException("kryo deSerialize error" + e.getMessage());
        }
        return object;
    }

    @Override
    public String getSchemeName(a) {
        return "kryoSerializer"; }}Copy the code

Use Kryo’s serialization mode. Kryo is a fast and efficient Java object graphics serialization framework that supports Java natively and is even better at serializing Java than Protobuf, Google’s famous serialization framework.

public class JavaSerializer implements ObjectSerializer {
    @Override
    public byte[] serialize(Object obj) throws ObjectSerializerException {
        ByteArrayOutputStream arrayOutputStream;
        try {
            arrayOutputStream = new ByteArrayOutputStream();
            ObjectOutput objectOutput = new ObjectOutputStream(arrayOutputStream);
            objectOutput.writeObject(obj);
            objectOutput.flush();
            objectOutput.close();
        } catch (IOException e) {
            throw new ObjectSerializerException("JAVA serialize error " + e.getMessage());
        }
        return arrayOutputStream.toByteArray();
    }

    @Override
    public <T> T deSerialize(byte[] param, Class<T> clazz) throws ObjectSerializerException {
        ByteArrayInputStream arrayInputStream = new ByteArrayInputStream(param);
        try {
            ObjectInput input = new ObjectInputStream(arrayInputStream);
            return (T) input.readObject();
        } catch (IOException | ClassNotFoundException e) {
            throw new ObjectSerializerException("JAVA deSerialize error "+ e.getMessage()); }}@Override
    public String getSchemeName(a) {
        return "javaSerializer"; }}Copy the code

Java native serialization.

The meta-INF directory file was added

Create a file named after the service interface in the meta-INF /services directory

com.blueskykong.javaspi.serializer.KryoSerializer
com.blueskykong.javaspi.serializer.JavaSerializer
Copy the code

The Service class

@Service
public class SerializerService {


    public ObjectSerializer getObjectSerializer(a) {
        ServiceLoader<ObjectSerializer> serializers = ServiceLoader.load(ObjectSerializer.class);

        final Optional<ObjectSerializer> serializer = StreamSupport.stream(serializers.spliterator(), false)
                .findFirst();

        return serializer.orElse(newJavaSerializer()); }}Copy the code

Gets the defined serialization mode, and only takes the first (we wrote two in the configuration), and returns the Java native serialization mode if not found.

The test class

    @Autowired
    private SerializerService serializerService;

    @Test
    public void serializerTest(a) throws ObjectSerializerException {
        ObjectSerializer objectSerializer = serializerService.getObjectSerializer();
        System.out.println(objectSerializer.getSchemeName());
        byte[] arrays = objectSerializer.serialize(Arrays.asList("1"."2"."3"));
        ArrayList list = objectSerializer.deSerialize(arrays, ArrayList.class);
        Assert.assertArrayEquals(Arrays.asList("1"."2"."3").toArray(), list.toArray());
    }
Copy the code

The test case passes and kryoSerializer is displayed.

The purpose of the SPI

Database DriverManager, Spring, ConfigurableBeanFactory, and others all use the SPI mechanism. Here, take database DriverManager as an example to see the inside of its implementation.

DriverManager is a JDBC utility class that manages and registers drivers for different databases. There may be different database-driven implementations for a single database. When we use a specific driver implementation, we don’t want to modify existing code, but a simple configuration can do the job. When using mysql drivers, there is a question: how does DriverManager get a certain driver class? After we load the mysql Driver using class.forname (” com.mysql.jdbc.driver “), we execute the static code to register the Driver with DriverManager for future use.

Before JDBC4.0, it is common to use the class.forname (” com.mysql.jdbc.driver “) to load the database Driver, and then to obtain the connection. After JDBC4.0, Class. ForName is not required to load the driver, and the connection can be obtained directly. Here, Java SPI extension mechanism is used to achieve.

The Java interface java.sql.Driver is defined in Java, and there is no specific implementation. The specific implementation is provided by different vendors.

mysql

In mysql-connector-java-5.1.45.jar, there is a file named java.sql.Driver in the meta-inf /services directory:

com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
Copy the code

pg

In the postgresql-42.2.2.. jar, there is a file named java.sql.Driver in the meta-inf /services directory:

org.postgresql.Driver
Copy the code

usage

String url = "jdbc:mysql://localhost:3306/test";
Connection conn = DriverManager.getConnection(url,username,password);
Copy the code

The mysql usage is shown above, and the PG usage is similar. You do not need to use class.forname (” com.mysql.jdbc.driver “) to load the Driver.

Mysql DriverManager implementation

How do we determine which database connection driver to use? DriverManager is a Java implementation that is used to obtain database connections. In DriverManager, there is a static block of code as follows:

static {
	loadInitialDrivers();
	println("JDBC DriverManager initialized");
}
Copy the code

You can see that inside the static code block there is a loadInitialDrivers method that uses the SPI utility class ServiceLoader mentioned above:

    public Void run(a) {

        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();

        /* Load these drivers, so that they can be instantiated. * It may be the case that the driver class may not be there * i.e. there may be a packaged driver with the service class * as implementation of java.sql.Driver but the actual class * may be missing. In that case a java.util.ServiceConfigurationError * will be thrown at runtime by the VM trying to locate * and load the service. * * Adding a try catch block to catch those runtime errors * if driver not available in classpath but it's * packaged as service and that service is there in classpath. */
        try{
            while(driversIterator.hasNext()) { driversIterator.next(); }}catch(Throwable t) {
        // Do nothing
        }
        return null;
    }
Copy the code

Iterate over the concrete implementation obtained using SPI, instantiating the implementation classes. To iterate, first call driversiterator.hasnext (), which searches the java.sql.Driver file in the classpath and all meta-INF /services directories in the JAR package, and finds the name of the implementation class in the file. No concrete implementation class is instantiated at this point.

conclusion

There are also many scenarios where the SPI mechanism can be used in real development. In particular, different vendors implement unified standards. After the relevant organization or company defines the standard, specific vendors or framework developers implement it and then provide it to developers.

This article code: https://github.com/keets2012/Spring-Boot-Samples/tree/master/java-spi

Subscribe to the latest articles, welcome to follow my public number

reference

  1. Java SPI mechanism in depth and source code parsing
  2. Java SPI thought combing