Use spring boot to load MySQL driven process through SPI mechanism

Time:2021-9-15

SPI is a flexible mechanism for loading plug-ins provided by JDK, which separates the interface and implementation. For common database drivers, we only need to introduce corresponding database dependency packages (such as MySQL connector Java and ojdbc6 driver for Oracle) into the spring system, Then, the corresponding data source configuration in the YML or properties configuration file can automatically use the corresponding SQL driver,

For example, MySQL configuration:


spring:
  datasource:
    url: jdbc:mysql://localhost:3306/xxxxx?autoReconnect=true&useSSL=false&characterEncoding=utf-8&serverTimezone=Asia/Shanghai
    username: dev
    password: xxxxxx
    platform: mysql

The SPI mechanism is like the JDK classloader. If you do not reference it, it will not be automatically loaded into the JVM. If the following two SQL driver dependencies are not introduced, the oracle and MySQL drivers will be loaded:

<!-- Oracle driver -- >
        <dependency>
            <groupId>com.oracle</groupId>
            <artifactId>ojdbc6</artifactId>
            <version>12.1.0.1-atlassian-hosted</version>
        </dependency>
 
        <!-- MySQL driver -- >
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>

Because of the SPI mechanism of JDK, it is so simple for us to use the corresponding driver in spring projects,

We just need to do two things:

1. Introduce the corresponding driver dependency in the POM file

2. Configure the corresponding data source in the configuration file

So who triggered the database driven SPI loading mechanism in the spring project? To illustrate this problem, let’s talk about the working mechanism of the JDK SPI. The JDK SPI completes the loading of the corresponding interface implementation class through the serviceloader class. Take the database driver we want to talk about,

Serviceloader will look for those classes that meet the following conditions in the classpath of the spring project:

1. Meta-inf / services of these jar packages have a java.sql.driver file

Corresponding to the implementation class of the database driver corresponding to the database driver in the java.sql.driver file, for example, the MySQL driver corresponds to com.mysql.cj.jdbc.driver, as shown in the following figure:

For the specific implementation mechanism of SPI in JDK, you can read the internal class lazyitterer of serviceloader. The hasnextservice and nextservice methods of this class are the underlying mechanism of specific SPI mechanism.

Well, the SPI working mechanism of JDK is briefly outlined above. Next, let’s continue to see how the spring framework uses the SPI mechanism to complete the automatic management of database driver (loading and logoff). Next, let’s repeat the whole process of MySQL driver loading according to the sequence of events. The author uses springboot 2. X and the data source is Hikari, This is a data source from behind. With its excellent performance and monitoring mechanism, it has become the first data source after springboot 2. X,

The automatic loading mechanism of springboot by the partners who have used springboot, and the automatic assembly mechanism for data source configuration are also used,

Concrete class datasourceautoconfiguration

Note the red part above. Hikari and Tomcat (except datasourcejmxconfiguration) introduced here are all data source configurations. Let’s take a look first

Hikari data source configuration recommended by springboot:

/**
    **This is a configuration class that defines the bean method for creating hikaridatasource
   ***/
  	@Configuration
	@ConditionalOnClass(HikariDataSource.class)
	@ConditionalOnMissingBean(DataSource.class)
	@ConditionalOnProperty(name = "spring.datasource.type", havingValue = "com.zaxxer.hikari.HikariDataSource",
			matchIfMissing = true)
	static class Hikari {
 
		@Bean
		@ConfigurationProperties(prefix = "spring.datasource.hikari")
		public HikariDataSource dataSource(DataSourceProperties properties) {
            //Use the data source configuration in the configuration file to create a Hikari data source
			HikariDataSource dataSource = createDataSource(properties, HikariDataSource.class);
			if (StringUtils.hasText(properties.getName())) {
				dataSource.setPoolName(properties.getName());
			}
			return dataSource;
		}
 
	}

Since the configuration of Hikari is first introduced into the datasourceautoconfiguration class, datasource is not created. If conditionalonmissingbean and other conditions are met, the configuration class will be used to create the data source. OK, next, let’s see how createdatasource creates the data source,

How does this process relate to SPI

abstract class DataSourceConfiguration {
 
