spi和双亲委派机制

spi

SPI service provider Interface是一种服务发现机制

概念

SPI的全名为Service Provider Interface

主要是应用于厂商自定义组件或插件中,在java.util.serviceLoeader的文档里有比较详细的介绍

简单的总结下java SPI机制的思想,我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块,XML解析模块,JDBC模块等方案.面向对象的设计里,我们一般推荐模块之间基于接口变成,模块之间不对实现类进行编码.一旦代码里设计具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码.为了实现在模块装配的时候,不在程序里面动态指明,这里就需要一种服务发现机制.JAVA SPI就是提供这样的一种机制:为了接口寻找服务实现的机制

Java SPI的具体定义为:当服务的提供者提供了服务接口的一种实现之后,在jar包的MATE-INF/service/ 目录里配置文件找到具体的实现类名,并装载实例化,完成模块的注入.基于这样的一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定.jdk提供服务查找的一个工具类(java.util.SErviceLoeader)


假设我们按照SPI来设计JDK,那么流程图如下

根据SPI的特点,我们会发现

  • 服务方(JDK)提供标准接口
  • 实现放独立的jar包,作为接口的实现:实现类的提供方.此处的场景就是
  • 服务方(JDK)需要有一种服务发现能力,才能找到实体类

服务发现机制

顾名思义,服务方制定好了标准接口后,利用方法去把实现类找到

我们日常开发中会遇到俩类

  • 一类是项目里引入jar包
  • 另一类是在微服务的环境中的服务治理去发现

这里只讲引jar包的方式,对JDK来说,就是从classpath中如何加载的问题

我们来看看JDK是怎么使用的

这段代码是DriverManager类中loadInitialDrivers()方法里一段逻辑;

//代码逻辑片段
...
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
    while(driversIterator.hasNext()) {
        driversIterator.next();
    }
} catch(Throwable t) {
// Do nothing
}
...

简单分析下源码

public static <S> ServiceLoader<S> load(Class<S> service) {
    //从当前线程中获取 线程上下文类加载器
    //默认情况下,就是AppClassLoader,也就是系统加载器。
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);
}

private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //这里就是Driver.class
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //线程上下文类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

接着我们来看看while(driversIterator.hasNext())这段源码:

public boolean hasNext() throws ServiceConfigurationError {
    if (this.nextName != null) {
        return true;
    } else {
        if (this.configs == null) {
            try {
 //会去classpath路径中的META-INF/services/去寻找以接口标准命名的文件。
 //在JDK中JDBC实现中就是java.sql.Driver。
 //也就是说JDK提供的ServiceLoader工具类会去classpath中META-INF/services/路径下去找以java.sql.Driver命名的文件
            //然后读取文件,文件里写着的就是接口的实现类
                String var1 = "META-INF/services/" + this.service.getName();
                if (this.loader == null) {
                    this.configs = ClassLoader.getSystemResources(var1);
                } else {
                    this.configs = this.loader.getResources(var1);
                }
            } catch (IOException var2) {
                Service.fail(this.service, ": " + var2);
            }
        }

        while(this.pending == null || !this.pending.hasNext()) {
            if (!this.configs.hasMoreElements()) {
                return false;
            }

            this.pending = Service.parse(this.service, (URL)this.configs.nextElement(), this.returned);
        }

        this.nextName = (String)this.pending.next();
        return true;
    }
}

通过阅读源码知道

  • JDK提供的ServiceLoader工具类回去classpath中META-INF/.service/路径下去找以java.sql.Driver命名的文件
  • 接着从文件中得到标准接口的实现类,并用线程上下文类加载器加载进JVM

为什么要使用线程上下文类加载器?

我们知道jdk1.2开始,使用双亲委派的方式加载类

  • Bootstrap classLoader 主要负责加载核心的类库,默认加载的是%JAVA_HOME%中lib下的jar包和class类文件,构造ExtClassLoeader和APPClassloader
  • ExtClassLoader:主要负责加载jre/lib/ext目录下的一些扩展的jar
  • AppClassLoader:主要负责加载当前应用程序的主函数类

我们开发人员编写的类,包括第三方类库,都是appClassloader类加载器来进行的

回到上面的SPI设计思路,MYSQL提供的驱动类库,就是APClassLoader类加载器来加载的

既然如此为什么还要设计上下文类加载器呢?

首先我们知道标注接口java.sql.Driver是在核心类库里面的,也就是说JDK中的java.sql.Driver是由Bootstrap classLoader(启动类加载器)进行加载的.那么它的实现类,自然也是Bootstrap classLoader进行加载.那如果Boostrap classloader加载器加载不了怎么办?(第三方类库不在核心包路径下,Bootstrap是无法加载的)

Bootstrap classLoader无法加载怎么办?

按照双亲委派机制,如果上层类加载器加载不了,那就交给下一层类加载器

按照双亲委派的机制,由系统类加载器,加载第三方类库(比如:MySQL驱动),根据规则,最终会交给启动类加载器进行加载。也就符合了SPI接口java.sql.Driver由启动类加载器加载,实现类也得是启动类加载器加载。

