Java深入讲解SPI的使用

 更新时间:2022年06月16日 08:58:50   作者:hi wei  
SPI英文全称为Service Provider Interface,顾名思义,服务提供者接口,它是jdk提供给“服务提供厂商”或者“插件开发者”使用的接口

什么是Java SPI

    SPI的全名为:Service Provider Interface。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下 Java SPI 机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。

    Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要Java SPI 的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader。

Java SPI使用demo

定义一个接口:

package com.hiwei.spi.demo;
public interface Animal {
    void speak();
}

创建两个实现类:

package com.hiwei.spi.demo;
public class Cat implements Animal {
    @Override
    public void speak() {
        System.out.println("喵喵喵!");
    }
}
package com.hiwei.spi.demo;
public class Dog implements Animal {
    @Override
    public void speak() {
        System.out.println("汪汪汪!");
    }
}

在resources目录下创建META-INF/services目录:

创建以接口类路径命名的文件,文件中添加实现类路径:

com.hiwei.spi.demo.Cat
com.hiwei.spi.demo.Dog

使用

package com.hiwei.spi;
import com.hiwei.spi.demo.Animal;
import java.sql.SQLException;
import java.util.ServiceLoader;
public class SpiDemoApplication {
    public static void main(String[] args){
    	//会根据文件找到对应的实现类
        ServiceLoader<Animal> load = ServiceLoader.load(Animal.class);
        //执行实现类方法
        for (Animal animal : load) {
            animal.speak();
        }
    }
}

执行结果:

上面我们可以看到java spi会帮助我们找到接口实现类。那么实际生产中怎么使用呢? 将上面的代码打成jar,然后在其它项目中引入,同样的目录下创建文件,并写上自己实现类的路径:

本项目实现类:

package com.example.demo;
import com.hiwei.spi.demo.Animal;
public class Pig implements Animal {
    @Override
    public void speak() {
        System.out.println("哼哼哼!");
    }
}

代码中,我们调用jar中的main方法:

package com.example.demo;
import com.hiwei.spi.SpiDemoApplication;
public class DemoApplication {
    public static void main(String[] args) {
        SpiDemoApplication.main(args);
    }
}

执行结果:

可以看见自定义的实现类也被执行了。在实际生产中,我们就可以使用java spi面向接口编程,实现可插拔。

SPI在JDBC中的应用

以最新的mysql-connector-java-8.0.27.jar为例

<!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.27</version>
</dependency>

在使用JDBC连接数据库时,只需要使用:

DriverManager.getConnection("url", "username", "password");

DriverManager有静态方法:

    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }

看下loadInitialDrivers()方法,其中有:

AccessController.doPrivileged(new PrivilegedAction<Void>() {
            public Void run() {
				//获取Driver.class的实现类
                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.
                 */
                try{
                    while(driversIterator.hasNext()) {
                        driversIterator.next();
                    }
                } catch(Throwable t) {
                // Do nothing
                }
                return null;
            }
        });

可以看见,会根据java spi获取Driver.class的实现类,可以在mysql-connector-java-8.0.27.jar下面看到,定义的文件:

程序会根据文件找到对应的实现类,并连接数据库。

SPI在sharding-jdbc中的应用

    sharding-jdbc是一款用于分库分表的中间件,在数据库分布式场景中,对于主键生成要保证唯一性,主键生成策略有很多种实现。sharding-jsbc在主键生成上就使用了SPI进行扩展。

下面看下sharding-jdbc源码在主键生成上是怎么应用的: 源码中的 ShardingRule.class主要封装分库分表的策略规则,包括主键生成。看下createDefaultKeyGenerator方法:

//生成默认主键生成策略
private ShardingKeyGenerator createDefaultKeyGenerator(final KeyGeneratorConfiguration keyGeneratorConfiguration) {
     //SPI服务发现
     ShardingKeyGeneratorServiceLoader serviceLoader = new ShardingKeyGeneratorServiceLoader();
     return containsKeyGeneratorConfiguration(keyGeneratorConfiguration)
             ? serviceLoader.newService(keyGeneratorConfiguration.getType(), keyGeneratorConfiguration.getProperties()) : 					serviceLoader.newService();
    }

继续看ShardingKeyGeneratorServiceLoader(),有静态代码块注册:

    static {
        //SPI: 加载主键生成策略
        NewInstanceServiceLoader.register(ShardingKeyGenerator.class);
    }

看下register方法:

    public static <T> void register(final Class<T> service) {
    	//服务发现
        for (T each : ServiceLoader.load(service)) {
            registerServiceClass(service, each);
        }
    }

看到这,真相大白,就是应用java spi机制。

我们再看下resources目录下:

可以看到有对应接口命名的文件,文件内容:

