java spi 类加载_Java 基础之 SPI 机制

本文介绍了Java SPI(Service Provider Interface)机制,它用于实现服务接口与实现的解耦,增强程序的可扩展性。通过创建`META-INF/services/`目录下的接口全限定名文件,并列出实现类全限定名,可以使用`ServiceLoader`动态加载实现类。文中通过动物歌唱比赛的示例详细展示了SPI的使用,并讨论了MySQL驱动如何利用SPI加载实现类。最后,解释了ContextClassLoader的作用,允许在特定情况下变更类加载器,以便加载由不同加载器加载的类。
摘要由CSDN通过智能技术生成

# Java 基础之 SPI 机制

在前几天的译文 [Java中的类加载器](https://chenyongjun.vip/articles/67) 中有部分关于ContextClassLoader的内容,涉及到SPI机制,本文将学习下相关知识。

## 什么是SPI?

SPI全称为 **Service Provider Interface**,直译为 **服务提供者接口**,翻译成中文后比较拗口,难以理解。

简单来说,**SPI通过将服务的接口与实现分离以实现解耦,提高程序拓展性的机制,达到插拔式的效果**。相同的标准,各服务厂商可以提供不同的实现。这尤其适合于面对未知的实现或者对拓展开放的系统,可以先行制定标准,服务提供者根据标准提供实现即可。

Java中使用SPI机制的例子很多,例举几个:

* 数据库驱动 ( *java.sql.Driver* ),各数据库厂商(Mysql、Oracle等)可以遵守规范独立开发自己的驱动

* Servlet容器初始化接口( *javax.servlet.ServletContainerInitializer* ),Tomcat提供了实现

* Apache common-logging 中提供的日志接口,许多日志框架做了实现

稍微延伸一下,其实不仅仅是Java,像计算机行业的各种规范、协议也是类似的。甚至生活中的例子,如:

* 命题作文:设立标题,大家各自发挥成文

* 手机壳:根据手机尺寸标准,实现各式各样的手机壳

* 喜剧:以逗笑观众为标准,各表演者以不同的作品与形式为观众送去欢乐

扯的有点远,下面以一个简单例子演示下。

## SPI HelloWorld

首先,了解下SPI机制的约定(约定优于配置理念):

* 在 `META-INF/services/` 目录下创建一个以 **接口全限定名** 命名的文件,文件内容为 **实现类的全限定名**

* 使用 *java.util.ServiceLoader* 来动态加载 `META-INF/services/` 下的实现类

* 实现类必须有一个无参构造器

假设森林动物园举行歌唱比赛,各参赛动物选手需高歌一曲。我们定义一个接口 `Animal`,标准为 `sing()` 唱歌。

创建一个普通maven项目,创建以下对象。

```java

// Animal接口, 制定了 sing() 标准

public interface Animal {

void sing();

}

```

三位参赛选手,分别实现了`sing()` 标准

`Cat.java`

```java

public class Cat implements Animal {

public void sing() {

System.out.println("喵~");

}

}

```

`Cuckoo.java`

```java

public class Cuckoo implements Animal {

public void sing() {

System.out.println("布谷~");

}

}

```

`Dog.java`

```java

public class Dog implements Animal {

public void sing() {

System.out.println("汪~");

}

}

```

在resource下创建`META-INF/services`目录,下面创建以接口全限定名`org.utopiavip.spi.Animal`命名的文件,内容为三位实现者:

```java

org.utopiavip.spi.Cat

org.utopiavip.spi.Dog

org.utopiavip.spi.Cuckoo

```

将项目打成jar包。

在另一个项目中引入该jar包,测试类如下:

```java

public class SpiDemo {

public ServiceLoader serviceLoader = ServiceLoader.load(Animal.class);

public static void main(String args[]) {

SpiDemo spiDemo = new SpiDemo();

spiDemo.sing();

}

public void sing() {

for (Animal singer : serviceLoader) {

singer.sing();

}

}

}

```

运行后输出如下:

```

喵~

汪~

布谷~

```

小例子就完成了。

## Mysql驱动Demo

Mysql驱动包中对 *java.sql.Driver* 的实现类为 *com.mysql.fabric.jdbc.FabricMySQLDriver*

![](https://imgcdn.chenyongjun.vip/2018/08/23/4b654a61115d414db957ffcf3488d6d5.png)

再看看接口和实现类的ClassLoader。

```java

System.out.println(java.sql.Driver.class.getClassLoader());

System.out.println(com.mysql.fabric.jdbc.FabricMySQLDriver.class.getClassLoader());

```

输出结果如下:

```

null

sun.misc.Launcher$AppClassLoader@135fbaa4

```

null表示Bootstrap class loader(SPI的接口都由Bootstrap class loader加载),而实现类是由AppClassLoader加载的。

## ContextClassLoader

类加载规则中有这么一点:**一个类中所关联的其他类都由当前类的加载器进行加载**。

仍然以Driver为例,Java中使用DriverManager来获取JDBC连接,*DriverManager* 位于 *rt.jar* 中,由Bootstrap class loader负责加载。

```java

java.sql.DriverManager.getConnection("url", "user", "pwd")

```

在getConnection()的调用过程中,需要加载 *java.sql.Driver* 的实现类 *com.mysql.fabric.jdbc.FabricMySQLDriver*,可Bootstrap class loader无法找到该实现类,因为FabricMySQLDriver由System class loader加载。

这是由于类加载的委派原则及可见性制约,Bootstrap class loader将无法获取子加载器System class loader中加载的FabricMySQLDriver类。

为了解决这个问题,提出了 **ContextClassLoader** 概念,**绕开委派原则,既然当前的加载器是Bootstrap class loader,导致无法加载FabricMySQLDriver类,那就变更当前的class loader,想加载谁就加载谁!**虽然有点流氓派头,但确实是这么干的。规则是人定的,变更规则成本太高,就搞点特殊化。

`java.lang.Thread` 有个NB的方法 `setContextClassLoader()`,用来变更当前线程的class loader。

```java

public void setContextClassLoader(ClassLoader cl) {

SecurityManager sm = System.getSecurityManager();

if (sm != null) {

sm.checkPermission(new RuntimePermission("setContextClassLoader"));

}

contextClassLoader = cl;

}

```

**contextClassLoader** 取名也很有趣,当前线程的 context ClassLoader。特殊化也就搞一小会,不大范围搞。

```java

/* The context ClassLoader for this thread */

private ClassLoader contextClassLoader;

```

## 小结

BB这么多,SPI其实非常简单:大佬们定规矩(规范),兄弟们实现后放到约定的地方(`META-INF/service/`),包装上写好是啥东西(接口全限定名),包装里写清楚东西放哪儿了(实现类全限定名)。

扫码或搜索 codercyj 关注微信公众号, 结伴学习, 一起努力

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值