“文本已收录至我的GitHub仓库,欢迎Star:https://github.com/bin392328206 种一棵树最好的时间是十年前,其次是现在 ”
一个Web后端框架的轮子从处理Http请求【基于Netty的请求级Web服务器】 到mvc【接口封装转发)】,再到ioc【依赖注入】,aop【切面】,再到 rpc【远程过程调用】最后到orm【数据库操作】全部自己撸一个(简易)的轮子。
github
其实是这样的,小六六自己平时呢?有时候喜欢看看人家的源码比如Spring,但是小六六的水平可能不怎么样,每次看都看得晕头转向,然后就感觉里面的细节太难了,然后我就只能观其总体的思想,然后我就想我如果可以根据各位前辈的一些思考,自己撸一个简单的轮子出来,那我后面去理解作者的思想是不是简单点呢?于是呢 six-finger-web就面世了,它其实就是我的一个学习过程,然后我把它开源出来,希望能帮助那些对于学习源码有困难的同学。还有就是可以锻炼一下自己的编码能力,因为平时我们总是crud用的Java api都是那些,久而久之,很多框架类的api我们根本就不熟练了,所以借此机会,锻炼一下。
前面是已经写好的章节,下面我给大家来一一走一遍搭建流程
上一篇文章,就大概的讲解了一下rpc,因为我们要模仿的是dubbo,然后我就去看了下,dubbo,我擦源码太多了,然后因为我本身公司技术栈也不是dubbo,所以呢?我就去github上找写其他的模仿dubbo的项目来学习,然后我就找到了 我们Guide哥的 guide-rpc-framework,然后我就先跟着这个项目来学习dubbo吧!然后跟,今天我们先来聊聊dubbo的SPI先。这个的运用还是很广的。
说概念的话,可能大家会比较蒙,我这边还是来看看例子吧,其实在小六六看来 这个spi机制可以类比于一个控制反转吧,反正你也不是自己去new你的实现类了,而是通过别人给你,差不多就是这个意思?我也不知道自己理解的怎么样。我们来看看spi机制呗!
package com.xiaoliuliu.spring.a;
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:16
*/
public interface Car {
String getBrand();
}
package com.xiaoliuliu.six.finger.web.demo.rpc;
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:17
*/
public class Benz implements Car {
@Override
public String getBrand() {
System.out.println("benz car");
return "Benz";
}
}
package com.xiaoliuliu.six.finger.web.demo.rpc;
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:16
*/
public class BM implements Car {
@Override
public String getBrand() {
System.out.println("BM car");
return "BM";
}
}
“Java的Spi 其实是有固定写法的,是因为在源码里面是写死了的再在resources下创建META-INF/services 文件夹,并创建一个文件,文件名称为Car接口的全限定名package com.xiaoliuliu.spring.a.Car。内容为接口实现类的全限定类名。 ”
com.xiaoliuliu.spring.a.Benz
com.xiaoliuliu.spring.a.BM
package com.xiaoliuliu.spring.a;
import java.util.ServiceLoader;
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:17
*/
public class Test {
public static void main(String[] args) {
ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
for (Car car : serviceLoader) {
System.out.println(car.getBrand());
}
}
}
“JAVA SPI实现了接口的定义与具体业务实现解耦,应用进程可以根据实际业务情况启用或替换具体组件。 ”
举例:JAVA的java.sql包中就定义一个接口Driver,各个服务提供商实现该接口。当我们需要使用某个数据库时就导入相应的jar包。
缺点
dubbo重新实现了一套功能更强的 SPI 机制, 支持了AOP与依赖注入,并且 利用缓存提高加载实现类的性能,同时 支持实现类的灵活获取,文中接下来将讲述SPI的应用与原理。
Dubbo的SPI接口都会使用@SPI注解标识,该注解的主要作用就是标记这个接口是一个SPI接口。源码如下:
@Documented@Retention(RetentionPolicy.RUNTIME)@Target({ElementType.TYPE})public @interface SPI { /**
* default extension name
* 设置默认拓展类
*/
String value() default "";
}
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.5.7</version>
</dependency>
在Car 接口上 添加注解
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:16
*/
@SPI
public interface Car {
String getBrand();
}
配置文件的路径与文件名也暂时不变,文件内容调整如下:
benz=com.xiaoliuliu.spring.a.Benz
修改测试类
package com.xiaoliuliu.spring.a;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
import java.util.ServiceLoader;
/**
* @author 小六六
* @version 1.0
* @date 2020/11/2 10:17
*/
public class Test {
public static void main(String[] args) {
ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class); //按需获取实现类对象
Car car = carExtensionLoader.getExtension("benz");
System.out.println(car.getBrand());
}
}
Dubbo对JDK SPI进行了扩展,对服务提供者配置文件中的内容进行了改造,由原来的提供者类的全限定名列表改成了KV形式的列表,这也导致了Dubbo中无法直接使用JDK ServiceLoader,所以,与之对应的,在Dubbo中有ExtensionLoader,ExtensionLoader是扩展点载入器,用于载入Dubbo中的各种可配置组件,比如:动态代理方式(ProxyFactory)、负载均衡策略(LoadBalance)、RCP协议(Protocol)、拦截器(Filter)、容器类型(Container)、集群方式(Cluster)和注册中心类型(RegistryFactory)等。
总之,Dubbo为了应对各种场景,它的所有内部组件都是通过这种SPI的方式来管理的,这也是为什么Dubbo需要将服务提供者配置文件设计成KV键值对形式,这个K就是我们在Dubbo配置文件或注解中用到的K,Dubbo直接通过服务接口(上面提到的ProxyFactory、LoadBalance、Protocol、Filter等)和配置的K从ExtensionLoader拿到服务提供的实现类。
我看了下 dubbo 源码的ExtensionLoader类 大概有1000行代码 方法也挺多的,然后我就不打算拿它的来讲,我们拿上面我们参考的那个 javaguide模仿 dubbo项目的来讲解一下,下面是类的代码
package github.javaguide.extension;
import github.javaguide.utils.Holder;
import lombok.extern.slf4j.Slf4j;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import static java.nio.charset.StandardCharsets.UTF_8;
/**
* refer to dubbo spi: https://dubbo.apache.org/zh-cn/docs/source_code_guide/dubbo-spi.html
*/
@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;
private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
private ExtensionLoader(Class<?> type) {
this.type = type;
}
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.");
}
if (type.getAnnotation(SPI.class) == null) {
throw new IllegalArgumentException("Extension type must be annotated by @SPI");
}
// firstly get from cache, if not hit, create one
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;
}
public T getExtension(String name) {
if (name == null || name.isEmpty()) {
throw new IllegalArgumentException("Extension name should not be null or empty.");
}
// firstly get from cache, if not hit, create one
Holder<Object> holder = cachedInstances.get(name);
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;
}
private T createExtension(String name) {
// load all extension classes of type T from file and get specific one by name
Class<?> clazz = getExtensionClasses().get(name);
if (clazz == null) {
throw new RuntimeException("No such extension of name " + name);
}
T instance = (T) EXTENSION_INSTANCES.get(clazz);
if (instance == null) {
try {
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;
}
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;
}
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());
}
}
}
对比Dubbo来说 精简了很多。不过麻雀虽下,五脏俱全嘛,通过调用一个个来分析一下吧。
ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class); //按需获取实现类对象
相比于Java 的 Dubbo 每个接口的Spi 都是有一个ExtensionLoader
其实嘛,就相当于做了一个工厂的角色吧!大概就是这个意思了。但是拿到具体的实现类还是再下面的代码
Car car = carExtensionLoader.getExtension("benz");
然后我来分析分析,这个代码
其实就是我圈的这几个地方拉,这个不是我们写缓存的经典写法嘛,哈哈,其实就是多了一个本地缓存嘛,基本上大家都能懂吧
通过扫描文件获得实现类
再dubbo的源码里面是三个
Dubbo默认依次扫描META-INF/dubbo/internal/、META-INF/dubbo/、META-INF/services/三个classpath目录下的配置文件。
然后就基本上就这样了,本质上还是把接口和实现解耦。然后dubbo可能还加了IOC的相关东西,这边就没有一一分析了,有兴趣的大家自己去看看。
好了,rpc的第二篇,我们就分析到这了,希望对大家有所帮助。