Java SPI机制


文章目录

  • Java SPI机制
  • 前言
  • 代码示例
  • ServiceLoader类源码解析
  • 成员
  • load部分
  • 迭代部分
  • JDBC连接数据库中的SPI
  • SPI在数据库连接中的具体体现
  • 总结


前言

SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现, 简单来说,它就是一种动态替换发现的机制, 举个例子来说, 有个接口,想运行时动态的给它添加实现,你只需要添加一个实现,然后,把新加的实现,描述给JDK知道就行啦(通过改一个文本文件即可)

java STOMP是什么 java speak什么意思_加载

代码示例

使用Eclipse完成一个简单的SPI例子需要三个步骤
1.完成一个接口(Animal),和两个接口的实现类 (Dog和Cat);

Animal接口:

package com.mec.aboutSPI;

public interface Animal {
	void shout();
}

Dog类:

package com.mec.aboutSPI;

public class Dog implements Animal{
	public Dog() {
		
	}
	@Override
	public void shout() {
		System.out.println("wangwang");
		
	}

}

Cat类

package com.mec.aboutSPI;

public class Cat implements Animal {
	public Cat() {
		
	}

	@Override
	public void shout() {
		System.out.println("miaomiao");

	}

}

2.在src目录下建立META-INF目录、再在META-INF目录下建立services目录

(具体为什么要这样建立,大家先接受就好,具体看到源码就明白了),在services目录下建立一个文件,文件的名字是接口的全限定名(包名 + 类名)。文件的内容为接口实现类的全限定名。一行写一个。

java STOMP是什么 java speak什么意思_类加载器_02


java STOMP是什么 java speak什么意思_java STOMP是什么_03

3.在主函数中进行测试

package com.mec.aboutSPI;

import java.util.ServiceLoader;

public class Text {

	public static void main(String[] args) {
		ServiceLoader<Animal> services = ServiceLoader.load(Animal.class);
		for (Animal service : services) {
			service.shout();
		}
	}
}

实验结果

java STOMP是什么 java speak什么意思_java STOMP是什么_04

ServiceLoader类源码解析

声明:关于System里面的方法,以我目前的能力没有办法看的很透彻,所以我只能大概的讲一讲里面的逻辑。

成员

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
    //这个就是个迭代器,LazyIterator 是一个内部类
    private LazyIterator lookupIterator;

load部分

load方法是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<S> service, ClassLoader loader) {
        return new ServiceLoader<>(service, loader);
    }
     
     private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //判断svc是否为null,是的话报错,不是的话将它赋值给service
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //判断类加载器cl是不是null,是的话,将系统类加载器赋值给loader,不是的话将cl赋值给它。
        
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
        reload();
    }
    
    public void reload() {
        //清空该Map
        providers.clear();
        //初始化迭代器,该类是ServiceLoader的一个内部类
        lookupIterator = new LazyIterator(service, loader);
    }

迭代部分

通过我们的例子可以明白,我们通过迭代遍历来使用每个实现类

这里我就不把所有源码展示出来了,因为我目前没有能力将每一行都解释出来。只能看个大概意思

  1. 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件。
    下面的代码可以间接的理解成hasNext方法下的内容
try {
        	//PREFIX是一个常量,就是 META-INF/services/
            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);
        }
  1. 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化,并且强传成该类实现的接口的类型, 最终加入到Map中。
    下面的代码可以间接的理解成next方法下的内容
