本文代码地址:https://github.com/MSC419/msc-rpc-framework
0.实现的改进
1.增加SPI机制,方便我们为程序提供扩展功能
1.SPI机制
参考:
搞懂dubbo的SPI扩展机制 - 知乎 (zhihu.com)
从零开始实现简单 RPC 框架 2:扩展利器 SPI - 小新是也 - 博客园 (cnblogs.com)
https://github.com/Snailclimb/guide-rpc-framework
SPI:全称为 Service Provider Interface,是一种服务发现机制。在项目中一个功能接口可由多种不同技术实现,这称为程序的可扩展性,SPI机制就是为了在不修改程序框架的情况下实现程序扩展
1.1 Java SPI
1.1.1 示例
先看看Java SPI 的代码,代码目录:
1.定义接口
public interface Car {
void say();
}
2.编写实现类
public class Tractor implements Car {
@Override
public void say() {
System.out.println("我是拖拉机");
}
}
public class Automobile implements Car {
@Override
public void say() {
System.out.println("我是汽车");
}
}
3.META-INF/services 文件夹下创建一个文件,名称为功能接口Car 的路径com.wx.car.Car,文件内容为接口实现类的路径名
com.wx.car.impl.Automobile
com.wx.car.impl.Tractor
4.测试
public static void main(String[] s){
System.out.println("======this is SPI======");
ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
Iterator<Car> cars = serviceLoader.iterator();
while (cars.hasNext()) {
cars.next().say();
}
}
从测试结果可以看出,两个实现类被成功加载,并输出了相应的内容。但我们并没有在代码中显示指定Car的类型,这就是java原生的SPI机制在发挥作用。
1.1.2 SPI作用
将接口实现类的全限定名写在META-INF/services/接口类名的配置文件中,服务加载器会读取配置文件,实例化实现类。在运行时,动态为接口替换实现类。这个特性让我们可以很容易为程序提供拓展功能,达到动态可插拔的效果。
1.1.3 源码分析
通过追踪查看ServiceLoader.load() 源码,发现程序会读取META-INF/services/接口路径名 文件
private static final String PREFIX = "META-INF/services/";
...
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();
再通过反射Class.forName()加载类对象,并用instance()方法将类实例化,从而完成了服务发现。
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
...
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);
}
再通过serviceLoader.iterator() 将providers 里的实现类实例转为Iterator ,供调用者使用
public Iterator<S> iterator() {
return new Iterator<S>() {
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
...
Java SPI 在查找实现类的时候,需要遍历配置文件中定义的所有实现类,而这个过程会把所有实现类都实例化。一个接口如果有很多实现类,而我们只需要其中一个的时候,就会产生其他不必要的实现类。
1.2 Dubbo SPI
1.2.1 示例
将Java项目引入mevan框架,引入Dubbo依赖,接口Car上加上@SPI ,配置文件com.wx.car.Car放入META-INF/dubbo 路径下。注意新建文件的时候一级一级新建,确保是两级目录META-INF/dubbo,而不是一级目录META-INF.dubbo。Dubbo SPI 是通过键值对的方式进行配置,这样就可以按需加载指定的实现类。
automobile=com.wx.car.impl.Automobile
tractor=com.wx.car.impl.Tractor
测试:
public static void main(String[] s){
System.out.println("======dubbo SPI======");
ExtensionLoader<Car> extensionLoader =
ExtensionLoader.getExtensionLoader(Car.class);
Car automobile = extensionLoader.getExtension("automobile");
automobile.say();
Car tractor = extensionLoader.getExtension("tractor");
tractor.say();
}
1.2.2 源码分析
Dubbo通过extensionLoader.getExtension("automobile") 获取实例化对象,先从缓存列表中取,如果为空,则创建该实例化对象
public T getExtension(String name) {
if (name != null && name.length() != 0) {
if ("true".equals(name)) {
//返回默认扩展对象
return this.getDefaultExtension();
} else {
//Holder:持有目标对象
//先通过name从缓存实例里找
Holder<Object> holder = (Holder)this.cachedInstances.get(name);
if (holder == null) {
//如果没有,就新建一个空Holder并存入cachedInstances缓存实例
this.cachedInstances.putIfAbsent(name, new Holder());
holder = (Holder)this.cachedInstances.get(name);
}
//从Holder中获取实例对象
Object instance = holder.get();
//如果为空,则加锁创建该实例对象
if (instance == null) {
synchronized(holder) {
instance = holder.get();
if (instance == null) {//双重保险
instance = this.createExtension(name);//创建实例化对象
holder.set(instance);
}
}
}
return instance;
}
} else {
throw new IllegalArgumentException("Extension name == null");
}
}
创建实例化对象:
private T createExtension(String name) {
// 从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表
//详细步骤:先调用this.getExtensionClasses()从缓存类cachedClasses中取,如果cachedClasses为空,
//再调用loadExtensionClasses(),从META-INF/dubbo/文件里加载
Class<?> clazz = (Class)this.getExtensionClasses().get(name);
if (clazz == null) {
throw this.findException(name);
} else {
try {
//先从EXTENSION_INSTANCES取实例
T instance = EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
//没有,就通过反射创建实例并存入EXTENSION_INSTANCES
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = EXTENSION_INSTANCES.get(clazz);
}
...
return instance;
} catch (Throwable var7) {
...
}
}
1.3 RPC SPI
Dubbo SPI相比Java SPI有很多优点,如:
- 配置文件改为键值对形式,可以通过名字获取任一实现类,而无需加载所有实现类,节约资源;
- 增加了缓存来存储实例
所以我们设计SPI时参照Dubbo的SPI。首先我们需要@SPI 、ExtensionLoader 以及其调用的Holder 类
@SPI :放在接口类
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SPI {
}
Holder :跟Dubbo一致,用于持有目标对象
public class Holder<T> {
private volatile T value;
public T get() {
return value;
}
public void set(T value) {
this.value = value;
}
}
ExtensionLoader :也是参照Dubbo的ExtensionLoader 来设计的
@Slf4j
public final class ExtensionLoader<T> {
/**
* 扩展类存放的目录地址
*/
private static final String SERVICE_DIRECTORY = "META-INF/extensions/";
/**
* 扩展加载器缓存 {类型:加载器}
*/
private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
/**
* 扩展类实例缓存 {类型:实例}
*/
private static final Map<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
/**
* 扩展类的类型
*/
private final Class<?> type;
/**
* 扩展实例类的Holder缓存 {name: 持有该实例的Holder}
*/
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
/**
* 扩展类配置列表缓存 {type: {name, 扩展类}}
*/
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
/**
* 构造函数
*/
private ExtensionLoader(Class<?> type) {
this.type = type;
}
/**
* @Description 根据类型得到扩展类加载器
* @param type 扩展类类型
* @Return 扩展类加载器
*/
public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {
//扩展类不能为空
if (type == null) {
throw new IllegalArgumentException("Extension type should not be null.");
}
//扩展类必须是接口
if (!type.isInterface()) {
throw new IllegalArgumentException("Extension type must be an interface.");
}
//扩展类必须有@SPI
if (type.getAnnotation(SPI.class) == null) {
throw new IllegalArgumentException("Extension type must be annotated by @SPI");
}
// 先从EXTENSION_LOADERS缓存里取
ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
// 如果缓存里没有,就新建一个,并放在缓存里
if (extensionLoader == null) {
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<S>(type));
extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADERS.get(type);
}
return extensionLoader;
}
/**
* @Description 根据名字获取扩展类实例对象
* @param name 扩展类在配置文件中配置的名字
* @Return 扩展类实例对象
*/
public T getExtension(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Extension name should not be null or empty.");
}
// 先从缓存cachedInstances里找
Holder<Object> holder = cachedInstances.get(name);
// 如果没有,新建一个空Holder放进cachedInstances
if (holder == null) {
cachedInstances.putIfAbsent(name, new Holder<>());
holder = cachedInstances.get(name);
}
// create a singleton if no instance exists
Object instance = holder.get();
if (instance == null) {
synchronized (holder) {
instance = holder.get();
if (instance == null) {//双重保险
instance = createExtension(name);
holder.set(instance);
}
}
}
return (T) instance;
}
/**
* 创建对应名字的扩展类实例对象
*
* @param name 扩展名
* @return 扩展类实例对象
*/
private T createExtension(String name) {
// 从配置文件中加载所有的扩展类,可得到“配置项名称”到“配置类”的映射关系表
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw new RuntimeException("No such extension of name " + name);
}
//先从EXTENSION_INSTANCES取实例
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
try {
//没有,就通过反射创建实例并存入EXTENSION_INSTANCES
EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance());
instance = (T) EXTENSION_INSTANCES.get(clazz);
} catch (Exception e) {
log.error(e.getMessage());
throw new RuntimeException("Fail to create an instance of the extension class " + clazz);
}
}
return instance;
}
/**
* 获取当前类型{@link #type}的所有扩展类
*
* @return {name: clazz}
*/
private Map<String, Class<?>> getExtensionClasses() {
// get the loaded extension class from the cache
Map<String, Class<?>> classes = cachedClasses.get();
// double check
if (classes == null) {
synchronized (cachedClasses) {
classes = cachedClasses.get();
if (classes == null) {
classes = new HashMap<>();
// load all extensions from our extensions directory
loadDirectory(classes);
cachedClasses.set(classes);
}
}
}
return classes;
}
/**
* 从资源文件中加载所有扩展类
*
* @return {name: 扩展类}
*/
private void loadDirectory(Map<String, Class<?>> extensionClasses) {
String fileName = ExtensionLoader.SERVICE_DIRECTORY + type.getName();
try {
Enumeration<URL> urls;
ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
urls = classLoader.getResources(fileName);
if (urls != null) {
while (urls.hasMoreElements()) {
URL resourceUrl = urls.nextElement();
loadResource(extensionClasses, classLoader, resourceUrl);
}
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {
try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), UTF_8))) {
String line;
// read every line
while ((line = reader.readLine()) != null) {
// get index of comment
final int ci = line.indexOf('#');
if (ci >= 0) {
// string after # is comment so we ignore it
line = line.substring(0, ci);
}
line = line.trim();
if (line.length() > 0) {
try {
final int ei = line.indexOf('=');
String name = line.substring(0, ei).trim();
String clazzName = line.substring(ei + 1).trim();
// our SPI use key-value pair so both of them must not be empty
if (name.length() > 0 && clazzName.length() > 0) {
Class<?> clazz = classLoader.loadClass(clazzName);
extensionClasses.put(name, clazz);
}
} catch (ClassNotFoundException e) {
log.error(e.getMessage());
}
}
}
} catch (IOException e) {
log.error(e.getMessage());
}
}
}
总结
学到的知识
1.认识IDEA图标:前者可以新建Java文件,后者不可以新建Java文件
2.新建文件夹时注意,没有空心圆标志的文件夹,命名为a.b意思不是两级文件夹a/b,意思是这个文件夹叫“a.b”
|