⚠️ This article is the first contract article in the nuggets community. It is prohibited to reprint without authorization


cause

In the middle of July, the investigation of potential risks of our company’s system was in full swing. I found that the call source of the current system lacked Token information, so it was difficult to identify and monitor it in detail. Therefore, it needed to be optimized.

For the two problems mentioned above, I just need to implement a framework base class and do a little business processing. According to the framework documentation, I can implement the following steps:

After finishing the development and debugging, I became curious about this implementation method and wondered how they were instantiated and played a role in the framework. If you are interested, please come with me to find out (😜)


What is the SPI

At first, I didn’t even know that this technology/solution was supported by Java itself. I thought it was designed by the framework itself. Later, I asked other colleagues to find out that this flexible way of providing service capability is called SPI.

SPI: Full name Service Provider Interface. Java is a set of interfaces provided by Java to be implemented or extended by third parties, mostly used for framework extension, plug-in development, etc.

For example, the implementation parameter filter mentioned above is a framework extension, so let’s do a little Demo.


How SPI works

SPI’s discovery capability is not dependent on other class libraries and can be achieved in two main ways:

  • Sun.misc.Service Load capability provided by Sun
  • Java.util.serviceloader #load The loading capability provided by the JDK itself

Because method 2 is JDK internal code, containing source code, it is used by default for subsequent instructions

Basic steps:

  1. Define an interface that needs to provide external capabilities

    public interface SPIInterface {
        String handle(a);
    }
    Copy the code
  2. Defines an implementation class that implements the specified interface

    public class SPIInterfaceImpl implements SPIInterface {
        @Override
        public String handle(a) {
            return "Current time is:"+ LocalDateTime.now(); }}Copy the code
  3. Configure the relevant implementation class at the specified location: Resource/meta-INF /services

    Note that resource is a resource file

    # file location (the resource/meta-inf/services/com. Mime. The spi. SPIInterface)
    # content (full class name of the implementation class)
    com.mine.spi.impl.SPIInterfaceImpl
    Copy the code
  4. Use the initialization capabilities provided by the JDK

    public class SpiApp {
        public static void main(String[] args) {
            ServiceLoader<SPIInterface> load = ServiceLoader.load(SPIInterface.class);
            for(SPIInterface ser : load) { System.out.println(ser.handle()); }}}/ / response
    // Current time: 2021-08-24T03:30:52.397
    Copy the code

Explosively simple, the key is that the JDK already helps us implement this set of discovery and initialization steps, so let’s take a closer look at the basic source 😁

Pass the current interface Class type and its classloader into the Loader variable from the java.util.serviceloader #load method as an entry: java.util.serviceloader #load

/** * service: interface type * loader: class loader * acc: security manager */
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null)? AccessController.getContext() :null;
    reload();
}
Copy the code


The class LazyIterator is a LazyIterator that is initialized only when triggered. The core initialization logic is in the method: Java. Util. ServiceLoader. LazyIterator# nextService.

private S nextService(a) {
    // omit other code...Class<? > c =null;
    try {
        c = Class.forName(cn, false, loader);
    } catch (ClassNotFoundException x) {
        fail(service,
             "Provider " + cn + " not found");
    }
    
    // omit other code...
}
Copy the code

Given the interface type and its full class name, it’s easy to build instance objects by reflection. Once you get the instantiated object, it’s no different from normal code.

Let’s take a look at a few more examples of frameworks using SPI in action, looking at our predecessors’ code 😎


SPI use case analysis

Log4j-Api

For example, log4J-API-2.13.3. jar implements the PropertySource interface based on SPI to collect configuration information about the current server, as shown in the following figure:


Similarly, log4J-core-2.13.3. jar implements the log facade binding based on SPI, as shown in the following core code:

/** * Binding for the Log4j API. */
public class Log4jProvider extends Provider {
    public Log4jProvider(a) {
        super(10."Server", Log4jContextFactory.class); }}Copy the code

JDBC driver

Take the common JDBC driver mysql-connector-java-5.1.43. Jar as an example, it also implements SPI interface. The driver classes are as follows: Class. ForName (” com.mysql.jdbc.driver “); FabricMySQLDriver (” FabricMySQLDriver “);

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

The FabricMySQLDriver class is the same as the FabricMySQLDriver class. We can also break the loading mechanism by implementing MySQLDriver to connect to the database.

public class CustomDriver extends NonRegisteringDriver implements Driver {

    static {
        try {
            java.sql.DriverManager.registerDriver(new CustomDriver());
        } catch (SQLException ignored) {}
    }

    public CustomDriver(a) throws SQLException {}@Override
    public Connection connect(String url, Properties info) throws SQLException {
        System.out.println("[Kerwin] perform database connection...");
        return super.connect(url, info);
    }

