Suddenly see the new vocabulary, understand! The word feels very high end, actually is not what high technology.
The SPI, or Service Provider Interface, is a built-in Java Service discovery mechanism (this Service discovery mechanism is not the Service discovery mechanism in the microservice registry).
In simple terms, Java’s SPI mechanism means that an external implementation of an interface needs to be loaded. As long as the implementation is configured in the meta-INF/Services folder in the classPath, the user can automatically load the classes defined in the file.
There are three important roles in SPI:
- interface
- The configuration file
- ServiceLoader Reflection obtain
We can intuitively understand: SPI is a function provided by the JDK. The ServiceLoader in the JDK can read a file configured by a third party in the Meta-INF/Services folder and automatically load the classes defined in the file, so that when we introduce a third party package, Implementation classes provided in third-party packages can be used without any hard coding.
See JDBC Case for details on why this mechanism exists.
SPI feels a bit like Spring’s IOC, where you specify an interface and automatically assemble the implementation classes for that interface through the JDK’s ServiceLoader. Control of the assembly is moved outside the program (in a third-party package), and the implementation is not dynamically specified in the program when the module is assembled. So the core idea of SPI is decoupling, which is especially important in modular design.
SPI makes it possible for many framework extensions, such as Dubbo, Spring, SpringBoot related starter components, and JDBC. Note the key word: frame! We generally use SPI is also used in the framework, because some things in the framework only need to define standards, and then the specific implementation needs to be selected according to different scenarios, in this case, the framework can use SPI interface to extend its own functionality.
BTW, JDK’s SPI mechanism has some disadvantages, frameworks like Dubbo have their own SPI implementation.
The JDK SPI of actual combat
-
You need an interface
public interface SpiService { void exe(a); } Copy the code
-
Defining multiple implementations
public class SpiServiceImplA implements SpiService { public void exe(a) { System.out.println("I am A..."); }}Copy the code
-
Configuration/meta-inf/services/work. Lollipops. Tutorial.. Java SpiService
work.lollipops.tutorial.java.SpiServiceImplA work.lollipops.tutorial.java.SpiServiceImplB Copy the code
-
test
ServiceLoader<SpiService> spiServices = ServiceLoader.load(SpiService.class); spiServices.forEach(SpiService::exe); Copy the code
Output:
I am A...
I am B...
I am A...
I am B...
Copy the code
Analysis of ServiceLoader source code
Sun.misc. Service source code belongs to Sun, we can not see, so analyze ServiceLoader.
The implementation of this class is simple and can be divided into three large chunks:
public final class ServiceLoader<S> implements 可迭代<S>{
/ / property values
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator;
// Initialize the Loader
public static <S> ServiceLoader<S> load(Class<S> service) {... }private ServiceLoader(Class<S> svc, ClassLoader cl) {... }// The LazyIterator class
private class LazyIterator implements Iterator<S> {... }// Iterator
public Iterator<S> iterator(a) {...}
}
Copy the code
Initialize the
Serviceloader.load (spiservice.class) essentially calls:
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// The interface to be loaded
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// Configure the classloader
loader = (cl == null)? ClassLoader.getSystemClassLoader() : cl;// Access controlacc = (System.getSecurityManager() ! =null)? AccessController.getContext() :null;
// Clear lookupIterator and initialize LazyIterator: new LazyIterator(service, loader);
reload();
}
Copy the code
The class objects to be loaded by the SpiService. Class interface are lazily loaded. After initialization, the ServiceLoader internal lookupIterator holds the lazy-loaded iterator.
Call the iteration
public Iterator<S> iterator(a) {
return new Iterator<S>() {
/ /...
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); }}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); }}public boolean hasNext(a) {
if (knownProviders.hasNext())
return true;
// Call the method of the LazyIterator
return lookupIterator.hasNext();
}
public S next(a) {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
/ /...
};
}
Copy the code
This is a forward, actually calling the iteration method of the LazyIterator.
Start iterative acquisition (core)
LazyIterator hasNext and next actually call hasNextService and nextService respectively.
private boolean hasNextService(a) {
// nextName is used to store the fully qualified class name of the interface implementation class
if(nextName ! =null) {
return true;
}
if (configs == null) {
try {
String fullName = PREFIX + service.getName();
// If the class loader is not obtained
if (loader == null)
/ / from the specified path < / / meta-inf/services/work. Lollipops. Tutorial. Java SpiService > below loading configuration
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 is also an iterator. Parse is parsing a file and returning what is read from the file
pending = parse(service, configs.nextElement());
}
// The implementation class of the interface is fully qualified
nextName = pending.next();
return true;
}
Copy the code
HasNextService is read our configuration / / meta-inf/services/work. Lollipops. Tutorial.. Java SpiService files, iterative reads the configuration of the fully qualified class name inside.
private S nextService(a) {
if(! hasNextService())throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class < ? > c = null;
try {
// Reflect the loading 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 {
S p = service.cast(c.newInstance());
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
NextService creates a concrete implementation class through reflection and returns it.
In essence, a class configured in a fixed location is specified by reflection loading
SPI Application Scenarios
JDBC is used as an example.
JDBC
Before JDBC4.0, you need to write class.forname (” XXX “) to load the mysql driver implementation. After JDBC4.0, you need to discover the driver provider based on spi. The JDK by METAINF/services/Java. SQL. Driver file specifies the implementation class to load the Driver implementation class.
- JDBC4.0 before
Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//....
Copy the code
The forName is hard coded, so it’s not very elegant. What we pursue is interface oriented programming, like this:
- After JDBC4.0
//Class.forName("com.mysql.cj.jdbc.Driver");
conn = DriverManager.getConnection(DB_URL, USER, PASS);
//....
Copy the code
The main load logic is in DriverManager. We can see that DriverManager has a static code block:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
Copy the code
The loadInitialDrivers method loads the driver implementation class through the SPI mechanism:
private static void loadInitialDrivers(a) {
/ /...
. / / spi call load Driver class in METAINF/services/Java, SQL. The Driver specified class
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
/ /...
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
Our mysql-connector-java.jar specifies
In essence, DriverManager helps us to configure the driver class from the mysql-connector-java.jar.
thinking
The JDK is the standardiser, and it specifies that anything that uses Java programs to read databases, for example, needs to comply with the JDBC standard. When the standard comes out, the major data manufacturers need to provide their own database implementation for this standard. Mysql-connector-java. jar is the mysql database manufacturers to implement JDK JDBC standard mysql implementation. (It is impossible for the JDK to provide a Driver implementation, it can only provide a specification, otherwise it is impossible to put various database vendors implementation in the JDK!)
Before THE SPI mechanism, there was no way to write a Java program to automatically discover the implementation that mysql was providing us, or when we wanted to load the mysql driver, We need to know what the fully qualified class name of the Driver provided by mysql (i.e. Com.mysql.cj.jdbc.driver) is in order to load it.
With the SPI mechanism, we don’t need to specify manually on the JDK side. This is done by the mysql provider JAR package. We just need to agree that ** specified configuration must be placed under METAINF/services/. ** This way, the JDK itself can load the implementation classes for that directory.
So SPI brings us benefits: by introducing third-party packages such as mysql-connector-java.jar, we can do without any hard coding such as class.forname (” com.mysql.cj.jdbc.driver “); You can use com.mysql.cj.jdbc.driver. The JDK itself can find that implementation class.
Break parental delegation?
Remember what a parent delegate is: when a classloader needs to load a.class file, it first delegates the task to its parent classloader, recurses the operation, and only loads the class itself if the parent classloader doesn’t.
Take JDBC again as an example: Driver and DriverManager are both in the JDK. They are loaded by the BootstrapClassLoader, while com.mysql.cJ.jdbc. Driver is a third-party implementation. It is loaded by the AppClassLoader system class loader.
We can implement:
System.out.println(DriverManager.class.getClassLoader());
System.out.println(Driver.class.getClassLoader());
System.out.println(Connection.class.getClassLoader());
System.out.println(conn.getClass().getClassLoader());
// Return the result
null
null
null
sun.misc.Launcher$AppClassLoader@18b4aac2
Copy the code
You can see that the Driver and DriverManager and Connection are loaded through the BootstrapClassLoader (Java could not get the loader so null is returned). But the classloader for Conn.getClass () is AppClassLoader.
The JVM first receives a class load request from DriverManager and delegates it up to the BootstrapClassLoader, which loads the Connection and Driver. However, the Driver implementation classes are provided by various vendors. If these implementation classes are placed in the JDK, it is not a problem. The BootstrapClassLoader will load them. However, these are not under the JDK lib and BootstrapClassLoader cannot load them.
When The BootstrapClassLoader loads DriverManager, DriverManager internally uses classes in the JDK directory. All DriverManager classes should be loaded by the BootstrapClassLoader (that is, the entire process of loading DriverManager should be done by the BootstrapClassLoader, because these classes are in the JDK lib). But the Connection we get is loaded by the AppClassLoader. This means that the BootstrapClassLoader delegates its child AppClassLoader to load the third party driver class while loading DriverManager. So SPI breaks the parent delegation mechanism (the parent can only delegate to the parent, and the parent can not be loaded by the child, but in this case, the parent delegated the App loader to load the third party package, i.e. the parent delegated the child!). .
However, we output:
System.out.println(conn.getClass());
/ / output
class com.mysql.cj.jdbc.ConnectionImpl
Copy the code
It turns out that the Connection class is essentially a com.mysql class! “Loading a third party class by the AppClassLoader does not seem to violate the model”, which is the point of contention that SPI breaks delegation. That makes sense.
But from a classloading point of view, in terms of parent delegation, I think it’s broken! Here’s why:
- DriverManager is a JDK thing that is loaded by BootstrapClassLoader. It is not possible for the DriverManager loaded by the BootstrapClassLoader to get the implementation class loaded by the AppClassLoader. It is not visible to the BootstrapClassLoader.
- We can imagine that these class loaders are like nesting dolls, with the BootstrapClassLoader at the very bottom, and the DriverManager classes at the very bottom. When it loads DriverManager, it only loads what it can see in JDK /lib. But SPI is different. When DriverManager is loaded, the third party package is loaded, which is the outermost part of the doll. This clearly does not conform to the rules of nesting dolls.
- The purpose of the parent delegate is to repeatedly load the class and prevent the core class from being overwritten. In appearance, the CORE Driver and Connection of the JDK seem to be overwritten by an outer third party implementation.
- And mainly because the parent delegate model is not a mandatory model, Java passes through a thread context classloader
setContextClassLoader()
The default is the application class loader and thenThread.current.currentThread().getContextClassLoader()
Get the class loader to load.
public static <S> ServiceLoader<S> load(Class<S> service) {
// Set the context classloader
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
Copy the code
We can load classes by getting a ClassLoader from the ThreadContext. This is the equivalent of a hole in the nesting doll. The hole is a pipe and the only thing inside the pipe is the loader (usually the AppClassLoader).
“Reference”
- www.zhihu.com/question/49…
- Segmentfault.com/a/119000002…