Dubbo中的SPI机制

Dubbo中的SPI机制

概述

Service Provider Interface 即 SPI,是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件。可以让不同的厂商针对统一接口编写不同的实现

SPI实际上是“接口+策略模式+配置文件”实现的动态加载机制。在系统设计中,模块之间通常基于接口编程,不直接显示指定实现类。一旦代码里指定了实现类,就无法在不修改代码的情况下替换为另一种实现。为了达到动态可插拔的效果,java提供了SPI以实现服务发现。

SPI机制的应用场景有很多,我们比较常用的就是JDBC,Dubbo等

在谈Dubbo的SPI机制前我们需要先了解一下Java原生的SPI机制

Java原生的SPI

  1. 首先我,我们先创建一个接口,接口中定义一个方法

package cuit.epoch.pymjl.spi;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:43**/
public interface Animal {/*** 动物叫*/void call();
}

  1. 然后我们分别写两个不同的实现类,实现这个接口

package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:44**/
public class Cat implements Animal {@Overridepublic void call() {System.out.println("猫叫:喵喵喵~");}
}

package cuit.epoch.pymjl.spi.impl;import cuit.epoch.pymjl.spi.Animal;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:45**/
public class Dog implements Animal {@Overridepublic void call() {System.out.println("狗叫:汪汪汪~");}
}

  1. 在项目./resources/META-INF/servcie下创建一个文件。文件名为你要动态扩展的接口的全限定名称,我们这里就是Animaal接口的全限定名称,如图所示:

image-20220504201416121

文件里面的内容就是实现类的全限定名称

cuit.epoch.pymjl.spi.impl.Cat
cuit.epoch.pymjl.spi.impl.Dog

  1. 编写测试类

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("这是Java原生的SPI机制");ServiceLoader<Animal> serviceLoader = ServiceLoader.load(Animal.class);for (Animal animal : serviceLoader) {animal.call();}}
}

  1. 运行测试

image-20220504201720318

通过测试代码可见,我们实现的两个类成功的被加载并运行了相关的方法。但是我们并没有在代码中显示的创建某个实现类,我们可以通过这种插件化的配置文件的形式实例化我们想要的对象。将程序的决定权解耦到配置文件中

但是Java原生的SPI也有其不足

  1. 通过测试代码我们可知,我们并不能直接指定加载某一个我们想要的类,只能通过遍历加载配置文件中所有类来找到我们想要的类,这样效率是很低的,造成资源的浪费
  2. 获取类的方式不够灵活,只能通过Iterator 形式获取,不能根据某个参数来获取对应的实现类。
  3. ServiceLoader线程不安全

针对以上不足,我们可以选择Dubbo的SPI机制,以此来规避不足

Dubbo的SPI

Dubbo 并未使用 Java SPI,而是重新实现了一套功能更强的 SPI 机制。Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。

另外,Dubbo是通过键值对的方式进行配置,我们可以直接通过Key获取我们想要加载的实体类。Dubbo默认的配置文件路径是在./resources/META-INF/dubbo

编写测试方法:

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.util.Iterator;
import java.util.ServiceLoader;/*** @author Pymjl* @version 1.0* @date 2022/5/2 17:47**/
@Slf4j
public class Main {public static void main(String[] args) {log.info("这是Dubbo的SPI机制");Animal cat = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("cat");Animal dog = ExtensionLoader.getExtensionLoader(Animal.class).getExtension("dog");cat.call();dog.call();}
}

更改配置文件

cat=cuit.epoch.pymjl.spi.impl.Cat
dog=cuit.epoch.pymjl.spi.impl.Dog

运行测试:

image-20220504203927835

通过代码可知,Dubbo的SPI机制可直接通过Key获取我们想要的对象实例,比原生的Java SPI更有优势,除此之外,Dubbo在设计中还用到了大量的全局缓存,提高了我们实例化对象的效率,同时还支持通过注解默认实现,AOP,IOC等功能

