way-to-architect
  • 前言
  • Java
    • Java关键字
      • Java中四种修饰符的限制范围
      • static和final
    • 容器
      • 容器概述
        • 容器:综述
        • Iterator原理及实现
        • fast-fail机制
        • 比较器Comparator
        • Collections工具类
      • List
        • List综述
        • ArrayList原理分析
        • ArrayList在循环过程中删除元素的问题
        • 常用的小技巧
        • CopyOnWrite
      • Set
        • Set综述
        • HashSet
        • LinkedHashSet
        • TreeSet
      • Queue
        • Queue综述
        • ArrayBlockingQueue实现原理
        • LinkedBlockingQueue实现原理
        • 高性能无锁队列Disruptor
      • Map
        • Map综述
        • HashMap
          • HashMap实现原理
          • HashMap中的位运算
          • HashMap其他问题
        • LinkedHashMap
        • TreeMap
        • ConcurrentHashMap
          • ConcurrentHashMap实现原理JDK1.7
          • ConcurrentHashMap实现原理JDK1.8
        • ConcurrentSkipListMap
        • Map中key和value的null的问题
    • 线程
      • 线程
        • 创建线程
        • 线程状态及切换
        • 线程中断的理解
        • 几种方法的解释
        • 用户线程与守护线程
        • 线程组ThreadGroup
      • 线程池
        • 线程池工作原理及创建
        • Executor
        • 如何确保同一属性的任务被同一线程执行
      • ThreadLocal
        • ThreadLocal原理
        • ThreadLocal之父子线程传值
        • InheritableThreadLocal
      • 同步与锁
        • 线程安全与锁优化
        • synchronize关键字
        • Lock
          • 队列同步器
            • 同步状态的获取与释放
            • 使用方式
            • 示例:Mutex
            • 示例:TwinsLock
          • 重入锁和读写锁
          • LockSupport
          • Condition
          • 并发工具类
        • CAS
          • CAS的理解
          • Java中原子操作类
        • 3个经典同步问题
      • fork/join的理解
    • I/O
      • I/O概述
        • 磁盘I/O与网络I/O
        • 主要接口
        • 输入流和输出流的使用示例
        • InputStream的重复读
        • BufferdxxxxStream
        • Serailizable
        • File常用方法
        • Files和Path
        • RandomAccessFile
        • 通过零拷贝实现有效数据传输
        • 正确地处理文件
      • NIO基础
      • NIO2
      • Netty
        • Java I/O的演进之路
        • 为什么是Netty
        • 更多
      • I/O调优
    • 异常
      • 异常体系及为什么要有这种异常设计
      • 多catch的执行情况
      • try catch finally 与reture
      • 异常处理的误区
      • Preconditions:方法入参校验工具
    • 枚举
      • 常见用法
      • 枚举类在序列化中的问题
    • 注解
      • 概述
      • Spring中的组合注解的条件注解
      • 常用注解
        • JSR-330标准注解
    • 反射
      • 概述
      • 内部类的反射
      • 反射中需要注意的地方
    • 流程控制
      • switch case without break
      • Java: for(;;) vs. while(true)
    • JVM
      • JVM内存结构
      • Java内存模型
      • 垃圾收集器和内存分配策略
      • 四种引用类型区别及何时回收
      • 类文件结构
      • 类初始化顺序
      • 类加载机制
      • 虚拟机执行引擎
      • 逃逸分析
      • JVM常用配置
      • GC日志分析
      • Java8 JVM 参数解读
      • 垃圾收集器和内存分配策略
    • 面向对象
      • Object类中的方法
      • Class类中的方法
      • 值传递还是引用传递?
      • 接口和抽象类的区别
      • 深拷贝和浅拷贝
      • Integer.parseInt()与Interger.valueof()
      • hashCode()与equal()
      • String
        • String池化及intern()方法的作用
        • 关于字符串
    • 序列化
      • Java序列化的方式有哪些?
    • 新特性
      • 流 Stream
        • Stream是什么
        • Stream API详解
        • Stream进阶
        • 流编程
        • 其他事项
      • lambda表达式
      • 默认方法(Default Methods)
      • @FunctionalInterface注解
    • SPI
      • 理解SPI
    • 字节码
      • javaagent
      • 字节码操纵
      • 如何查看类编译后的字节码指令
      • 字节码指令有哪些
  • Python
    • 异常处理
  • Go
  • 数据结构与算法
    • 数据结构
      • 概述
        • 线性表
        • 栈
        • 队列
        • 串
        • 树
        • 图
      • Java的一些实现
      • 红黑树
      • 双缓冲队列
      • 跳表SkipList
    • 算法
      • 概述
      • 常见算法
        • 基本排序
        • 高级排序
        • 动态规划
  • 框架或工具
    • Spring
      • Spring基础
        • Spring整体架构
        • 什么是IoC
        • Ioc容器的基本实现
        • Spring的MainClass
          • Spring的BeanFactory
          • Spring的Register
          • Spring的Resource和ResourceLoader
          • Spring的PropertySource
          • Spring的PropertyResolver
          • Spring的PropertyEditor
          • Spring的Convert
          • Spring的BeanDefinition
          • Spring的BeanDefinitionReader
          • Spring的BeanDefiniton其他Reader
          • Spring的BeanDefinition其他Reader2
          • Spring的Aware
          • Spring的BeanFctoryPostProcessor
          • Spring的BeanPostProcessor
          • Spring的Listener
        • Xml格式的应用启动
          • Xml格式的应用启动2
          • Xml格式的应用启动3
          • Xml格式的应用启动4
          • Xml格式的应用启动5
          • Xml格式的应用启动6
          • Xml格式的应用启动7
        • Spring中的设计模式
        • 什么是AOP
        • Spring中AOP的实现
      • Spring应用
        • Spring的事务控制
        • @Transactional注解在什么情况下会失效
        • 如何在数据库事务提交成功后进行异步操作
        • Spring中定时任务的原理
    • SpringMVC
      • Controller是如何将参数和前端传来的数据一一对应的
      • 请求处理流程
    • Zookeeper
      • Zookeeper是什么
      • Zookeeper能干啥
    • Shiro
    • druid
    • Netty
    • Consul
      • Consul是什么
    • etcd
    • confd
    • Akka
      • Actor模型是什么
  • 数据库
    • 基本概念
    • MySQL
      • 基本配置
      • MySQL数据类型
      • MySQL存储引擎
      • MySQL事务
        • MySQL事务概念
      • MySQL索引
        • MySQL中的索引类型
        • B-Tree/B+Tree概述
        • 为什么使用B+Tree
        • MySQL中的B+Tree索引
        • MySQL高性能索引策略
      • MySQL查询
        • MySQL查询过程
        • MySQL查询性能优化
        • 使用EXPLAIN
      • MySQL锁
        • MySQL中锁概述
        • InnoDB的并发控制
        • MySQL乐观锁
      • MySQL分库分表
        • 分库/分表
        • 跨库JOIN
        • 跨库分页
        • 分库分表后的平滑扩容
        • 分区表
        • 分布式ID生成方法
      • MySQL实战
        • 在线表结构变更
        • MySQL优化规则
        • MySQL问题排查
        • 常见查询场景
    • Redis
    • Hbase
    • OpenTSDB
    • rrd
    • MongoDB
    • 连接池
  • 系统设计
    • 一致性Hash算法
    • 限流
      • 限流是什么
      • 限流算法
      • 应用内限流
      • 分布式限流
      • 接入层限流
        • ngx_http_limit_conn_module
        • ngx_http_limit_req_module
        • lua_resty_limit-tarffic
      • 节流
    • 降级
      • 降级详解
      • 人工降级开关的实现
      • 自动降级的实现:Hystrix
    • 负载均衡
      • 概述
      • 互联网架构下的负载均衡
      • Nginx负载均衡(七层)
      • Nginx负载均衡(四层)
      • Nginx动态配置
    • 超时与重试机制
      • 什么地方要超时与重试
      • 代理层超时与重试
      • Web容器超时
      • 中间件客户端超时与重试
      • 数据库超时
      • NoSQL客户端超时设置
      • 业务超时
      • 前端请求超时
    • 网关
    • CAP
      • 什么是CAP
      • CAP理解
    • 生产者-消费者模型
      • 使用notify/wait方式
      • 使用await/signal实现
      • 使用阻塞队列实现
      • 使用信号量实现
      • 使用管道流实现
      • 无锁队列Disruptor
      • 双缓冲队列
    • 缓存
      • 缓存概述
      • 数据库缓存
      • 应用缓存
      • 前端缓存
      • 本地缓存
    • 秒杀
    • LRU
  • 版本控制
    • Git
      • Git常用命令
      • 场景命令
    • Svn
  • 计算机操作系统
    • Linux
      • Linux中重要概念
      • 常用命令
      • 查看日志
      • 权限管理
      • 登录或传输
      • 防火墙
      • 配置ssh免密
      • 进程
      • 防火墙
    • Mac
    • 计算机基础
      • 进制
      • Java中的位运算
      • 计算机存储系统结构
  • 网络
    • TCP三次握手和四次挥手
    • 网络术语
      • 网关、路由器、交换机、IP等
      • VLAN
      • LAN
  • 设计模式
    • 设计模式概述
    • 创建型
      • 单例模式
      • 工厂模式
      • 建造者模式
      • 原型模式
      • 享元模式
    • 行为型
      • 观察者模式
      • 策略模式
      • 模板模式
      • 责任链模式
      • 命令模式
      • 外观模式
      • 迭代器模式
      • 中介者模式
        • 中介模式续
      • 状态模式
        • 状态模式实例
        • 状态模式思考
      • 访问者模式
        • 访问者实例1
        • 访问者模式续
    • 结构型
      • 组合模式
        • 组合模式续
      • 装饰模式
        • 装饰模式续
      • 代理模式
      • 备忘录模式
      • 桥接模式
        • 桥接模式实例一
  • 构建工具
    • Maven
      • 常用命令
      • Maven生命周期
      • Maven中的变量和属性
      • 不同环境的如何配置不同的变量
      • 常用插件及配置
      • 其他问题
      • dependencies与dependencyManagement的区别
    • Gradle
  • 大数据
    • Hadoop
    • Storm
    • Spark
  • 服务器
    • Tomcat
      • server.xml配置详解
      • 线程池和连接数配置
      • Maven远程部署
      • 一些小技巧
      • Tomcat类加载机制分析
      • Tomcat的日志
      • Tomcat架构
        • 概述
        • Server 的启动流程
        • 请求处理流程
    • Nginx
      • 常用命令
      • 基本配置
      • Lua
    • Tengine
  • 中间件
    • 任务调度
      • 为什么需要任务调度
    • 消息队列
      • 为什么需要消息队列
      • 消息队列关键点
      • 消息中间件需要解决的问题
      • 不同消息队列产品对比
      • RocketMQ
        • 快速入门
        • 整体架构
        • 部署方式
          • Broker部署方案
        • 客户端使用
          • 客户端使用指南
          • 快速开始
          • 简单示例
          • 有序消息示例
          • 广播消息示例
          • 定时消息示例
          • 批量消息示例
          • 过滤消息示例
          • 日志输出配置示例
        • 关键点实现
          • 顺序消息的实现
        • 最佳实践
          • Broker的最佳实践
          • 生产者最佳实践
            • 生产者最佳实践续
          • 消费者最佳实践
            • 消费者最佳实践续
          • 名称服务最佳实践
          • JVM/kernel配置的最佳实践
          • 新特性 Filter Server
          • 其他事项
      • RabbitMQ
      • Kafka
    • 分布式事务
      • 什么是分布式事务
      • 解决方案
    • 服务治理
      • RPC概念
      • RPC最简实现
      • 为什么需要服务治理
      • Dubbo
        • Dubbo整体架构
      • Java RMI
    • 分布式锁
      • 如何设计分布式锁
        • 基于zookeeper
        • 基于Redis
    • 注册中心
      • 注册中心的职责
      • 不同注册中心的比较
    • 配置中心
      • 概述
      • 配置中心的实现与选型
  • Web开发
    • Http请求类型及区别
    • 常见的content-type
    • 如何处理跨域
    • Restful最佳实践
    • HTTP状态码
    • Http下载原理
  • 测试
    • 压测:apache bench
    • 压测:Jmeter