这么看来,线程上下文类加载器,设计的不是有点多余吗?

哪里错了?

在加载第三方类库时,加载Driver类的实现类时,一开始就不是系统类加载器去加载的,而是直接使用启动类加载器去加载的:

而ServiceLoeader根据类,可以理解为就是对上下文类加载器进行了封装

这就是为什么要破坏双亲委派,引入线程上下文类加载器的原因

聊聊SPI与API、SPI打破双亲委派的原因

双亲委派的破坏

上面讲的SPI是第二次破坏双亲委派

到目前为止总共有四次破坏了双亲委派

第一次,历史原因,在jdk1.2开始引入双亲委派机制,那么在此之前如 jdk1.1的时候,存在的自定义的类加载器,它们就不符合双亲委派。而实际上,我们可以继承ClassLoader类,然后重写loadClass方法,也可以打破双亲委派。(打破双亲委派,不一定是坏事)

第二次,SPI上面已经讲了,引入线程上下文,特意打破双亲委派

第三次,用户对程序动态性的追求导致的即代码热替换,模块热部署

OSGI实现模块化热部署的关键则是它自定义类加载器机制的实现.每一个程序模块(Bundle)都有一个自己类加载器,当需要更换一个Bundile时,就把Bundle炼铜类加载器一起替换掉,以实现代码的热替换.在OSGI环境下,类加载器不再是双亲委派模型中的树状结构,而是交一部分发展为更加复杂的网状结构

第四次是JDK9,引入了模块化,当平台及应用程序类加载器收到类加载请求,在委托给父类加载器加载前,要先判断该类是否能够树与岛某一个系统模块中,如果找到这样的归属关系,就要有限负责给那个模块的加载器完成加载

经过破坏后的双亲委派模型更加高效,减少了很多类加载器之间不必要的委派操作。

练习

最近人民日报,批评国内互联网巨头,不要盯着老百姓的菜篮子,要向往科技创新的星辰大海。但商人逐利,肯定不怎么听话。

假设高层要求,外卖接口以后全国统一,国家实现统一的外卖平台。现有的外卖平台:开水团和饿不死,将作为内容提供方;即:国家制定SPI接口,开水团和饿不死 实现这些接口,该怎么实现?

自己模拟的话,就需要有个思路:

模拟服务方定义SPI接口
模式提供方实现SPI的接口
利用工具类ServiceLoader,模拟服务发现。在resources目录下创建META-INF/services创建SPI接口全类限定名的文件,里面存放实现类的全类限定名。

服务提供方接口

package com.ssm.boot.admin.java.spi.waimai.spi;

/**
 * SPI  点外卖
 */
public interface OrderTakeOut {

    void order();
}

服务实现方

饿不死

package com.ssm.boot.admin.java.spi.waimai.provider;

import com.ssm.boot.admin.java.spi.waimai.spi.OrderTakeOut;

public class EbsOrder implements OrderTakeOut {
    @Override
    public void order() {
        System.out.println("饿不死下单了");
    }
}

开水团

package com.ssm.boot.admin.java.spi.waimai.provider;

import com.ssm.boot.admin.java.spi.waimai.spi.OrderTakeOut;

public class WaterOrder implements OrderTakeOut {
    @Override
    public void order() {
        System.out.println("开水团下单了");
    }
}

package com.ssm.boot.admin.java.spi.waimai;

import com.ssm.boot.admin.java.spi.waimai.spi.OrderTakeOut;

import java.util.Iterator;
import java.util.ServiceLoader;

public class TakeOutTest {

    public static void main(String[] args) {

        //这里是模拟服务发现
        ServiceLoader<OrderTakeOut> load = ServiceLoader.load(OrderTakeOut.class);
        Iterator<OrderTakeOut> iterator = load.iterator();
        while (iterator.hasNext()) {
            OrderTakeOut next = iterator.next();
            next.order();
        }
    }
}

打印结果

开水团下单了
饿不死下单了

OSGI

简介

OSGI(Open Service Getway Intiative 直译为开放服务网关)实际上是由一个OSGI联盟发起的以java为技术的平台的动态模块化规范

随着OSGi技术的不断发展,OSGi联盟的成员数量已经由最开始的几个增长到目前超过100个,很多世界著名的IT企业都加入到OSGi的阵营之中,如Adobe、IBM、Oracle、SAP、RedHat和Siemens等。它们推出的许多产品都支持OSGi技术,甚至产品本身就使用了OSGi技术构建,例如IBM的WebSphere、Lotus和JAZZ,Oracle的GlassFish和Weblogic,RedHat的JBoss,Eclipse基金会的Eclipse IDE、Equinox及之下的众多子项目,Apache基金会的Karaf、Aries、Geronimo、Felix及之下的众多子项目等。这些IT巨头的踊跃参与,也从侧面证明了OSGi技术有着非常广阔的市场前景。
今天,OSGI的已经不再是原来的Open Service Getway Intiative的字面意思能涵盖的了,OSGI联盟给出的最新OSGI定义是The Dynamic Module System for Java

OSGI的功能

