简言:今天在学习shardingsphere时候发现他们接口扩展使用SPI 模式来扩展接口具体实现内容,在很多框架中都有被广泛使用,特此进入记录学习。
定义
它是在JDK 1.6版本引入的,SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
SPI和API的使用场景
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
来个简单例子
代码结构
定义一个接口
/**
* @author zhaoyy
* @version 1.0
* @description TODO
* @date 2022/10/10
**/
public interface UserServer {
String getName();
}
两个实现类
/**
* @author zhaoyy
* @version 1.0
* @description TODO
* @date 2022/10/10
**/
public class UserServiceImpl01 implements UserServer {
@Override
public String getName() {
return "is name UserServiceImpl01 ";
}
}
```java
/**
* @author zhaoyy
* @version 1.0
* @description TODO
* @date 2022/10/10
**/
public class UserServiceImpl02 implements UserServer {
@Override
public String getName() {
return "is name UserServiceImpl02 ";
}
}
测试类:
import java.util.ServiceLoader;
/**
* @author zhaoyy
* @version 1.0
* @description TODO
* @date 2022/10/10
**/
public class Test {
public static void main(String[] args) {
ServiceLoader<UserServer> userServers = ServiceLoader.load(UserServer.class,
Thread.currentThread().getContextClassLoader());
for (UserServer userServer : userServers) {
System.out.println(userServer.getName());
}
}
}
工程resoures/META-INFO/services
创建文件:com.zyy.java.server.loader.UserServer 没有的新建
文件内容:
com.zyy.java.server.loader.UserServiceImpl01
com.zyy.java.server.loader.UserServiceImpl02
输出结果:
这样一个简单的spi的demo就完成了。可以看到其中最为核心的就是通过ServiceLoader这个类来加载具体的实现类的。
SPI原理解析
我们来看下源码,源码比较多只复制重要的部分,感兴趣的小伙伴可以自己深入研究下,下面贴出比较直观的spi加载的主要流程供参考:
public final class ServiceLoader<S>
implements Iterable<S>
{
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;
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
// Private inner class implementing fully-lazy provider lookup
//
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;
}
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
}
}
来参考下sharding SIP结构
下面介绍的是表主键id设置参数可以是:UUID,SNOWFLAKE两种类型
简单介绍下uuid实现过程:
application.properties 文件中我们需要指定分表主键id生成类型
spring.shardingsphere.sharding.tables.bill.key-generator.type=UUID
我们来看下他们具体源码
servers 配置文件下
对应接口ShardingKeyGenerator 实现类UUIDShardingKeyGenerator
package org.apache.shardingsphere.core.strategy.keygen;
import lombok.Getter;
import lombok.Setter;
import org.apache.shardingsphere.spi.keygen.ShardingKeyGenerator;
import java.util.Properties;
import java.util.UUID;
/**
* UUID key generator.
*
* @author panjuan
*/
@Getter
@Setter
public final class UUIDShardingKeyGenerator implements ShardingKeyGenerator {
private Properties properties = new Properties();
@Override
public String getType() {
return "UUID";
}
@Override
public synchronized Comparable<?> generateKey() {
return UUID.randomUUID().toString().replaceAll("-", "");
}
}
sharding 实现uid 很简单就是 UUID.randomUUID()
serviceLoader 如何实现的:
1 代码入口:
org.apache.shardingsphere.core.rule.ShardingRule# createDefaultKeyGenerator 默认初始化keyGenerator方法,初始加载接口实现类方法:serviceLoader.newService
源码方法:
回过来我们看上图对象: ShardingKeyGeneratorServiceLoader当前类完成 UUID,SNOWFLAKE 具体实现接口加载类到注册对象SERVICE_MAP 具体源码请看下面:
public final class ShardingKeyGeneratorServiceLoader extends TypeBasedSPIServiceLoader<ShardingKeyGenerator> {
// 静态方法完成 初始化对象是完成接口注册到NewInstanceServiceLoader#SERVICE_MAP 下面会介绍当前类
static {
NewInstanceServiceLoader.register(ShardingKeyGenerator.class);
}
public ShardingKeyGeneratorServiceLoader() {
super(ShardingKeyGenerator.class);
}
}
2 看下newService 是如何实现的,方法会调用loadTypeBasedServices 方法,结果 会返回集合泛类对象
,
NewInstanceServiceLoader.newServiceInstances(classType),该方法先从共享变量SERVER_MAP中查找,如果有直接返回,重新加载一次,下图片2, each.newInstance()会根据classtype类型去加载对应类,最终到了jdk Class方法中完成实现类具体加载过程
总结下
- 关于spi的详解到此就结束了,总结下spi能带来的好处:
- 不需要改动源码就可以实现扩展,解耦。
- 实现扩展对原来的代码几乎没有侵入性。
- 只需要添加配置就可以实现扩展,符合开闭原则。