Powered by GitBook
On this page
  • 概念
  • SPI应用方式
  • 方式1
  • 方式2
  • 方式3
  • 方式4
  • SPI思想
  • 1、日志框架
  • 2、各种插件
  • 3、Spring
  • 4、Dubbo
  • 5、自定义SPI
  • 参考
  1. Java
  2. SPI

理解SPI

概念

SPI,Service Provider Interface,即服务提供者接口,是Java 6提供的新特性:对于给定接口,发现和加载该接口的实现类。

在面向对象的编程中,我们推荐面向接口编程。假如在项目A中有接口EchoService,它有两个实现类SimpleEchoServiceImpl和PrettyEchoServiceImpl,在项目B中,我们需要调用EchoService,并选择其实现SimpleEchoServiceImpl,则项目B中需要依赖项目A的jar文件,并编写代码如下:

EchoService echoService = new SimpleEchoServiceImpl();

如果后期在项目B中,需要使用实现PrettyEchoServiceImpl,则需要修改代码为:

EchoService echoService = new PrettyEchoServiceImpl();

这样硬编码的方式,违反了可插拔的原则。

SPI就是为解决这种问题而生的。

现在,我们使用SPI的方式去解决上述问题,需要遵循以下步骤:

①在项目B中的resource目录下新建目录META/service,并在该目录中新建名为com.xxx.yyy.EchoService的文件,并在该文件中写上如下内容:

