Jdk、Spring、Dubbo之SPI机制

目录

SPI简介

JdkSPI机制

SpringSPI机制

DubboSPI机制


SPI简介

SPI全称是Service Provider Interface是一种自动服务发现注册机制。本质是将接口实现类的全限定名配置在约定的配置文件,然后由服务加载器读取指定的加载文件名称,再动态加载实现类。这样我们可以在运行的时候动态替换接口实现,通过spi机制可以轻松实现我们应用程序的拓展功能。

那么spi到底有什么用?在哪里用到了呢?下面将会以mysql根据jdk中定义数据库驱动接入顶级接口 java.sql.Driver,实现了自己定义具体实现类加载,作为例子讲述spi的好处。

jdk在rt包中定义了数据库驱动顶级接口java.sql.Driver;

Jdk、Spring、Dubbo之SPI机制_第1张图片

 mysql在services中定义了数据库驱动具体实现的全限定名称com.mysql.jdbc.Driver;

Jdk、Spring、Dubbo之SPI机制_第2张图片

mysql实现jdk的Driver接口规范的mysql驱动实现类,通过这样的方式就可以通过loader获取加载的mysql驱动;

public class Driver extends NonRegisteringDriver implements java.sql.Driver {
    public Driver() throws SQLException {
    }

    static {
        try {
            DriverManager.registerDriver(new Driver());
        } catch (SQLException var1) {
            throw new RuntimeException("Can't register driver!");
        }
    }
}

这就是jdk spi 在数据库驱动中的应用,jdk定义数据库驱动接口规范和spi的加载机制。不同的数据库厂商不需要修改原有的jdk封装的代码,只需要根据接口和spi机制,就可以轻松实现自己的数据库驱动加载共功能,这就是spi的好处。

JdkSPI机制

原理

jdk的spi是通过ServiceLoader类实现,里面定义了实现文件加载路径“META-INF/services/”,获取配置文件名称解析获取配置接口中的全限定名称,然后通过全限定名称和反射将对象初始化。

private static final String PREFIX = "META-INF/services/"; 

