Art is long, life is long
What is SPI
SPI is relative to API.
API refers to the interface provided by the application to the service caller, which is used to provide certain services and functions for the service caller.
SPI refers to the interface provided by the application to the service implementer to implement a certain service or function
Second, SPI mechanism
Service Provider Interface (SPI) is a built-in Service discovery mechanism in JDK.
We have already touched on this in our daily development, but we don’t really need it in general development, such as JDBC, Apache Logging, etc. SPI mechanism is used
Interfaces to these SPIs are provided by Java core libraries, and implementations of SPI are included in the CLASSPATH as jar packages that Java applications depend on. For example, the implementation of JDBC mysql is dependent on maven.
The problem, then, is that SPI’s interfaces are part of the Java core library and are loaded by the Bootstrap Classloader. SPI implementation classes are loaded by the System ClassLoader.
The bootloader cannot find the implementation class of SPI at load time because the parent delegate model states that the bootloader, BootstrapClassloader, cannot delegate the system classloader, AppClassLoader, to load it. At this point, how to solve the problem?
Thread-context class loading was born, which also broke the parent delegate model of class loaders and enabled programs to reverse class loading.
From the JDBC case study:
Connection conn = java.sql.DriverManager.getConnection(url, "name"."password");
Copy the code
Get the database connection as described above, then look at the DriverManager class
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
Copy the code
There is a static code block in the DriverManager class. It’s obviously going to call loadInitialDrivers, and then loadInitialDrivers
private static void loadInitialDrivers(a) {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run(a) {
return System.getProperty("jdbc.drivers"); }}); }catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run(a) {
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
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); }}}Copy the code
The key piece of information is the ServiceLoader utility class, which the JDK provides for service implementation lookup: java.util.Serviceloader. It is used for service implementation using the SPI mechanism
The Driver described above is in the java.sql.Driver package, provided by the JDK as a specification, which is an interface. How do other vendors do that?
Maven’s introduction of mysql dependencies automatically adds jars to the CLASSPATH /CLASSPATH
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
Copy the code
It provides the SPI mechanism configuration file (must be the meta-INF /services path in the classpath)
The service implementation provided happens to be the Driver of the mysql package we introduced
The file name must be the same as the name of the fully qualified interface class java.sql.Driver
Com.mysql.cj.jdbc.driver needs to implement java.sql.Driver
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
Thread context class loading
To get a database connection initially, the getConnection() method:
// Worker method called by the public getConnection() methods.
private static Connection getConnection( String url, java.util.Properties info, Class
caller) throws SQLException {
/* * When callerCl is null, we should check the application's * (which is invoking this class indirectly) * classloader, so that the JDBC driver class outside rt.jar * can be loaded from here. */ClassLoader callerCL = caller ! =null ? caller.getClassLoader() : null;
synchronized(DriverManager.class) {
// synchronize loading of the correct classloader.
if (callerCL == null) { callerCL = Thread.currentThread().getContextClassLoader(); }}if(url == null) {
throw new SQLException("The url cannot be null"."08001");
}
println("DriverManager.getConnection(\"" + url + "\")");
// Walk through the loaded registeredDrivers attempting to make a connection.
// Remember the first exception that gets raised so we can reraise it.
SQLException reason = null;
for(DriverInfo aDriver : registeredDrivers) {
// If the caller does not have permission to load the driver then
// skip it.
if(isDriverAllowed(aDriver.driver, callerCL)) {
try {
println(" trying " + aDriver.driver.getClass().getName());
Connection con = aDriver.driver.connect(url, info);
if(con ! =null) {
// Success!
println("getConnection returning " + aDriver.driver.getClass().getName());
return(con); }}catch (SQLException ex) {
if (reason == null) { reason = ex; }}}else {
println(" skipping: "+ aDriver.getClass().getName()); }}// if we got here nobody could connect.
if(reason ! =null) {
println("getConnection failed: " + reason);
throw reason;
}
println("getConnection: no suitable driver found for "+ url);
throw new SQLException("No suitable driver found for "+ url, "08001");
}
Copy the code
Emphasis: Thread. CurrentThread (). GetContextClassLoader ()
3. Use of SPI
3.1. Create a service interface
package com.jw.spi;
public interface Fruit {
String getName(a);
}
Copy the code
3.2. Create multiple service implementations
package com.jw.spi;
public class Apple implements Fruit {
@Override
public String getName(a) {
return "apple"; }}Copy the code
package com.jw.spi;
public class Banana implements Fruit {
@Override
public String getName(a) {
return "Banana"; }}Copy the code
The two service implementation classes here are for two service implementation parties, one implementing Apple and the other implementing Banana.
3.3. Create a configuration file
Create a/meta-INF /services directory under resource and create a file named com.jw.spi.fruit under services with the fully qualified name of the service interface
The content of the file is the fully qualified name of the service implementer class for the current service implementation
com.jw.spi.Apple
Copy the code
3.4. Create a test class
public class Test {
public static void main(String[] args) {
ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
Iterator<Fruit> it = s.iterator();
while(it.hasNext()){ System.out.println(it.next().getName()); }}}Copy the code
The execution result is as follows:
apple
Copy the code
4. Implementation principle of SPI
The main implementation of SPI relies on the ServiceLoader class. Use this class to load interface types (e.g. Fruit.class)
ServiceLoader<Fruit> s = ServiceLoader.load(Fruit.class);
Copy the code
This is a load method, but it does not load the specified service implementation class. Here are some preparations for loading the service implementation class:
The ServiceLoader class is a ServiceLoader class.
public final class ServiceLoader<S>
implements 可迭代<S>
{
// Scan directory prefixes
private static final String PREFIX = "META-INF/services/";
// The loaded class or interface
private final Class<S> service;
Class loaders used to locate, load, and instantiate classes implemented by the implementer
private final ClassLoader loader;
// Context object
private final AccessControlContext acc;
// Cache instantiated classes in the order in which they are instantiated
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// lazy search iterator
private LazyIterator lookupIterator;
/ / create the ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static <S> ServiceLoader<S> load(Class service, ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// Assign a value to service
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// Assign to loader
loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Assign accacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
reload();
}
public void reload(a) {
// Clear the providers cache
providers.clear();
// Assign a lookupIterator to create a LazyIterator delay iterator.
lookupIterator = new LazyIterator(service, loader);
}
// A private inner class that provides the loading and instantiation of all service classes
private class LazyIterator implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader; }... }Copy the code
Iterator
it = s.iterator();
public Iterator<S> iterator(a) {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
public boolean hasNext(a) {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
public S next(a) {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove(a) {
throw newUnsupportedOperationException(); }}; }Copy the code
The iterator method uses an anonymous inner class to define a new iterator, each of which is done by calling lookupIterator, a deferred iterator created earlier
The last step is iterative loading.
while(it.hasNext()){
System.out.println(it.next().getName());
}
Copy the code
public boolean hasNext(a) {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run(a) { returnhasNextService(); }};returnAccessController.doPrivileged(action, acc); }}private boolean hasNextService(a) {
if(nextName ! =null) {
return true;
}
if (configs == null) {
try {
// Get all the classes in the directory
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x); }}while ((pending == null) | |! pending.hasNext()) {if(! configs.hasMoreElements()) {return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
Copy the code
The hasNext method calls the hasNext method of the deferred iterator and internally calls the hasNextService method, in which it tries to find the resource file with the specified name (meta-INF /services/+ interface fully qualified name). And finish reading the contents of the file.
It then executes it.next(), which in turn calls hasNext, the corresponding method of the deferred iterator, internally calling the nextService method, whose main function is to load the class represented by the fully qualified name read from the file above. Generate instances and save them to providers.
public S next(a) {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run(a) { returnnextService(); }};returnAccessController.doPrivileged(action, acc); }}private S nextService(a) {
if(! hasNextService())throw new NoSuchElementException();
String cn = nextName;
nextName = null; Class<? > c =null;
try {
// reflection load class
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if(! service.isAssignableFrom(c)) { fail(service,"Provider " + cn + " not a subtype");
}
try {
/ / instantiate
S p = service.cast(c.newInstance());
// Save the instance to providers
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
Copy the code
Five, the summary
That concludes the detailed explanation of SPI and summarizes the benefits it can bring:
- It can be extended and decoupled without changing the source code.
- Implementing extensions is almost non-intrusive to the original code.
- You only need to add configuration to achieve the extension, in accordance with the open closed principle.