com.xxx.yyy.impl.PrettyEchoServiceImpl

②在项目B中,使用SPI提供的ServiceLoader工具类加载实现类,代码如下:

ServiceLoader<EchoService> s = ServiceLoader.load(EchoService.class);
Iterator<EchoService> iterator = s.iterator();
while (iterator.hasNext()) {
    EchoService echoService =  iterator.next();
    echoService.methodXxxx();
}

SPI应用方式

通过上面的介绍和示例,我们了解了SPI的核心概念和一般使用流程。在实际场景中,SPI的使用方式或者项目组织方式并非总是如此。

方式1

上面的示例中,接口与实现方是在同一项目中,最终会打包为一个jar文件。而调用方则会依赖这个jar文件,并在自己的代码结构中添加指定实现类的目录和文件,最后使用ServiceLoader去加载实现类。

方式2

对于接口与实现方是在同一项目中的情况,通常会按照接口/实现1/实现2/...这种方式将整个项目划分为多个子模块(一个模块会打包为一个jar文件),在具体的实现者模块的代码结构中,新建META/service目录,并在该目录中新建名为com.xxx.yyy.ZzzService的配置文件,并在该文件中写明自己实现类全限定名。最后,调用方或使用方依赖接口模块的jar,并根据自己的需求,选择依赖一个或多个实现模块的jar,再通过ServiceLoader工具类加载实现类。