private boolean hasNextService() {
            if (nextName != null) {
                return true;
            }
            if (configs == null) {
                try {
                    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;
        }

private S nextService() {
            if (!hasNextService())
                throw new NoSuchElementException();
            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 {
                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
        }

示例

首先定义任务执行的顶级接口TaskJob;

/**
 * 定时任务接口
 * @author zy
 */
public interface TaskJob {
	/**
	 * 执行任务
	 */
	void handle();

然后在创建了订单清除任务ClearOrderTaskJob;

/**
 * 订单清除任务
 * @author zy
 * @Description:
 */
public class ClearOrderTaskJob implements TaskJob {
	@Override
	public void handle() {
		System.out.println("############订单清除逻辑##############");
	}
}

制定接口实现加载的配置,在“META-INF/services/”中创建com.zy.order文件,添加ClearOrderTaskJob全限定名称路径地址(多个实现用回车分隔);

Jdk、Spring、Dubbo之SPI机制_第3张图片

ServiceLoader实现了迭代器并且重写了迭代器中的方法,所以可以通过迭代器获取到任务的具体实现类,进行后面的任务执行。

public class TaskJobFactory {
	
	public TaskJob getTaskJob() {
		ServiceLoader serviceLoader = ServiceLoader.load(TaskJob.class);
		Iterator iterator = serviceLoader.iterator();
		while (iterator.hasNext()) {
			return iterator.next();
		}
		return null;
	}
}

 现在新增加了一个ClearLogTaskJob的实现类,配置如下;

com.zy.order.ClearOrderTaskJob
com.zy.order.ClearLogTaskJob

这时获取任务执行发现执行的还是第一个任务执行器;这是因为代码中获取第一个配置的任务就返回执行。所以不管加了多少个实现都是第一个,那么是否可以改为获取最后一个配置返回呢? 

Jdk、Spring、Dubbo之SPI机制_第4张图片

 在同一个jar包是这种情况,但是在不同jar包中定义了不同实现,比如在a.jar包中定了ClearLogTaskJob,在b.jar包中定义了ClearOrderTaskJob,那么加载顺序就取决于在运行ClassPath配置,在前面加载的jar自然在前,在后面加载的自然在后。

比如按照下面启动脚本启动应用程序,任务ClearLogTaskJob就在第一个。

java -cp a.jar:b.jar:main.jar app.Main

 按照下面启动脚本启动应用程序,任务ClearOrderTaskJob就在第一个。

java -cp b.jar:a.jar:main.jar app.Main

 按照下面启动脚本启动应用程序,main.jar中有实现就会获取main中的实现类。

java -cp main.jar:b.jar:a.jar app.Main

由于加载顺序由用户指定,所以不管怎么配置都有可能导致加载不了用户想要的那个实现类。

所以jdk的spi劣势就是无法确定具体加载的哪一个实现类,都是一哈梭全部都给加载进来,无法指定需要加载的具体实现,仅靠配置顺序和classPath加载jar顺序是非常不严谨的

SpringSPI机制

spring的spi机制主要在springboot中使用,springboot通过固定的文件META-INF/spring.factories加载我们需要扩展的接口,并且支持一个接口有多个扩展实现,使用起来十分简单。

下面截取了springboot默认配置,其中是读取配置文件实现,事件监听实现,不同上下文实现。

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

然后统一由SpringFactoriesLoader读取spring.factories,获取对应的实现类进行实例化。

spring也支持多个spring.factories文件,加载顺序会按照classpath顺序依次加载,最后添加到一个ArrayList中。如果项目中定了自己的spring.factories文件,那么项目中的spring.factories将会首先加载。如果我们需要扩展某个接口,只需要新建一个spring.factories文件,然后配置自己定义的实现类全路径。

org.springframework.boot.env.PropertySourceLoader=\

com.my.dd.XmlPropertySourceLoader

DubboSPI机制

最后揭秘dubbo的spi机制,dubbo的spi完全是自己实现的一套机制,功能更加强大,也更加复杂,更加重。其主要逻辑封装在ExtensionLoader中,通过ExtensionLoader类我们可以加载指定的类。下面是ExtensionLoader的部分源码:

    private Map> loadExtensionClasses() {
        SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
        if (defaultAnnotation != null) {
            String value = defaultAnnotation.value();
            if (value != null && (value = value.trim()).length() > 0) {
                String[] names = NAME_SEPARATOR.split(value);
                if (names.length > 1) {
                    throw new IllegalStateException("more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
                }

                if (names.length == 1) {
                    this.cachedDefaultName = names[0];
                }
            }
        }

        Map> extensionClasses = new HashMap();
        this.loadFile(extensionClasses, "META-INF/dubbo/internal/");
        this.loadFile(extensionClasses, "META-INF/dubbo/");
        this.loadFile(extensionClasses, "META-INF/services/");
        return extensionClasses;
    }

dubbospi加载有一个加载优先级,优先加载内置的,然后加载外部的(internal),按照优先级顺序加载(dubbo)。 “META-INF/services/”这个地址可以看出dubbo支持jdk原生的spi,“META-INF/dubbo/internal/”dubbo内部的spi文件,“META-INF/dubbo/”自定义扩张dubbo文件配置,如果遇到重复的就跳过不会配置了。配置的样式如下:

threadlocal=com.alibaba.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory
lru=com.alibaba.dubbo.cache.support.lru.LruCacheFactory
jcache=com.alibaba.dubbo.cache.support.jcache.JCacheFactory

与jdk的spi机制不一样,dubbo的spi是通过键值对的方式进行配置,这样就解决了在jdkspi中无法指定具体实现类的问题,使用时按照需要在接口上标注@SPI注解。

@SPI("lru")
public interface CacheFactory {
    @Adaptive({"cache"})
    Cache getCache(URL var1);
}


public static void main(String[] args) {
		ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(CacheFactory.class);
		CacheFactory lru = extensionLoader.getExtension("lru");
	}

@SPI注解的value属性表示默认别名实现,比如上面的“lru”表示默认实现是LruCacheFactory类。然后通过getDefaultExtension()方法就可以获取到value属性上对应那个扩展实现了。

	    ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(CacheFactory.class);
		CacheFactory defaultExtension = extensionLoader.getDefaultExtension();
		String defaultExtensionName = extensionLoader.getDefaultExtensionName();

你可能感兴趣的:(伸缩式架构设计,java,spring,spring,boot,spi,dubbo)