	@SuppressWarnings("unchecked")
	protected static <T> T createDataSource(DataSourceProperties properties, Class<? extends DataSource> type) {
        //Create a datasourcebuilder object (builder mode in design mode) using datasourceproperties data source configuration
		return (T) properties.initializeDataSourceBuilder().type(type).build();
	}
 
 
   //Let's take a look at the build method of datasourcebuilder
    public T build() {
        //In this example, type returns the com.zaxxer.hikari.hikaridatasource class
		Class<? extends DataSource> type = getType();
        //Instantiate the hikaridatasource class
		DataSource result = BeanUtils.instantiateClass(type);
		maybeGetDriverClassName();
        //The bind method will call the attribute setting and reflection mechanism when setting the driverclassname attribute
		bind(result);
		return (T) result;
	}
 
 
   //The hikariconfig method, hikaridatasource, inherits from the hikariconfig class
public void setDriverClassName(String driverClassName)
   {
      checkIfSealed();
 
      Class<?> driverClass = null;
      ClassLoader threadContextClassLoader = Thread.currentThread().getContextClassLoader();
      try {
         if (threadContextClassLoader != null) {
            try {
                //Load the class corresponding to driverclassname, i.e. com.mysql.cj.jdbc.driver class, which is the driver class corresponding to MySQL
               driverClass = threadContextClassLoader.loadClass(driverClassName);
               LOGGER.debug("Driver class {} found in Thread context class loader {}", driverClassName, threadContextClassLoader);
            }
            catch (ClassNotFoundException e) {
               LOGGER.debug("Driver class {} not found in Thread context class loader {}, trying classloader {}",
                            driverClassName, threadContextClassLoader, this.getClass().getClassLoader());
            }
         }
 
         if (driverClass == null) {
            driverClass = this.getClass().getClassLoader().loadClass(driverClassName);
            LOGGER.debug("Driver class {} found in the HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
         }
      } catch (ClassNotFoundException e) {
         LOGGER.error("Failed to load driver class {} from HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
      }
 
      if (driverClass == null) {
         throw new RuntimeException("Failed to load driver class " + driverClassName + " in either of HikariConfig class loader or Thread context classloader");
      }
 
      try {
         //Create the com.mysql.cj.jdbc.driver object. Next, let's look at what happened during the creation of the object by com.mysql.cj.jdbc.driver
         driverClass.newInstance();
         this.driverClassName = driverClassName;
      }
      catch (Exception e) {
         throw new RuntimeException("Failed to instantiate class " + driverClassName, e);
      }
   }
 
 
//Com.mysql.cj.jdbc.driver class
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    //
    // Register ourselves with the DriverManager
    //
    static {
        try {
            //Call the drivermanager to register itself. The drivermanager uses copyonwritearraylist to store the loaded database drivers. When creating a connection, it will eventually call the getconnection method of the drivermanager. This is really database oriented, but spring JDBC helps us shield these details
            java.sql.DriverManager.registerDriver(new Driver());
        } catch (SQLException E) {
            throw new RuntimeException("Can't register driver!");
        }
    }

The drivermanager class has been reached above. Is there any secret in the drivermanager class? Go on,

Take a look at the important methods of drivermanager:

static {
        //Static method, which will be called when the JVM loads the class for the first time
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
 
    //Loadinitialdrivers method of drivermanager class
 
    private static void loadInitialDrivers() {
        String drivers;
        try {
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
 
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
             
                //This is the final answer. Finally, the driver provided by SPI mechanism is loaded through serviceloader. This paper uses two methods, one is MySQL and the other is Oracle. Note that this method will only be called when the JVM loads the drivermanager class for the first time, so all database drivers will be loaded at one time
                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.
                 */
                 //The following code is where the database driver loading is really completed. It corresponds to the lazyitterer class of serviceloader class. Therefore, just look at the hasnext level next method of this class. It has been mentioned above and will not be repeated here
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });
 
        println("DriverManager.initialize: jdbc.drivers = " + drivers);
 
        if (drivers == null || drivers.equals("")) {
            return;
        }
        String[] driversList = drivers.split(":");
        println("number of Drivers:" + driversList.length);
        for (String aDriver : driversList) {
            try {
                println("DriverManager.Initialize: loading " + aDriver);
                Class.forName(aDriver, true,
                        ClassLoader.getSystemClassLoader());
            } catch (Exception ex) {
                println("DriverManager.Initialize: load failed: " + ex);
            }
        }
    }

Well, the above has described how springboot uses the SPI mechanism of JDK to load the database driver. As for the calling process of drivermanager’s getconnection method, we can analyze it in a similar way. When the code stops at the break point, we can see the general information through the stack information of idea or eclipse.

I hope this article can help some people understand the whole process of MySQL driven loading and deepen their understanding of SPI mechanism. I hope I can give you a reference, and I hope you can support developpaer.