- 浏览: 447826 次
- 性别:
- 来自: 北京
文章分类
- 全部博客 (153)
- linux (17)
- struts2 (3)
- mongodb (1)
- mysql (13)
- ant (1)
- java (12)
- hibernate,spring (4)
- 测试 (3)
- eclipse (2)
- svn (2)
- hudson (2)
- 程序员 (1)
- maven (3)
- jenkins (1)
- vnc (1)
- sublime (1)
- shell (6)
- cookie,session,token区别 (2)
- idea (4)
- git (6)
- hive (3)
- 效率 (2)
- nginx (2)
- vim (1)
- 高性能mysql (1)
- js (1)
- http (1)
- 消息队列 (1)
- 单点登录 (1)
- ElementUI (1)
- springboot (1)
- redis (1)
- CommandLineRunner (1)
- 存储过程 (1)
- 慢查询 (1)
- mysql主从 (1)
- canal (1)
- jdk proxy (1)
- cglib (1)
- JVM (8)
- 字节码 (1)
- 分布式 (2)
- rpc (1)
- 网卡 (2)
- IM (1)
- QA (1)
最新评论
-
sunshine_bean:
值得参考
异常 A web application registered the JBDC driver -
babyhhcsy:
你跟进出来结果没有啊
To prevent a memory leak, the JDBC Driver has been forcibly unregistered -
左岸代码右岸诗:
换tomcat的版本不是解决问题的根本方法啊,继续寻找解决方案 ...
异常 A web application registered the JBDC driver -
ywf008:
我在win7环境下报java.net.UnknownHostE ...
java.net.UnknownHostException 异常处理 -
javapub:
用目前oracle提供的java7,上面说的新特性好像都编译不 ...
Java 7七大新功能预览z
前言
这段时间在研究一个开源框架,发现其中有一些以SPI命名的包,经过搜索、整理以及思考之后,将学习的笔记、心得整理出来,供日后复习使用。
SPI
SPI全称是Service Provider Interface,翻译过来是服务提供者接口,这个翻译其实不那么形象,理解起来也不是很好理解,至少不那么见名知意。
其实SPI是一种机制,一种类似于服务发现的机制,什么叫做服务发现呢,就是能够根据情况发现已有服务的机制,好像说了跟没说一样,对吧,下面我们逐个来理解。
首先是服务,英文叫做Service,服务可以理解为就是某一种或者某几种功能,比如日常生活中的医生,提供看病的服务;家政公司,提供家政服务;房产中介公司,提供,这样子的话,关于服务,应该是理清楚了。
接下来是服务的发现,英文是Service Discovery,理解了服务,那么服务的发现就应该很好理解了,用大白话讲就是具有某种能力,可以发现某些服务,比如生活中的房产中介公司(服务发现),他们就能够发现很多的拥有空闲房子并且愿意出租的人(服务)。
SPI机制的作用就是服务发现,也就是说,我们有一些服务,然后通过SPI机制,就能让这些服务被需要的人所使用,而我们这些服务被发现的过程就是SPI的任务了。
说到这里,可能你还是不太理解SPI是什么,接下来我们通过具体的例子分析来理解SPI。
在JDBC4.0之前,我们使用JDBC去连接数据库的时候,通常会经过如下的步骤
将对应数据库的驱动加到类路径中
通过Class.forName()注册所要使用的驱动,如Class.forName(com.mysql.jdbc.Driver)
使用驱动管理器DriverManager来获取连接
后面的内容我们不关心了。
这种方式有个缺点,加载驱动是由用户来操作的,这样就很容易出现加载错驱动或者更换驱动的时候,忘记更改加载的类了。
在JDBC4.0,现在我们使用的时候,上面的第二步就不需要了,并且能够正常使用,这个就是SPI的功劳了。
接下来我们先来看下为什么不需要第二步。
熟悉反射的同学应该知道,第二步其实就是将对应的驱动类加载到虚拟机中,也就是说,现在我们没有手动加载,那么对应的驱动类是如何加载到虚拟机中的呢,我们通过DriverManger的源码的了解SPI是如何实现这个功能的。
DriverManager.java
在DriverManager中,有一段静态代码(静态代码在类被加载的时候就会执行)
static {
// 在这里加载对应的驱动类
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
接下来我们来具体看下其内容
loadInitialDrivers()
private static void loadInitialDrivers() {
String drivers;
try {
// 先获取系统变量
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// SPI机制加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通过ServiceLoader.load进行查找,我们的重点也是这里,后面分析
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获取迭代器,也请注意这里
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 遍历迭代器
// 这里需要这么做,是因为ServiceLoader默认是延迟加载
// 只是找到对应的class,但是不加载
// 所以这里在调用next的时候,其实就是实例化了对应的对象了
// 请注意这里 -------------------------------------------------------------------- 1
while(driversIterator.hasNext()) {
// 真正实例化的逻辑,详见后面分析
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 同时加载系统变量中找到的驱动类
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 由于是系统变量,所以使用系统类加载器,而不是应用类加载器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
从上面的代码中并没有找到对应的操作逻辑,唯一的一个突破点就是ServiceLoader.load(Driver.class)方法,该方法其实就是SPI的核心啦
接下来我们来分析这个类的代码(代码可能有点长哦,要有心理准备)
ServiceLoader.java
public final class ServiceLoader<S>
implements Iterable<S>
{
/**
* 由于是调用ServiceLoader.load(Driver.class)方法,所以我们先从该方法分析
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前的上下文线程
// 默认情况下是应用类加载器,具体的内容稍后分析
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用带加载器的加载方法
return ServiceLoader.load(service, cl);
}
/**
* 带类加载器的加载方法
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 只是返回一哥ServiceLoader对象,调用自己的构造函数嘛
return new ServiceLoader<>(service, loader);
}
/**
* 私有构造函数
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 目标加载类不能为null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 获取类加载器,如果cl是null,则使用系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 调用reload方法
reload();
}
// 用于缓存加载的服务提供者
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正查找逻辑的实现
private LazyIterator lookupIterator;
/**
* reload方法
*/
public void reload() {
// 先清空内容
providers.clear();
// 初始化lookupIterator
lookupIterator = new LazyIterator(service, loader);
}
}
LazyIterator.class
LazyIterator是ServiceLoader的私有内部类
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
/**
* 私有构造函数,用于初始化参数
*/
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
到了上面的内容,其实ServiceLoader.load()方法就结束了,并没有实际上去查找具体的实现类,那么什么时候才去查找以及加载呢,还记得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();这一行代码吗,这一行代码用于获取一个迭代器,这里同样也没有进行加载,但是,其后面还有遍历迭代器的代码,上面标注为1的部分。
迭代器以及遍历迭代器的过程如下所示
ServiceLoader.java
public Iterator<S> iterator() {
return new Iterator<S>() {
// 注意这里的providers,这里就是上面提到的用于缓存
// 已经加载的服务提供者的容器。
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 底层其实委托给了providers
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
// 如果没有缓存,则查找及加载
return lookupIterator.hasNext();
}
// 同上
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
上面已经分析过了,ServiceLoader.load()方法执行到LazyIterator的初始化之后就结束了,真正地查找直到调用lookupIterator.hasNext()才开始。
LazyIterator.java
// 希望你还记得他
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
//检查 AccessControlContext,这个我们不关系
// 关键的核心是都调用了hasNextService()方法
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private boolean hasNextService() {
// 第一次加载
if (nextName != null) {
return true;
}
// 第一次加载
if (configs == null) {
try {
// 注意这里,获取了的完整名称
// PREFIX定义在ServiceLoader中
// private static final String PREFIX = "META-INF/services/"
// 这里可以看到,完整的类名称就是 META-INF/services/CLASS_FULL_NAME
// 比如这里的 Driver.class,完整的路径就是
// META-INF/services/java.sql.Driver,注意这个只是文件名,不是具体的类哈
String fullName = PREFIX + service.getName();
// 如果类加载器为null,则使用系统类加载器进行加载
// 类加载会加载指定路径下的所有类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else // 使用传入的类加载器进行加载,其实就是应用类加载器
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果pending为null或者没有内容,则进行加载,一次只加载一个文件的一行
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析读取到的每个文件,高潮来了
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
/**
* 解析读取到的每个文件
*/
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
// utf-8编码
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
// 一行一行地读取数据
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
// 返回迭代器
return names.iterator();
}
// 解析一行行的数据
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
// 查找是否存在#
// 如果存在,则剪取#前面的内容
// 目的是防止读取到#及后面的内容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
// 不能包含空格及制表符\t
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
// 检查第一个字符是否是Java语法规范的单词
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
// 检查每个字符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 如果缓存中没有,并且当前列表中也没有,则加入列表。
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
/**
* 上面解析完文件之后,就开始加载文件的内容了
*/
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
}
}
到此,解析的步骤就完成了,在一开始的DriverManager中,我们也看到了在DriveirManager中一直在调用next方法,也就是持续地加载找到的所有的Driver的实现类了,比如MySQL的驱动类,Oracle的驱动类啦。
这个例子有点长,但我们收获还是很多,我们知道了JDBC4不用手动加载驱动类的实现原理,其实就是通过ServiceLoader去查找当前类加载器能访问到的目录下的WEB-INF/services/FULL_CLASS_NAME文件中的所有内容,而这些内容由一定的规范,如下
每行只能写一个全类名
#作为注释
只能使用utf-8及其兼容的编码
每个实现类必须提供一个无参构造函数,因为是直接使用class.newInstance()来创建实例的嘛
由此我们也明白了SPI机制的工作原理,那么这个东西有什么用呢,其实JDBC就是个最好的例子啦,这样用户就不需要知道到底是要加载哪个实现类,一方面是简化了操作,另一方面避免了操作的错误,当然,这种一般是用于写框架之类的用途,用于向框架使用者提供更加便利的操作,比如上面的引导我看到SPI的例子,其实是来自一个RPC框架,通过SPI机制,让我们可以直接编写自定义的序列化方式,然后由框架来负责加载即可。
SPI实战小案例
上面学习完了SPI的例子,也学习完了JDBC是如何实现的,接下来我们来通过一个小案例,来动手实践一下SPI是如何工作的。
新建一个接口,内容随便啦
HelloServie.java
public interface HelloService {
void sayHello();
}
然后编写其实现类
HelloServiceImpl.java
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
关键点来了,既然是学习SPI,那么我们肯定不是手动new一个实现类啦,而是通过SPI的机制来加载,如果认真地看完上面的分析,那么下面的内容应该很容易看懂啦,如果没看懂,再回去看一下啦。
在实现类所在项目(这里是同个项目哈)的类路径下,如果是maven项目,则是在resources目录下
建立目录META-INF/services
建立文件cn.xuhuanfeng.spi.HelloService(接口的全限定名哈)
内容是实现类的类名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意这里我们直接放在同个项目,不是同个项目也可以的!!!)
自定义一个加载的类,并且通过ServiceLoader.load()方法进行加载,如下所示
public class HelloServiceFactory {
public HelloService getHelloService() {
ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class);
return load.iterator().next();
}
}
测试一下,enjoy
如果你有兴趣的话,可以尝试将实现放在另一个项目中,然后打包成jar包,再放置在测试项目的classpath中,enjoy
总结
本小节我们主要学习了SPI,主要包括了SPI是什么,JDBC4中不需要手动加载驱动类的原理,并且详细看了DriverManager中的代码实现,最后,通过一个简单的小案例来实现我们自己的SPI服务,通过这个小节,应该说,SPI的大部分内容我们是掌握了,当然,里面管理类加载器部分我们还没有学习,这里先挖个坑,后面有时间再分析一下。
作者:颜洛滨
链接:https://www.jianshu.com/p/3039aa89b1b5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
这段时间在研究一个开源框架,发现其中有一些以SPI命名的包,经过搜索、整理以及思考之后,将学习的笔记、心得整理出来,供日后复习使用。
SPI
SPI全称是Service Provider Interface,翻译过来是服务提供者接口,这个翻译其实不那么形象,理解起来也不是很好理解,至少不那么见名知意。
其实SPI是一种机制,一种类似于服务发现的机制,什么叫做服务发现呢,就是能够根据情况发现已有服务的机制,好像说了跟没说一样,对吧,下面我们逐个来理解。
首先是服务,英文叫做Service,服务可以理解为就是某一种或者某几种功能,比如日常生活中的医生,提供看病的服务;家政公司,提供家政服务;房产中介公司,提供,这样子的话,关于服务,应该是理清楚了。
接下来是服务的发现,英文是Service Discovery,理解了服务,那么服务的发现就应该很好理解了,用大白话讲就是具有某种能力,可以发现某些服务,比如生活中的房产中介公司(服务发现),他们就能够发现很多的拥有空闲房子并且愿意出租的人(服务)。
SPI机制的作用就是服务发现,也就是说,我们有一些服务,然后通过SPI机制,就能让这些服务被需要的人所使用,而我们这些服务被发现的过程就是SPI的任务了。
说到这里,可能你还是不太理解SPI是什么,接下来我们通过具体的例子分析来理解SPI。
在JDBC4.0之前,我们使用JDBC去连接数据库的时候,通常会经过如下的步骤
将对应数据库的驱动加到类路径中
通过Class.forName()注册所要使用的驱动,如Class.forName(com.mysql.jdbc.Driver)
使用驱动管理器DriverManager来获取连接
后面的内容我们不关心了。
这种方式有个缺点,加载驱动是由用户来操作的,这样就很容易出现加载错驱动或者更换驱动的时候,忘记更改加载的类了。
在JDBC4.0,现在我们使用的时候,上面的第二步就不需要了,并且能够正常使用,这个就是SPI的功劳了。
接下来我们先来看下为什么不需要第二步。
熟悉反射的同学应该知道,第二步其实就是将对应的驱动类加载到虚拟机中,也就是说,现在我们没有手动加载,那么对应的驱动类是如何加载到虚拟机中的呢,我们通过DriverManger的源码的了解SPI是如何实现这个功能的。
DriverManager.java
在DriverManager中,有一段静态代码(静态代码在类被加载的时候就会执行)
static {
// 在这里加载对应的驱动类
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
接下来我们来具体看下其内容
loadInitialDrivers()
private static void loadInitialDrivers() {
String drivers;
try {
// 先获取系统变量
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// SPI机制加载驱动类
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 通过ServiceLoader.load进行查找,我们的重点也是这里,后面分析
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获取迭代器,也请注意这里
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
// 遍历迭代器
// 这里需要这么做,是因为ServiceLoader默认是延迟加载
// 只是找到对应的class,但是不加载
// 所以这里在调用next的时候,其实就是实例化了对应的对象了
// 请注意这里 -------------------------------------------------------------------- 1
while(driversIterator.hasNext()) {
// 真正实例化的逻辑,详见后面分析
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
// 同时加载系统变量中找到的驱动类
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
// 由于是系统变量,所以使用系统类加载器,而不是应用类加载器
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
从上面的代码中并没有找到对应的操作逻辑,唯一的一个突破点就是ServiceLoader.load(Driver.class)方法,该方法其实就是SPI的核心啦
接下来我们来分析这个类的代码(代码可能有点长哦,要有心理准备)
ServiceLoader.java
public final class ServiceLoader<S>
implements Iterable<S>
{
/**
* 由于是调用ServiceLoader.load(Driver.class)方法,所以我们先从该方法分析
*/
public static <S> ServiceLoader<S> load(Class<S> service) {
// 获取当前的上下文线程
// 默认情况下是应用类加载器,具体的内容稍后分析
ClassLoader cl = Thread.currentThread().getContextClassLoader();
// 调用带加载器的加载方法
return ServiceLoader.load(service, cl);
}
/**
* 带类加载器的加载方法
*/
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
// 只是返回一哥ServiceLoader对象,调用自己的构造函数嘛
return new ServiceLoader<>(service, loader);
}
/**
* 私有构造函数
*/
private ServiceLoader(Class<S> svc, ClassLoader cl) {
// 目标加载类不能为null
service = Objects.requireNonNull(svc, "Service interface cannot be null");
// 获取类加载器,如果cl是null,则使用系统类加载器
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
// 调用reload方法
reload();
}
// 用于缓存加载的服务提供者
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正查找逻辑的实现
private LazyIterator lookupIterator;
/**
* reload方法
*/
public void reload() {
// 先清空内容
providers.clear();
// 初始化lookupIterator
lookupIterator = new LazyIterator(service, loader);
}
}
LazyIterator.class
LazyIterator是ServiceLoader的私有内部类
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
/**
* 私有构造函数,用于初始化参数
*/
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
}
到了上面的内容,其实ServiceLoader.load()方法就结束了,并没有实际上去查找具体的实现类,那么什么时候才去查找以及加载呢,还记得上面的Iterator<Driver> driversIterator = loadedDrivers.iterator();这一行代码吗,这一行代码用于获取一个迭代器,这里同样也没有进行加载,但是,其后面还有遍历迭代器的代码,上面标注为1的部分。
迭代器以及遍历迭代器的过程如下所示
ServiceLoader.java
public Iterator<S> iterator() {
return new Iterator<S>() {
// 注意这里的providers,这里就是上面提到的用于缓存
// 已经加载的服务提供者的容器。
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();
// 底层其实委托给了providers
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
// 如果没有缓存,则查找及加载
return lookupIterator.hasNext();
}
// 同上
public S next() {
if (knownProviders.hasNext())
return knownProviders.next().getValue();
return lookupIterator.next();
}
public void remove() {
throw new UnsupportedOperationException();
}
};
}
上面已经分析过了,ServiceLoader.load()方法执行到LazyIterator的初始化之后就结束了,真正地查找直到调用lookupIterator.hasNext()才开始。
LazyIterator.java
// 希望你还记得他
private class LazyIterator
implements Iterator<S>
{
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;
//检查 AccessControlContext,这个我们不关系
// 关键的核心是都调用了hasNextService()方法
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
private boolean hasNextService() {
// 第一次加载
if (nextName != null) {
return true;
}
// 第一次加载
if (configs == null) {
try {
// 注意这里,获取了的完整名称
// PREFIX定义在ServiceLoader中
// private static final String PREFIX = "META-INF/services/"
// 这里可以看到,完整的类名称就是 META-INF/services/CLASS_FULL_NAME
// 比如这里的 Driver.class,完整的路径就是
// META-INF/services/java.sql.Driver,注意这个只是文件名,不是具体的类哈
String fullName = PREFIX + service.getName();
// 如果类加载器为null,则使用系统类加载器进行加载
// 类加载会加载指定路径下的所有类
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else // 使用传入的类加载器进行加载,其实就是应用类加载器
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 如果pending为null或者没有内容,则进行加载,一次只加载一个文件的一行
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
// 解析读取到的每个文件,高潮来了
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
/**
* 解析读取到的每个文件
*/
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
// utf-8编码
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
// 一行一行地读取数据
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
// 返回迭代器
return names.iterator();
}
// 解析一行行的数据
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
String ln = r.readLine();
if (ln == null) {
return -1;
}
// 查找是否存在#
// 如果存在,则剪取#前面的内容
// 目的是防止读取到#及后面的内容
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
// 不能包含空格及制表符\t
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
// 检查第一个字符是否是Java语法规范的单词
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
// 检查每个字符
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
// 如果缓存中没有,并且当前列表中也没有,则加入列表。
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
/**
* 上面解析完文件之后,就开始加载文件的内容了
*/
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
}
}
到此,解析的步骤就完成了,在一开始的DriverManager中,我们也看到了在DriveirManager中一直在调用next方法,也就是持续地加载找到的所有的Driver的实现类了,比如MySQL的驱动类,Oracle的驱动类啦。
这个例子有点长,但我们收获还是很多,我们知道了JDBC4不用手动加载驱动类的实现原理,其实就是通过ServiceLoader去查找当前类加载器能访问到的目录下的WEB-INF/services/FULL_CLASS_NAME文件中的所有内容,而这些内容由一定的规范,如下
每行只能写一个全类名
#作为注释
只能使用utf-8及其兼容的编码
每个实现类必须提供一个无参构造函数,因为是直接使用class.newInstance()来创建实例的嘛
由此我们也明白了SPI机制的工作原理,那么这个东西有什么用呢,其实JDBC就是个最好的例子啦,这样用户就不需要知道到底是要加载哪个实现类,一方面是简化了操作,另一方面避免了操作的错误,当然,这种一般是用于写框架之类的用途,用于向框架使用者提供更加便利的操作,比如上面的引导我看到SPI的例子,其实是来自一个RPC框架,通过SPI机制,让我们可以直接编写自定义的序列化方式,然后由框架来负责加载即可。
SPI实战小案例
上面学习完了SPI的例子,也学习完了JDBC是如何实现的,接下来我们来通过一个小案例,来动手实践一下SPI是如何工作的。
新建一个接口,内容随便啦
HelloServie.java
public interface HelloService {
void sayHello();
}
然后编写其实现类
HelloServiceImpl.java
public class HelloServiceImpl implements HelloService {
@Override
public void sayHello() {
System.out.println("hello world");
}
}
关键点来了,既然是学习SPI,那么我们肯定不是手动new一个实现类啦,而是通过SPI的机制来加载,如果认真地看完上面的分析,那么下面的内容应该很容易看懂啦,如果没看懂,再回去看一下啦。
在实现类所在项目(这里是同个项目哈)的类路径下,如果是maven项目,则是在resources目录下
建立目录META-INF/services
建立文件cn.xuhuanfeng.spi.HelloService(接口的全限定名哈)
内容是实现类的类名:cn.xuhuanfeng.spi.impl.HelloServiceImpl(注意这里我们直接放在同个项目,不是同个项目也可以的!!!)
自定义一个加载的类,并且通过ServiceLoader.load()方法进行加载,如下所示
public class HelloServiceFactory {
public HelloService getHelloService() {
ServiceLoader<HelloService> load = ServiceLoader.load(HelloService.class);
return load.iterator().next();
}
}
测试一下,enjoy
如果你有兴趣的话,可以尝试将实现放在另一个项目中,然后打包成jar包,再放置在测试项目的classpath中,enjoy
总结
本小节我们主要学习了SPI,主要包括了SPI是什么,JDBC4中不需要手动加载驱动类的原理,并且详细看了DriverManager中的代码实现,最后,通过一个简单的小案例来实现我们自己的SPI服务,通过这个小节,应该说,SPI的大部分内容我们是掌握了,当然,里面管理类加载器部分我们还没有学习,这里先挖个坑,后面有时间再分析一下。
作者:颜洛滨
链接:https://www.jianshu.com/p/3039aa89b1b5
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
发表评论
-
java线程状态
2021-08-15 21:42 425iteye简直烂透了 -
java线上常见问题定位
2020-07-29 19:53 831一 线上常见问题定位 常见问题 1:CPU 利用率高 C ... -
双亲委派模型知识点
2020-07-20 18:56 1129一。 JVM设计者把类加载阶段中的“通过'类全名'来获取定义此 ... -
java问题排查工具
2020-07-19 23:07 5181.arthas排查工具: https://alibaba.g ... -
JAVA线上问题定位
2020-07-19 23:07 292https://blog.csdn.net/weilan06/ ... -
【java基础】java注解知识
2018-10-08 12:52 569java注解有这一张图就够了 -
使用cookie做用户登陆详解【z】
2018-01-11 11:22 5344转自:http://blog.csdn.net/ ... -
深入java,初探jvm(转)
2012-12-19 23:34 1043转自:http://lrysir.iteye.com/blog ... -
java.net.UnknownHostException 异常处理
2012-12-05 00:05 111273在linux系统下部署Java产品的集群环境时,后台报出如下异 ... -
junit4的参数化测试z
2012-11-27 21:37 1468我感觉它的设计意图就是为了解决我们有时候测试的时候,测试数据的 ... -
ant编译java报“非法字符: \65279 ”错误的解决方法 z
2012-11-22 23:55 1482compile: [javac] Compiling ...
相关推荐
Java SPI 机制详解 Java SPI 机制,全称 Service Provider Interface,是 Java 内置的服务发现机制。SPI 机制的主要思想是解耦,通过提供一个标准接口,允许第三方提供实现类,而无需在程序中硬编码。SPI 机制广泛...
SPI机制则是一种服务提供者接口的机制,它允许第三方为某个接口实现提供实现,通过扩展名定的目录(通常是META-INF/services/目录)中的配置文件,服务提供方指定的实现类的全限定名,从而使得服务加载器可以动态地...
Java SPI 机制详解 Java SPI(Service Provider Interface)机制是一种服务提供发现机制,能够实现框架的扩展和替换组件,主要被框架的开发人员使用。SPI 机制的核心思想是将装配的控制权移到程序之外,在模块化...
Dubbo作为一款高性能的Java RPC框架,也引入了类似的SPI机制,但相对于Java内置的SPI,Dubbo的SPI机制更为强大和灵活。Dubbo的SPI机制主要由`dubbo-common`模块中的`ExtensionLoader`类实现,它支持以下特性: 1. ...
该项目为基于SPI机制设计的SpringBoot框架样例,旨在实现外部插件的热插拔功能,共计包含49个文件,其中包括32个Java源代码文件、9个XML配置文件、3个Git忽略文件、2个YAML文件、1个Markdown文件、1个JAR包文件以及1...
Java SPI机制详解.md
而SPI机制是Java平台提供的一种灵活的服务发现和加载方式,它促进了模块化开发和插件化的实现,增强了软件的可扩展性。这两个知识点对于Java开发者来说是必不可少的,深入掌握能提高开发效率和代码质量。
SPI机制的核心是`java.util.ServiceLoader`类,它允许我们按照约定在`META-INF/services`目录下创建配置文件,来指定哪些类实现了特定的服务接口。 服务接口定义:在SPI机制中,首先我们需要定义一个服务接口,这个...
除了基本的SPI机制,还可以通过自定义`java.util.ServiceLoader.Provider-Implementation`元数据来控制服务加载行为,或者使用第三方库如Apache Commons Lang的`ClassUtils`进行更复杂的类加载操作。 总之,JDK的...
"深入学习Java中的SPI机制" Java中的SPI(Service Provider Interface)机制是一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。SPI机制的主要思想是将装配的控制权移到程序...
尽管SPI通信具备诸多优点,例如低成本、易使用、支持全双工通信,并且通信过程中的数据可以一位一位地传输,但SPI通信机制存在明显的缺点,即缺乏数据校验机制、通信过程中没有握手和应答环节。这意味着主端设备无法...
1. SPI机制原理: - 服务接口:首先,定义一个公共的服务接口,例如`Search`,供其他组件使用。 - 服务实现:不同的提供商根据接口提供自己的实现,如`FileSearch`和`DatabaseSearch`。 - 配置文件:在每个提供商...
Java SPI机制实例详解 Java的SPI机制实例详解是Java提供的一种服务提供商接口机制,英文全名为Service Provider Interface。SPI机制实例详解主要是面向厂商或者插件的,普通开发人员可能不熟悉。Java的SPI机制实例...
在这个"spi_spi_SPI验证_"项目中,我们关注的是SPI接口的验证过程,它对于确保SPI设备的正确功能至关重要。SPI验证平台通常是一个综合性的测试环境,用于模拟不同主设备和从设备之间的交互,确保数据传输的准确性和...
这两个函数会处理必要的等待状态,确保数据正确发送或接收,并可能包含错误处理机制。 3. `SPI_TransmitReceive()`函数:同时处理数据的发送和接收,通常通过双数据速率(DDR)模式或者交替读写操作来实现。 4. 可能...
在此,我们将深入探讨SPI接口、DMA机制以及它们如何协同工作以优化LCD显示。 SPI(Serial Peripheral Interface)是一种同步串行接口,广泛用于连接微控制器和各种外围设备,如LCD显示屏。SPI协议通常包括四个信号...
SPI(Serial Peripheral Interface)是一种广泛应用于微控制器和其他设备之间的串行通信接口,它允许设备以全双工模式进行高速数据传输。在FPGA设计中,SPI接口常常被用来与外部设备如传感器、存储器等进行通信。...
SPI(Serial Peripheral Interface)协议是一种同步串行通信接口,广泛应用于微控制器和其他外围设备之间,如传感器、存储器、显示屏等。SPI协议以其简单、高效的特点,在嵌入式系统和物联网设备中扮演着重要角色。V...