    @Override
    public Logger getParentLogger(a) throws SQLFeatureNotSupportedException {
        return null; }}Copy the code

Then inject the CustomDriver into the SPI.

Note that the CustomDriver class must inherit from the NonRegisteringDriver class, otherwise it will be registered by the default Driver first. After that, it will be called using the old JDBC code, which can simulate the breaking of SPI, as shown in the figure:

public void customDriver(a) throws SQLException {
    Connection conn = DriverManager.getConnection("JDBC: mysql: / / 127.0.0.1:3306 / db_file? characterEncoding=UTF-8&useSSL=false"."root"."");
    Statement stmt = conn.createStatement();
    ResultSet rs = stmt.executeQuery("SELECT * FROM script_dir LIMIT 1");
    while (rs.next()) {
        System.out.println(rs.getString(1)); }}Copy the code

As you can see, we have successfully obtained the database connection using the custom Driver class and replaced the original Driver class. The details need to be checked again, because it involves the interface type and Return after getting the connection, etc.

Console output:

[Kerwin] Perform database connection...Copy the code


Application scenarios of SPI

After understanding its basic usage and principles, the mystery of SPI suddenly becomes a virtual one, which boils down to the mechanism by which the JDK dynamically initializes and executes interface implementation classes selectively configured at specified locations based on conventions.


Do you want to use SPI for daily development?

From the above, we can directly experience the benefits of SPI mechanism. It can play the role of policy selection, dynamic initialization and decoupling. Should we use it in common project development? I personally do not recommend using SPI, mainly because we can use a more elegant alternative to SPI mechanism, such as:

  • Dynamic initialization, policy selection = “We can use policy + factory mode to achieve dynamic policy selection, with ZK to achieve dynamic initialization (enable/disable)
  • Decoupling = is based on good design and can be easily decoupled

Based on the above solution, the project code can be guaranteed to have the benefits of SPI while being more readable and reducing the cost of understanding.


Should FRAMEWORK/component tool development use SPI?

There is no doubt that many frameworks and tools are implemented using SPI. The introduction of SPI allows for a separation of service interfaces and service implementations, enabling decoupling and extensibility.

For example, the Sharding-JDBC encryption algorithm interface only provides AES and MD5 encryption methods. Projects that need other encryption methods can use SPI mechanism to write their encryption methods into the framework and then call them as needed, which is more convenient both in use and maintenance.

Because the SPI version of Java implementation is relatively rough and violent, it will instantiate all the interface implementation classes. Therefore, some frameworks will encapsulate and optimize Java SPI, such as Dubbo, which changes the name of the whole class in the configuration file to the way of key-value pair, so as to meet the need of loading on demand. At the same time, the features of IOC and AOP, adaptive extension and other mechanisms are added.

As we can see from the above work, the mechanism of SPI is not mysterious, and it can be easily encapsulated if individuals need simple encapsulation.


Learn SPI’s ideas

SPI mechanism has a certain inevitability. For example, the sharding-JDBC encryption algorithm mentioned above, only real users know what they really need, so the ability to give part of the decision (implementation) to the user is a must. Otherwise, the framework or tools, in order to meet all situations, The code is bound to become very bloated. The key design principles are:

Dependency inversion principle (program for an abstraction layer, not a concrete class)

We also want to think about how to design interfaces in the day-to-day development, how to rely on programming abstraction layer, reduce the coupling between the implementation class and, in the same way, in order to achieve this requirement, we will go to learn the knowledge of design patterns, design principles, such as, the best practice to understand all kinds of design patterns, to optimize the code step by step, in this recommend my previous article: Design patterns: From why principles are needed to practice (with knowledge maps).


conclusion

So far, we have understood what SPI is and how it works. We are familiar with its typical cases, as well as its application scenarios and design concepts. Here are some specific suggestions:

  1. SPI mechanism is one of the necessary capabilities of framework/tool level projects, and those who aspire to be senior engineers must understand its design philosophy and implementation principles
  2. The core idea of SPI is to put some of the decisions (implementation) in the hands of the user, dependency inversion
  3. After understanding the advantages and characteristics of SPI, we can completely use other schemes to achieve better results in a single project. We should not use it just for the sake of using it
  4. When developing or using certain middleware/tools in the future, paying attention to whether it provides the relevant SPI interface may be more effective with less effort.


If you find this helpful:

  1. Of course, give me a thumbs up
  2. In addition, you can search and follow the public account “Yes Kerwin ah,” and go on the road of technology together ~ 😋


Refer to the article

  1. ServiceLoader
  2. Java SPI mechanism in depth and source code analysis