String cn = nextName;
            nextName = null;
            Class<?> c = null;
            try {
            	//对该类进行加载
                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 {
            	//将该实现类的对象的类型强传成接口类型,并加入到Map中
                S p = service.cast(c.newInstance());
                providers.put(cn, p);

上述代码中有这么一行 c = Class.forName(cn, false, loader);

这里面的loader是上下文类加载器,首先我们要明确一个概念:假设A类中有对B类和C类的引用,如果B类和C类并没有被类加载器所加载,系统会使用加载A类的类加载器在双亲委托机制下去加载B类和C类。

**ServiceLoader类是由根类加载器即启动类加载器加载的,所以在该类中的引用的其它类也要使用根类加载器,但是根类加载器加载的是系统类,所以在ServiceLoader中引用的非系统类无法被加载,比如数据库驱动Driver类,这也算是双亲委托机制的一个局限性,所以为了打破这种尴尬,产生了线程上下文类加载器,这也就是上下文类加载器所存在的意义。初始线程的上下文类加载器就是系统类加载器。**关于上下文类加载,大家尽情期待我的下一篇博文吧!

JDBC连接数据库中的SPI

使用SPI的优点:
降低了程序的耦合性,比如你想替换一个接口的实现方式,你只需要更改配置文件就可以了,就拿连接数据库来说吧。

//我们连接数据库一般第一行都会写这个,目的是加载第三方架包的驱动
//如果使用SPI的话,这一行是可以不用写的,具体原因请往下看
Class.forName("com.mysql.jdbc.Driver");

下面的图片是将连接数据库jar包解压结果:

java STOMP是什么 java speak什么意思_类加载器_05


java STOMP是什么 java speak什么意思_java_06

我们发现它也存在META-INF目录

再看看它的配置文件

配置文件的名称是Java核心包中的接口

java STOMP是什么 java speak什么意思_java STOMP是什么_07

配置文件的内容是实现类的全限定名

java STOMP是什么 java speak什么意思_加载_08


通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类。

SPI在数据库连接中的具体体现

在rt.jar目录下有一个叫做java.sql的包,这是Java系统为用户提供的连接数据库的接口,以及少部分的实现类,我认为这就是Java提供给数据库厂商的API,数据库厂商去实现这些接口,最终做成jar包供用户使用。

java STOMP是什么 java speak什么意思_java_09


我们温故一下连接数据库的操作

//我们连接数据库一般第一行都会写这个,目的是加载第三方架包的驱动
//如果使用SPI的话,这一行是可以不用写的,具体原因请往下看
Class.forName("com.mysql.jdbc.Driver");
connection = DriverManager.getConnection(url, root, password);
//这样就算是连接上了
);

getConnection方法是DriverManager类的一个静态方法,当调用它时,会对DriverManager类进行初始化,当然会提前进行加载,初始化时会执行该类静态块的内容。

java STOMP是什么 java speak什么意思_类加载器_10

private static void loadInitialDrivers() {
        String drivers;
        try {
        	/*
        	先粗略说一下AccessController.doPrivileged,这就要涉及到Java安全模式,JVM为了防止
        	外部的代码可以随意访问本地资源设定了不同的权限和不同的域,类加载器加载类时,会把不同权限的类
        	加载道不同的域里,也就是说,一个类拥有它所在域的所有权限。如果一个域中的类进行了该域没有权限的操作
        	就会报错。为了外部的扩展,提供了doPrivileged方法,该方法执行的操作与其说是无视权限,还不如说它是拥有极高的权限
        	这部分的内容很深,很复杂,不我这样的小博主能解释清楚的,我认为当下的学习了解这是跟安全有关的就行。
        	*/
            drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
                public String run() {
                	/*
                	这里我们希望得到连接数据库的驱动的全限定名(com.mysql.jdbc.Driver);
                	这块我写了些代码做了测试,如果你提前没有进行
                	System.setProperty("jdbc.drivers","com.mysql.jdbc.Driver");操作
                	那你得到会是null,下面还有一层保险,就是使用SPI的方式
                	*/
                    return System.getProperty("jdbc.drivers");
                }
            });
        } catch (Exception ex) {
            drivers = null;
        }
        
        AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
            	//这里的内容在博文的前面已经说过,不再多说
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

                
                try{
                    while(driversIterator.hasNext()) {
                    	//这个方法中有一行执行了Class.ForName()的操作
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

        println("DriverManager.initialize: jdbc.drivers = " + drivers);

        //System.getProperty("jdbc.drivers");返回值为null所以程序该方法结束了
        if (drivers == null || drivers.equals("")) {
            return;
        }
        //如果你很早就进行了设置,那么这个将会对得到的字符串进行处理,并最终加载com.mysql.jdbc.Driver类
        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);
            }
        }

总结

SPI可以同过配置文件的方式,加载一个类或者,得到该实现类的对象,降低了程序间的耦合度。方便了我们对接口实现方式的替换,据说ServiceLoader多线程时不安全。