方式3

更为常见的应用方式是:接口和实现方分别是不同的项目。这是Java提供SPI机制的经典应用方式。

比如,JDBC中不同数据库驱动的加载,之前我们往往会通过如下方式去获取(假如获取MySQL的驱动):

Class.forName("com.mysql.Driver")

对于不同的数据库,如MySQL/Oracel/PostgreSQL,它们的驱动器肯定是不同的,甚至对同一种数据库,也可能会有多种驱动实现。如果使用上面硬编码的方式,很显然是不妥的。

看看使用SPI机制是如何解决上述问题的:Java提供了java.sql.Driver接口,不同的数据库厂商需要在自己的驱动实现中,按照Java SPI规范,在驱动包中,新建META/service目录,在其中新建一个名为java.sql.Driver的配置文件,并在该配置文件中指明自己的实现类。如mysql-connector-java.jar:

此后,通过Java提供的DriverManager进行加载:

public class DriverManager {
    ... ... 
    static {
        loadInitialDrivers();
        println("JDBC DriverManager initialized");
    }
    ... ...
    private static void loadInitialDrivers() {
        ... ... 
        ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
        Iterator<Driver> driversIterator = loadedDrivers.iterator();
        try{
            while(driversIterator.hasNext()) {
                driversIterator.next();
            }
        } catch(Throwable t) {
            // Do nothing
        }  
        ... ...   
    }
}

注意:Oracel并没有遵循该规范,所有还是要使用Class.forName的方式去加载驱动。

方式4

接口与调用方在同一项目中,不同实现者为不同的项目。这种方式下,需要在调用方和接口所在的项目中,根据需要去选择依赖实现者,再借助ServiceLoader去加载实现者。

在Java中,除了Driver这个大家比较熟悉的接口,还要很多SPI接口(但很少有实现方去遵循):

注意

假如在配置文件中,写了多个实现类,那么在ServiceLoader加载具体实现类的时候,要根据自己的需求去调用实现者:是任意选择一个实现类,还是根据某种规则选择一个实现类,还是这多个实现类都要去调用。

SPI思想

SPI的思想,其实就是让“使用方-接口-实现者”这三者尽量松耦合,避免使用硬编码的方式,而是声明一些规范,并通过配置文件的方式来解决硬编码的问题(当然,使用方与实现者是一定会耦合的,这里只是说让耦合方式更合理,改动的代价更小,维护的成本更低)。

这种思想的体现,其实非常常见。

1、日志框架

一开始,Java的日志工作都由log4j来处理,但后来发现项目太依赖log4j或者log4j不能满足需求想要更换日志框架,发现代价太大。这时,Apache Common Logging(前身为Jakarta Commons Logging,JCL)出现了,它只提供接口,不提供具体实现。这样,项目中只要依赖这个Common Logging,并选择依赖一种日志实现框架即可,项目可以随时切换日志实现方式, 避免了和具体的日志方案直接耦合。这里,Common Logging通过配置信息动态加载实现框架。这是SPI思想的一种体现。