有两个实现,分别是雪花算法和UUID,这也对应了sharding-jdbc的提供的两种生成策略。我们在使用sharding-jdbc时,也可以自定义策略,便于扩展。 sharding-jdbc对于SPI的使用点还有很多,这里就不一一列举了。对于SPI机制,我们在工作中也可以实际应用,提升程序的可扩展性。

扩展

以上是Java SPI的解析。其实SPI机制在很多地方都有用到,只是以不同的形式应用,具体的实现略有不同。例如dubbo中也有类似的spi机制;springboot的自动装配,也使用了spi机制:

springboot自动装配:

定义文件:

文件中声明需要发现的类:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.hiwei.valve.ValveAutoConfiguration

springboot的扫描文件,装配对应的类:

	private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
		Map<String, List<String>> result = cache.get(classLoader);
		if (result != null) {
			return result;
		}
		result = new HashMap<>();
		try {
			//加载文件中的类
			Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
			while (urls.hasMoreElements()) {
				URL url = urls.nextElement();
				UrlResource resource = new UrlResource(url);
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				for (Map.Entry<?, ?> entry : properties.entrySet()) {
					String factoryTypeName = ((String) entry.getKey()).trim();
					String[] factoryImplementationNames =
							StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
					for (String factoryImplementationName : factoryImplementationNames) {
						result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
								.add(factoryImplementationName.trim());
					}
				}
			}
			// Replace all lists with unmodifiable lists containing unique elements
			result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
					.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
			cache.put(classLoader, result);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" +
					FACTORIES_RESOURCE_LOCATION + "]", ex);
		}
		return result;
	}

FACTORIES_RESOURCE_LOCATION的值:

SPI在Java开发中是个很重要的设计,所以我们一定要熟练掌握。

到此这篇关于Java深入讲解SPI的使用的文章就介绍到这了,更多相关Java SPI内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Spring Cloud 服务网关Zuul的实现

    Spring Cloud 服务网关Zuul的实现

    这篇文章主要介绍了Spring Cloud 服务网关Zuul的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-07-07
  • 解决启用 Spring-Cloud-OpenFeign 配置可刷新项目无法启动的问题

    解决启用 Spring-Cloud-OpenFeign 配置可刷新项目无法启动的问题

    这篇文章主要介绍了解决启用 Spring-Cloud-OpenFeign 配置可刷新项目无法启动的问题,本文重点给大家介绍Spring-Cloud-OpenFeign的原理及问题解决方法,需要的朋友可以参考下
    2021-10-10
  • Intellij IDEA 的maven项目通过Java代码实现Jetty的Http服务器(推荐)

    Intellij IDEA 的maven项目通过Java代码实现Jetty的Http服务器(推荐)

    Jetty是一个轻量级的高度可扩展的基于 java的web服务器和servlet引擎,这篇文章主要介绍了Intellij IDEA 的maven项目通过Java代码实现 Jetty 的 Http服务器从而获取到 Http 的 Get 和 Post 请求,需要的朋友可以参考下
    2022-07-07
  • Java Optional的判空操作详解

    Java Optional的判空操作详解

    JAVA在1.8版本推出Optional,官方文档将其描述为可能包含或不包含非空值的容器对象,目前Optional用于避免程序出现异常NullPointerException,感兴趣的可以了解一下
    2022-09-09
  • Java 如何通过Magic 魔数获取文件类型

    Java 如何通过Magic 魔数获取文件类型

    魔数有很多种定义,这里我们讨论的主要是在编程领域的定义,文件的起始几个字节的内容是固定的,本文给大家介绍Java Magic 魔数获取文件类型的相关知识,感兴趣的朋友一起看看吧
    2023-11-11
  • MyBatis Generator生成数据库模型实现示例

    MyBatis Generator生成数据库模型实现示例

    这篇文章主要为大家介绍了MyBatis Generator生成数据库模型实现示例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-12-12
  • 详解Java读取本地文件并显示在JSP文件中

    详解Java读取本地文件并显示在JSP文件中

    这篇文章主要介绍了详解Java读取本地文件并显示在JSP文件中的相关资料,这里提供实例帮助大家实现这样的功能,希望能帮助到大家,需要的朋友可以参考下
    2017-08-08
  • 流式图表拒绝增删改查之框架搭建过程

    流式图表拒绝增删改查之框架搭建过程

    这篇文章主要为大家介绍了流式图表拒绝增删改查之框架搭建过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-04-04
  • 解决SpringBoot webSocket 资源无法加载、tomcat启动报错的问题

    解决SpringBoot webSocket 资源无法加载、tomcat启动报错的问题

    这篇文章主要介绍了解决SpringBoot webSocket 资源无法加载、tomcat启动报错的问题,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-11-11
  • IDEA生成servlet程序的实现步骤

    IDEA生成servlet程序的实现步骤

    这篇文章主要介绍了IDEA生成servlet程序的实现步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2021-03-03

最新评论