下面是仿造Dubbo的源码自定义实现的SPI机制的代码,来自于Guide哥开源的RPC项目。虽然和源码的有一些差别,也没有Dubbo的功能完善,但核心思想还是一样的

仿造源码自定义实现Dubbo SPI机制

自定义注解MySpi

通过上文可知,Dubbo的SPI机制还要根据@spi注解实现,对需要扩展的接口做一个标记,所以我们先自定义一个注解,用来标记我们想要扩展的接口

package cuit.epoch.pymjl.spi;import java.lang.annotation.*;/*** 标记接口,声明该接口是一个扩展点** @author Pymjl* @version 1.0* @date 2022/5/3 12:05**/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface MySpi {//TODO value可以提供对默认实现的支持,但是这方面的切面并没有写,只是写在这儿表示有这个东西String value() default "";
}

Holder对象

顾名思义,Holder就是持有通过ExtensionLoader加载来的实例化对象的对象

package cuit.epoch.pymjl.spi;/*** 持有人** @author Pymjl* @version 1.0* @date 2022/5/3 15:11**/
public class Holder<T> {private volatile T value;public T get() {return value;}public void set(T value) {this.value = value;}
}

ExtensionLoader

Dubbo SPI机制的核心类,使用它从文件中读取配置,并通过反射实例化对象

package cuit.epoch.pymjl.spi;import lombok.extern.slf4j.Slf4j;import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;/*** 模仿Dubbo自定义扩展机制** @author Pymjl* @version 1.0* @date 2022/5/3 15:12**/
@Slf4j
public class ExtensionLoader<T> {/*** SPI配置文件根目录*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地缓存,Dubbo会先通过getExtensionLoader方法从缓存中获取一个ExtensionLoader* 若缓存未命中,则会生成一个新的实例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = 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<>();/*** 扩展类实例对象,key为配置文件中的key,value为实例对象的全限定名称*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();public ExtensionLoader(Class<?> type) {this.type = type;}/*** 得到扩展加载程序** @param type 要扩展的接口,必须被MySpi标记* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判断type是否为nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判断是否被MySpi标记if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先从缓存中获取扩展加载器,如果未命中,则创建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中则创建,并放入缓存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}/*** 得到扩展类对象实例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//检查参数if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先从缓存中获取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder还未持有目标对象,则为其创建一个单例对象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 {@code T}*/private T createExtension(String name) {//从文件中加载所有类型为 T 的扩展类并按名称获取特定的扩展类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 (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 获取所有扩展类** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//从缓存中获取已经加载的扩展类Map<String, Class<?>> classes = cachedClasses.get();//双重检查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//从配置文件中加载所有扩展类loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 从配置目录中加载所有扩展类** @param extensionsClasses 扩展类的K,V键值对*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {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(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通过Url加载资源** @param extensionClasses 扩展类,key为配置文件中的key,Value为实现类的全限定名称* @param classLoader      类加载器* @param resourceUrl      资源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//读取文件中的每一行数据while ((line = reader.readLine()) != null) {//先排除配置文件中的注释final int noteIndex = line.indexOf('#');//我们应该忽略掉注释后的内容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}
}

  1. 先声明了一些常量,例如配置文件的根目录,本地缓存等

     /*** SPI配置文件根目录*/private static final String SERVICE_DIRECTORY = "META-INF/diy-rpc/";/*** 本地缓存,Dubbo会先通过getExtensionLoader方法从缓存中获取一个ExtensionLoader* 若缓存未命中,则会生成一个新的实例*/private static final Map<Class<?>, ExtensionLoader<?>> EXTENSION_LOADER_MAP = 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<>();/*** 扩展类实例对象,key为配置文件中的key,value为实例对象的全限定名称*/private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
    
  2. ExtensionLoader 的getExtensionLoader 方法先从缓存中获取一个ExtensionLoader 实例,若缓存未命中,则new一个。