当程序规模越来越庞大时,JCL的动态绑定并不是总能成功。解决方法之一就是在程序部署时静态绑定指定的日志工具,这就是 SLF4J 产生的原因。跟 JCL 一样,SLF4J 也是只提供 log 接口,具体的实现是在打包应用程序时所放入的绑定器(名字为 slf4j-XXX-version.jar)来决定,XXX 可以是 log4j12, jdk14, jcl, nop 等,他们实现了跟具体日志工具(比如 log4j)的绑定及代理工作。举个例子:如果一个程序希望用 log4j 日志工具,那么程序只需针对 slf4j-api 接口编程,然后在打包时再放入 slf4j-log4j12-version.jar 和 log4j.jar 就可以了。

2、各种插件

作为Java程序员,会对IDE工具如Eclipse/IDEAJ/Sublime等非常熟悉。通过,在使用这些IDE工具时,我们会下载很多扩展插件,以便使其具有我们想要的某个特定功能:下载插件文件,将其放在IDE工具安装目录下的某个文件夹下(通常是叫plugins),然后重启IDE,IDE就拥有了这个插件提供的功能。

这其实是IDE工具制定了一系列的规则,如文件结构、类型、参数等。插件开发者遵循这些规则去开发自己的插件,IDE工具并不需要知道插件具体是怎样开发的,只需要在启动的时候根据配置文件解析、加载到系统里就行了,这也是SPI思想的一种体现。

3、Spring

Spring的IOC容器可以看做是各种接口的实现类的大集合。我们在Bean的时候,需要指定bean的class属性,这个操作其实就是在告诉IOC容器实现类的路径在哪,在使用的时候,我们通常会使用注解并指明一个接口,这个操作其实就是在告诉IOC容器我需要这个接口的实现类。这种配置方式和使用与Java原生SPI机制的配置文件和Serviceloader就很像,也是SPI思想的体现。

在定义Bean的时候,我们还可以通过设置scope属性来告诉IOC容器,在创建实现者实例的时候,通过哪种方式去创建,是单例,还是每次使用都新建一个实现者实例。这可以理解为Spring为原生SPI机制提供了扩展:不仅接管发现和去哪加载实现者的工作,还接管了如何创建这个实现者的这个工作。

4、Dubbo

Dubbo插件化的实现其实就是SPI,但它对原生的SPI进行了扩展:Dubbo的规则是在META-INF/dubbo、META-INF/dubbo/internal或者META-INF/services下面去创建一个文件,并且在文件中以properties的规则去配置实现类,如

failover=com.alibaba.dubbo.rpc.cluster.support.FailoverCluster

Dubbo的插件化的辅助类是ExtensionLoader。它可以理解为Spring的IOC容器,只不过IOC容器里面做的事情是帮我们初始化和管理bean,我们可以根据我们需要的bean类型或者bean的id来获取对应的bean实体,而Dubbo里面的ExtensionLoader管理的是插件,同样我们可以根据具体插件实现别名和插件接口来获取我们想要的插件实现。另一个不同点是Spring是通过XML的方式告诉Spring我的bean的实现类全路径,而Dubbo则是通过SPI的方式告诉ExtensionLoader具体实现类信息。

5、自定义SPI

理解了SPI的思想之后,我们也可以实现一套自己的SPI机制,以便进行一些功能扩展:如接口的约束(比如接口必须要做某种标识--如可插拔的标识,标识该接口有多个实现类),接管实现类的创建方式(单例还是多例,要不要缓存等)等。在具体实现的时候,可以仿照原生SPI的做法:①约定配置文件的目录,配置文件的格式;②参照ServiceLoader,写一个加载实现类的类,这个类中,做一些约束检查、缓存等。

参考

PreviousSPINext字节码

Last updated 6 years ago

provides localized currency symbols for the Currency class.

: provides localized names for the Locale class.

provides localized time zone names for the TimeZone class.

: provides date and time formats for a specified locale.

: provides monetary, integer and percentage values for the NumberFormatclass.

as of version 4.0, the JDBC API supports the SPI pattern. Older versions uses the method to load drivers.

provides the implementation of the JPA API.

provides JSON processing objects.

provides JSON binding objects.

provides extensions for the CDI container.

provides a source for retrieving configuration properties.

CurrencyNameProvider:
LocaleNameProvider
TimeZoneNameProvider:
DateFormatProvider
NumberFormatProvider
Driver:
Class.forName()
PersistenceProvider:
JsonProvider:
JsonbProvider:
Extention:
ConfigSourceProvider:
Java SPI思想梳理
java SPI机制原理
设计原则:小议 SPI 和 API
Java Service Provider Interface
java日志组件介绍(common-logging,log4j,slf4j,logback )
跟我学Dubbo系列之Java SPI机制简介
从ExtensionLoader看Dubbo插件化
Java中SPI机制深入及源码解析