新宾区网站建设,学校网站开发必要性与意义,wordpress怎么设置跳站外链接,石家庄权威发布在此前的内容中#xff0c;已经详细介绍了 SkyWalking Agent 用到的多种基础技术#xff0c;例如#xff0c;Byte Buddy、Java Agent 以及 OpenTracing 中的核心概念。本课时将深入介绍 SkyWalking Agent 以及 OAP 中都会使用到的 SPI 技术。
JDK SPI 机制
SPI#xff08…在此前的内容中已经详细介绍了 SkyWalking Agent 用到的多种基础技术例如Byte Buddy、Java Agent 以及 OpenTracing 中的核心概念。本课时将深入介绍 SkyWalking Agent 以及 OAP 中都会使用到的 SPI 技术。
JDK SPI 机制
SPIService Provider Interface主要是被框架开发人员使用的一种技术。例如使用 Java 语言访问数据库时我们会使用到 java.sql.Driver 接口每个数据库厂商使用的协议不同提供的 java.sql.Driver 实现也不同在开发 java.sql.Driver 接口时开发人员并不清楚用户最终会使用哪个数据库在这种情况下就可以使用 Java SPI 机制为 java.sql.Driver 接口寻找具体的实现。
当服务的提供者提供了一种接口的实现之后需要在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件此文件记录了该 jar 包提供的服务接口的具体实现类。当某个应用引入了该 jar 包且需要使用该服务时JDK SPI 机制就可以通过查找这个 jar 包的 META-INF/services/ 中的配置文件来获得具体的实现类名进行实现类的加载和实例化最终使用该实现类完成业务功能。
下面通过一个简单的示例演示 JDK SPI 的基本使用方式示例如下 首先我们需要创建一个 Log 接口来模拟日志打印的功能
public interface Log {void log(String info);
}接下来提供两个实现 —— Logback 和 Log4j分别代表两个不同日志框架的实现如下所示
public class Logback implements Log {Overridepublic void log(String info) {System.out.println(Logback: info);}
}public class Log4j implements Log { Override public void log(String info) { System.out.println(“Log4j:” info); } }
在项目的 resources/META-INF/services 目录下添加一个名为 com.xxx.Log 的文件这是 JDK SPI 需要读取的配置文件具体内容如下
com.xxx.impl.Log4j
com.xxx.impl.Logback最后创建 main() 方法其中会加载上述配置文件创建全部 Log 接口实现的实例并执行其 log() 方法如下所示
public class Main {public static void main(String[] args) {ServiceLoaderLog serviceLoader ServiceLoader.load(Log.class);IteratorLog iterator serviceLoader.iterator();while (iterator.hasNext()) {Log log iterator.next();log.log(JDK SPI); }}
}
// 输出如下:
// Log4j:JDK SPI
// Logback:JDK SPIJDK SPI源码分析
通过上述示例我们可以看到 JDK SPI 的入口方法是 ServiceLoader.load() 方法接下来我将对其具体实现进行深入分析。
在 ServiceLoader.load() 方法中首先会尝试获取当前使用的 ClassLoader获取当前线程绑定的 ClassLoader查找失败后使用 SystemClassLoader然后调用 reload() 方法调用关系如下图所示 在 reload() 方法中首先会清理 providers 缓存LinkedHashMap 类型的集合该缓存用来记录 ServiceLoader 创建的实现对象其中 Key 为实现类的完整类名Value 为实现类的对象。之后创建 LazyIterator 迭代器该迭代器用于读取 SPI 配置文件并实例化实现类对象。
ServiceLoader.reload() 方法的具体实现如下所示
// 缓存用来缓存 ServiceLoader创建的实现对象
private LinkedHashMapString,S providers new LinkedHashMap();public void reload() { providers.clear(); // 清空缓存 lookupIterator new LazyIterator(service, loader); // 迭代器 }
在前面的示例中main() 方法中使用的迭代器底层就是调用了 ServiceLoader.LazyIterator 实现的。Iterator 接口有两个关键方法hasNext() 方法和 next() 方法这里的 LazyIterator 中的next() 方法最终调用的是其 nextService() 方法hasNext() 方法最终调用的是 hasNextService() 方法调用关系如下图所示 首先来看 LazyIterator.hasNextService() 方法该方法主要负责查找 META-INF/services 目录下的 SPI 配置文件并进行遍历大致实现如下所示
private static final String PREFIX META-INF/services/;
EnumerationURL configs null;
IteratorString pending null;
String nextName null;private boolean hasNextService() { if (nextName ! null) { return true; } if (configs null) { // PREFIX前缀与服务接口的名称拼接起来就是META-INF目录下定义的SPI配 // 置文件(即示例中的META-INF/services/com.xxx.Log) String fullName PREFIX service.getName(); // 加载配置文件 if (loader null) configs ClassLoader.getSystemResources(fullName); else configs loader.getResources(fullName); } // 按行SPI遍历配置文件的内容 while ((pending null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } // 解析配置文件 pending parse(service, configs.nextElement()); } nextName pending.next(); // 更新 nextName字段 return true; }
接下来在 hasNextService() 方法中完成 SPI 配置文件的解析之后再来看 LazyIterator.nextService() 方法该方法负责实例化 hasNextService() 方法读取到的实现类其中会将实例化的对象放到 providers 集合中缓存起来核心实现如下所示
private S nextService() {String cn nextName;nextName null;// 加载 nextName字段指定的类Class? c Class.forName(cn, false, loader);if (!service.isAssignableFrom(c)) { // 检测类型fail(service, Provider cn not a subtype);}S p service.cast(c.newInstance()); // 创建实现类的对象providers.put(cn, p); // 将实现类名称以及相应实例对象添加到缓存return p;
}在 main() 方法中使用的迭代器的底层实现介绍完了我们再来看一下其使用的真正迭代器核心实现如下
public IteratorS iterator() {return new IteratorS() {// knownProviders用来迭代 providers缓存IteratorMap.EntryString,S knownProviders providers.entrySet().iterator();public boolean hasNext() { // 先走查询缓存缓存查询失败再通过 LazyIterator加载 if (knownProviders.hasNext()) return true; return lookupIterator.hasNext(); } public S next() { // 先走查询缓存缓存查询失败再通过 LazyIterator加载 if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } // 省略remove()方法 };
JDK SPI 在 JDBC 中的应用
了解了 JDK SPI 实现的原理之后我们来看实践中 JDBC 是如何使用 JDK SPI 机制加载不同数据库厂商的实现类。
JDK 中只定义了一个 java.sql.Driver 接口具体的实现是由不同数据库厂商来提供的。这里以 MySQL 提供的 JDBC 实现包为例进行分析。
在 mysql-connector-java-*.jar 包中的 META-INF/services 目录下有一个 java.sql.Driver 文件中只有一行内容如下所示
com.mysql.cj.jdbc.Driver在使用 mysql-connector-java-*.jar 包连接 MySQL 数据库的时候我们会用到如下语句创建数据库连接
String url jdbc:xxx://xxx:xxx/xxx;
Connection conn DriverManager.getConnection(url, username, pwd);DriverManager 是 JDK 提供的数据库驱动管理器其中的代码片段如下所示
static {loadInitialDrivers();println(JDBC DriverManager initialized);
}在调用 getConnection() 方法的时候DriverManager 类会被 Java 虚拟机加载、解析并触发 static 代码块的执行在 loadInitialDrivers() 方法中通过 JDK SPI 扫描 Classpath 下 java.sql.Driver 接口实现类并实例化核心实现如下所示
private static void loadInitialDrivers() {String drivers System.getProperty(jdbc.drivers)// 使用 JDK SPI机制加载所有 java.sql.Driver实现类ServiceLoaderDriver loadedDrivers ServiceLoader.load(Driver.class);IteratorDriver driversIterator loadedDrivers.iterator();while(driversIterator.hasNext()) {driversIterator.next();}String[] driversList drivers.split(:);for (String aDriver : driversList) { // 初始化Driver实现类Class.forName(aDriver, true,ClassLoader.getSystemClassLoader());}
}在 MySQL 提供的 com.mysql.cj.jdbc.Driver 实现类中同样有一段 static 静态代码块这段代码会创建一个 com.mysql.cj.jdbc.Driver 对象并注册到 DriverManager.registeredDrivers 集合中 CopyOnWriteArrayList 类型如下所示
static {java.sql.DriverManager.registerDriver(new Driver());
}在 getConnection() 方法中DriverManager 从该 registeredDrivers 集合中获取对应的 Driver 对象创建 Connection核心实现如下所示
private static Connection getConnection(String url, java.util.Properties info, Class? caller) throws SQLException {// 省略 try/catch代码块以及权限处理逻辑for(DriverInfo aDriver : registeredDrivers) {Connection con aDriver.driver.connect(url, info);return con;}
}Dubbo 对 JDK SPI 的改进
通过前面的分析可以发现JDK SPI 在查找具体实现类的过程中需要遍历 SPI 配置文件中定义的所有实现类该过程中会将这些实现类全部实例化。如果 SPI 配置文件中定义了多个实现类而我们只需要其中一个实现类时就会生成不必要的对象。
Dubbo 为了解决上述问题自己设计了一套 SPI 实现但是思想与 JDK SPI 机制类似。作为思路的扩展这里简单介绍一下 Dubbo SPI 的实现原理SkyWalking 使用是 JDK SPI 而不是 Dubbo SPI 。
首先Dubbo 将 SPI 配置文件改成了 KV 格式例如
dubboorg.apache.dubbo.rpc.protocol.dubbo.DubboProtocol其中 key 就是一个简单的标记当我们在为一个接口查找具体实现类时可以指定 key 来选择具体实现例如这里指定 key 为 dubboDubbo SPI 就知道我们要的是org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol 这个实现类。
Dubbo SPI 核心实现是 ExtensionLoader位于 dubbo-common 模块中的 extension 包中功能类似于 JDK SPI 中的 java.util.ServiceLoader其使用方式如下所示
Protocol protocol ExtensionLoader.getExtensionLoader(Protocol.class).getExtension(dubbo);
// 很明显在查找 Protocol这个接口的实现类时还指定了dubbo这个keyExtensionLoader.getExtensionLoader() 方法会根据接口类型从缓存中查找相应的 ExtensionLoader 实现核心实现如下
private static final ConcurrentMapClass?, ExtensionLoader? EXTENSION_LOADERS new ConcurrentHashMap();public static T ExtensionLoaderT getExtensionLoader(ClassT type) { ExtensionLoaderT loader (ExtensionLoaderT) EXTENSION_LOADERS.get(type); if (loader null) { EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoaderT(type)); loader (ExtensionLoaderT) EXTENSION_LOADERS.get(type); } return loader; }
查找到接口对应的 ExtensionLoader 对象之后会调用 getExtension() 方法再根据传入的 key 查找相应的实现类最终将其实例化后返回
// 缓存记录了 key到实现类对象Holder之间的映射关系
private final ConcurrentMapString, HolderObject cachedInstances new ConcurrentHashMap();public T getExtension(String name) { HolderObject holder getOrCreateHolder(name); Object instance holder.get(); if (instance null) { // double-check防止并发问题 synchronized (holder) { instance holder.get(); if (instance null) { // createExtension()方法中完成了 SPI配置文件的查找以及实现类 // 的实例化具体实现与 JDK SPI原理类似其中还会处理 Dubbo中 // 自定义的一些注解不再展开分析 instance createExtension(name); holder.set(instance); } } } return (T) instance; }
总结
本课时首先介绍了 JDK SPI 机制的原理并通过 Log 示例演示了 JDK SPI 的使用方式然后深入到 ServiceLoader 的源码中分析了 JDK SPI 的实现方式接下来介绍了 JDBC 4.0 如何使用 JDK SPI 机制加载数据库驱动类最后介绍了 Dubbo 对 JDK SPI 的改进。