以一个简单开的开发场景作说明:假设我们使用SSM框架来开发我们的Web项目,我们做产品设计和开发的时候都是分模块的,我们分模块的目的就是实现模块之间的“解耦”,更进一步的目的是方便对一个项目的控制和管理。我们对一个项目进行模块化分解之后,我们就可以把不同模块交给不同的开发人员来完成开发,然后项目经理把大家完成的模块集中在一起,然后拼装成一个最终的产品。一般我们开发都是这样的基本情况。

在开发过程中,模块之间还要彼此保持联系,比如A模块要从B模块拿到一些数据,而B模块可能要调用C模块中的一些方法(除了公共底层的工具类之外)。

最后,我们要把最终的项目部署到tomcat或者jBoss的服务器中。那么我们启动服务器的时候,能不能关闭项目的某个模块或功能呢?很明显是做不到的,一旦服务器启动,所有模块就要一起启动,都要占用服务器资源,所以关闭不了模块,假设能强制拿掉,就会影响其它的功能。

以上就是我们传统模块式开发的一些局限性。

软件开发一直在追求一个境界,就是模块之间的真正“解耦”、“分离”,这样我们在软件的管理和开发上面就会更加的灵活,甚至包括给客户部署项目的时候都可以做到更加的灵活可控。但是我们以前使用SSM等架构模式进行产品开发的时候我们是达不到这种要求的。

所以OSGI的技术规范应运而生。现在的OSGI技术就可以满足我们之前所说的境界:在不同的模块中做到彻底的分离,而不是逻辑意义上的分离,是物理上的分离,也就是说在运行部署之后都可以在不停止服务器的时候直接把某些模块拿下来,其他模块的功能也不受影响。

现在主流的一些应用服务器,Oracle的weblogic服务器,IBM的WebSphere,JBoss,还有Sun公司的glassfish服务器,都对OSGI提供了强大的支持,都是在OSGI的技术基础上实现的。

简单点说,OSGI被设计专门用来开发可分解为功能模块的复杂的Java应用。 OSGI提供以下优势:

可以动态地安装、卸载、启动、停止不同的应用模块,而不需要重启容器。
在同一时刻可以跑多个同一个应用模块的实例。
OSGI在SOA领域提供成熟的解决方案,包括嵌入式,移动设备和富客户端应用等。

重要概念

Bundle

Bundle其实就是个jar包,是OSGI特定描述的一个jar包,具体位置在META-INF目录下的MANIFEST.MF文件,Bundle有很多种状态,他们之间的转换关系如图所示

这么做的好处就是很好的隔离了外部类和内部类,一个个的Bundle组成了基于OSGI的应用,Bundle是组件服务的载体

那么Bundle是如何交互的呢?这就要使用到service

service

一个OSGI Service就是一个注册到OSGI框架中的java对象,在注册的时候可以设置Service属性,在获取Service的时候也可以根据属性进行过滤,具体交互过程如图所示

SOCM

socm的全称是Service Oriented Component Model.意思是面向服务的组件模型,这里的Component,任何一个普通的java对象都可以通过配置文件定义来得到,Component其实就是服务的载体,即可以提供对外使用的服务也可以使用外部服务

那么问题又来了,Component如何组装起来的呢?这就要用到Declarative Service

Declarative Service

简称DS,DS可以让我们在Bundle中定义Component,通过配置的方式发现服务和获取服务

OSGI

OSGI框架属于OSGI服务平台,除此之外还有OSGI标注服务,前者是提供OSGI给你的运行环境,后者是定义了很多常见任务的可重用API,后者不需要我们参与,直接用就好,前者才是重点

OSGI就如同操作系统一样,为应用程序的运行提供运行环境,我们并不能直接感受到操作系统,但是他一直为我们服务,同样,OSGI我们感知不到他的存在,但是我们却每时每刻,在享受他带给我们的便利

OSGI一共分三层,就如同网络分层结构一样

模块层:模块层定义了OSGI的概念,称之为bundle,可以说跟模块层打交道,正因如此,我才把Bundle等重要概念放在前面,之前说的Bundle可能有些复杂,其实Bundle就是个包含元数据,类文件和相关资源的jar包,但比普通的jar包更加强大,Bundle可以声明哪些包对外可见,也就是哪些包是可以导出的,以及可以声明可以依赖哪些外部包,也就是哪些包可以导入的

下面是模块层的hello world

同时也应该在META-INF目录下的MANIFEST.MF文件中添加一写元数据信息;

生命周期层: 如果说之前的模块层负责盖房子,这一层就好比是房子盖好之后搞装修,把水电这类的通好才能生活;生命周期层一个是在应用程序的外部定义了Bundle生命周期的操作(安装、卸载等),另一个就是在应用程序的内部定义了Bundle如何访问他们的执行环境;

下面是生命周期层的hello world的代码示例

同时还要实现一个Bundle的激活器

服务层的工作原理与面向服务相似

实现的时候先定义一个接口再实现,

用Bundle激活器完成服务注册

用Bundle激活器完成服务发现

Last modification:December 23, 2022
如果觉得我的文章对你有用,请随意赞赏