    /*** 得到扩展加载程序** @param type 要扩展的接口,必须被MySpi标记* @return {@code ExtensionLoader<S>}*/public static <S> ExtensionLoader<S> getExtensionLoader(Class<S> type) {//判断type是否为nullif (type == null) {throw new IllegalArgumentException("Extension type should not be null.");}//如果不是接口if (!type.isInterface()) {throw new IllegalArgumentException("Extension type must be an interface.");}//判断是否被MySpi标记if (type.getAnnotation(MySpi.class) == null) {throw new IllegalArgumentException("Extension type must be annotated by @MySpi");}//先从缓存中获取扩展加载器,如果未命中,则创建ExtensionLoader<S> extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);if (extensionLoader == null) {//未命中则创建,并放入缓存EXTENSION_LOADER_MAP.putIfAbsent(type, new ExtensionLoader<>(type));extensionLoader = (ExtensionLoader<S>) EXTENSION_LOADER_MAP.get(type);}return extensionLoader;}
    
  3. 然后再通过ExtensionLoader 的getExtension 方法获取拓展类对象。跟上面的逻辑一样,先从缓存中查找,若缓存未命中则新建一个

   /*** 得到扩展类对象实例** @param name 配置名字* @return {@code T}*/public T getExtension(String name) {//检查参数if (name == null || name.trim().length() == 0) {throw new IllegalArgumentException("Extension name should not be null or empty.");}//先从缓存中获取,如果未命中,新建Holder<Object> holder = cachedInstances.get(name);if (holder == null) {cachedInstances.putIfAbsent(name, new Holder<>());holder = cachedInstances.get(name);}//如果Holder还未持有目标对象,则为其创建一个单例对象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 {@code T}*/private T createExtension(String name) {//从文件中加载所有类型为 T 的扩展类并按名称获取特定的扩展类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 (InstantiationException | IllegalAccessException e) {e.printStackTrace();log.error(e.getMessage());}}return instance;}/*** 获取所有扩展类** @return {@code Map<String, Class<?>>}*/private Map<String, Class<?>> getExtensionClasses() {//从缓存中获取已经加载的扩展类Map<String, Class<?>> classes = cachedClasses.get();//双重检查if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.get();if (classes == null) {classes = new HashMap<>();//从配置文件中加载所有扩展类loadDirectory(classes);cachedClasses.set(classes);}}}return classes;}/*** 从配置文件中加载所有扩展实现类** @param extensionsClasses 扩展类的K,V键值对*/private void loadDirectory(Map<String, Class<?>> extensionsClasses) {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(extensionsClasses, classLoader, resourceUrl);}}} catch (IOException e) {log.error(e.getMessage());}}/*** 通过Url加载资源** @param extensionClasses 扩展类,key为配置文件中的key,Value为实现类的全限定名称* @param classLoader      类加载器* @param resourceUrl      资源url*/private void loadResource(Map<String, Class<?>> extensionClasses, ClassLoader classLoader, URL resourceUrl) {try (BufferedReader reader = new BufferedReader(new InputStreamReader(resourceUrl.openStream(), StandardCharsets.UTF_8))) {String line;//读取文件中的每一行数据while ((line = reader.readLine()) != null) {//先排除配置文件中的注释final int noteIndex = line.indexOf('#');//我们应该忽略掉注释后的内容if (noteIndex > 0) {line = line.substring(0, noteIndex);}line = line.trim();if (line.length() > 0) {try {final int keyIndex = line.indexOf('=');String key = line.substring(0, keyIndex).trim();String value = line.substring(keyIndex + 1).trim();if (key.length() > 0 && value.length() > 0) {Class<?> clazz = classLoader.loadClass(value);extensionClasses.put(key, clazz);}} catch (ClassNotFoundException e) {e.printStackTrace();log.error(e.getMessage());}}}} catch (IOException e) {e.printStackTrace();log.error(e.getMessage());}}


---------------------
作者:Pymj
来源:CSDN
原文:https://blog.csdn.net/apple_52109766/article/details/124577928
版权声明:本文为作者原创文章,转载请附上博文链接!
内容解析By:CSDN,CNBLOG博客文章一键转载插件

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/283890.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

JWT:拥有我,即拥有权力

Hi&#xff0c;这里是桑小榆。上篇文章中&#xff0c;我们一起探讨了 OAuth 协议的原理以及授权认证流程&#xff0c;本次我们一起探讨 jwt 令牌作为授权协议的传输介质。OAuth协议规范了几个参与角色的授权标准&#xff0c;安全可控的授予第三方应用&#xff0c;第三方应用获取…

双十一到来之前,阿里AI设计师“鲁班”1天能做4000万张海报

相比较去年&#xff0c;“鲁班”的设计技艺有所提升。 人工智能很大程度上便利了我们的生活&#xff0c;现在他们甚至还能取代了一些设计师的工作&#xff0c;在双十一正式到来之前&#xff0c;淘宝的宣传已经铺天盖地&#xff0c;然而很多人都没想到&#xff0c;我们打开淘宝…

Appium移动自动化测试之获取appPackage和appActivity

方法一&#xff1a;直接打开Appium,点击左上角机器人图标 选择apk所在位置&#xff0c;如图所示&#xff0c;这里以ContactManager.apk为例 方法二&#xff1a;利用dex2jar和jd-gui这两个工具反编译apk文件 这里仍以ContactManager.apk为例 (1)重命名ContactManager.apk为Conta…

CAD转WPF: 关于CAD图纸文件转换为WPF矢量代码文件(xaml文件)的技巧

前言&#xff1a;下面的文章&#xff0c;我将会以几个很简单的步骤&#xff0c;来演示一下通过CAD图纸转换为XAML代码文件的方法&#xff0c;供大佬们参考。一、为了演示一个简单的操作&#xff0c;我此处先打开一个空白的CAD&#xff0c;等下用来进行绘制点内容使用。二、自定…

python之新式类与经典类

经典类与新式类经典类:P 或 P()--深度查找&#xff0c;向上查父节点新式类 :P(object)---广度查找&#xff0c;继承object&#xff0c;新式类的方法较多转载于:https://www.cnblogs.com/zyy98877/p/8574983.html

Flowportal-BPM——环境配置

环境配置&#xff1a; 一、控制面板→程序和功能→打开或不关闭Window功能→选择选项 二、控制面板→管理工具→Internet信息服务&#xff08;IIS&#xff09;管理器→左侧第一个→ISAPI和CGI限制→全部选为【允许】 三、控制面板→管理工具→Internet信息服务&#xff08;IIS&…

一篇文章带你搞懂什么是DevOps?

DevOps DevOps 它的英文发音是 /de’vɒps/&#xff0c;类似于“迪沃普斯”&#xff0c;一词本身是对于 development 以及 operation 两个词的混合&#xff0c;其目的在于缩短系统开发的生命周期&#xff0c;在这过程中发布特性、修复bug以及更新均被紧密的结合。 简化的含义为…

微服务架构下分布式事务解决方案 —— 阿里GTS

1 微服务的发展 微服务倡导将复杂的单体应用拆分为若干个功能简单、松耦合的服务&#xff0c;这样可以降低开发难度、增强扩展性、便于敏捷开发。当前被越来越多的开发者推崇&#xff0c;很多互联网行业巨头、开源社区等都开始了微服务的讨论和实践。Hailo有160个不同服务构成&…

重要消息丨.NET Core 3.1 将于今年12月13日结束支持

点击上方蓝字关注我们&#xff08;本文阅读时间&#xff1a;5分钟).NET Core 3.1 将于 2022 年 12 月 13 日结束支持。此后&#xff0c;Microsoft 将不再为 .NET Core 3.1 提供服务更新或技术支持。我们建议尽快迁移到 .NET 6。如果您在支持日期结束后仍在使用 .NET Core 3.1&a…

产品设计的三大原则

1.它有用吗? 如果我们必须从这三个特性中选择一个作为最重要的&#xff0c;那就是有用性。 首要的是&#xff0c;一个产品必须有用。如果它无用&#xff0c;其它任何东西都是不相关的&#xff0c;因为没有人会需要它。很明显&#xff0c;有用性和可享用性看上去一样重要&#…

常用的17个运维监控系统

1. Zabbix Zabbix 作为企业级的网络监控工具&#xff0c;通过从服务器&#xff0c;虚拟机和网络设备收集的数据提供实时监控&#xff0c;自动发现&#xff0c;映射和可扩展等功能。 Zabbix的企业级监控软件为用户提供内置的Java应用服务器监控&#xff0c;硬件监控&#xff0c…

关于html-三角的制作

因为最近看到别人写的不错的样式&#xff0c;所以就想自己实现&#xff0c;但是呢用到了一个三角形&#xff0c;所以稍微研究一下。效果是这样的&#xff1a;注意是下边那个浅色三角&#xff0c;感觉书签的效果有木有。看着很有层次感。接下来就是实现了&#xff0c;利用border…

ABP中的数据过滤器

本文首先介绍了ABP内置的软删除过滤器(ISoftDelete)和多租户过滤器(IMultiTenant)&#xff0c;然后介绍了如何实现一个自定义过滤器&#xff0c;最后介绍了在软件开发过程中遇到的实际问题&#xff0c;同时给出了解决问题的一个未必最优的思路。一.预定义过滤器ABP中的数据过滤…

ActiveMQ与spring整合

2019独角兽企业重金招聘Python工程师标准>>> 1 生产者 第一步&#xff1a;引用相关的jar包。 <dependency> <groupId>org.springframework</groupId><artifactId>spring-jms</artifactId> </dependency> <dependency><…

最新远程部署运维工具汇总

一&#xff0e;Puppet 转载https://baike.baidu.com/item/puppet/5109503?fraladdin puppet是一种Linux、Unix、windows平台的集中配置管理系统&#xff0c;使用自有的puppet描述语言&#xff0c;可管理配置文件、用户、cron任务、软件包、系统服务等。puppet把这些系统实体…

Kali Linux 2016.2初体验使用总结

Kali Linux 2016.2初体验使用总结Kali Linux官方于8月30日发布Kali Linux 2016的第二个版本Kali Linux 2016.2。该版本距离Kali Linux 2016.1版本发布&#xff0c;已经有7个月。在这期间&#xff0c;在Kali Linux 2016.2版本发布的这段时间&#xff0c;Kali Linux官方增补了94个…

Kafka入门教程:学习总结目录索引

【Kafka】| 总结/Edison ZhouEdison总结了Kafka的学习征途系列&#xff0c;特意整理了一份目录索引&#xff0c;希望对你有帮助。0Kafka学习路径在学习Kafka的途中&#xff0c;我总结了一个系列的Kafka学习征途系列教程&#xff0c;它只选取了我认为最实用的部分整理出来&#…

javaweb学习中的路径问题

1. 项目结构 2. 客户端路径 1. 超链接 <% page language"java" contentType"text/html; charsetUTF-8"pageEncoding"UTF-8"%> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/…

步步为营-11-ListT泛型的简单练习

using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks;namespace 集合简单练习 {class Program{static void Main(string[] args){}private static void Test3(){//奇偶分拣,奇数在前偶数在后List<int>…

SPI 与 API的区别

背景 Java 中区分 API 和 SPI&#xff0c;通俗的讲&#xff1a;API 和 SPI 都是相对的概念&#xff0c;他们的差别只在语义上&#xff0c;API 直接被应用开发人员使用&#xff0c;SPI 被框架扩展人员使用 理解 API &#xff08;Application Programming Interface&#xff09; …