范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

还不会JavaSPI机制?

  什么是 SPI 1. 背景
  在面向对象的设计原则中,一般推荐模块之间基于接口编程,通常情况下调用方模块是不会感知到被调用方模块的内部具体实现。一旦代码里面涉及具体实现类,就违反了开闭原则。如果需要替换一种实现,就需要修改代码。
  为了实现在模块装配的时候不用在程序里面动态指明,这就需要一种服务发现机制。Java SPI 就是提供了这样一个机制:为某个接口寻找服务实现的机制。这有点类似 IOC 的思想,将装配的控制权移交到了程序之外。
  SPI   英文为 Service Provider Interface   字面意思就是:"服务提供者的接口",我的理解是:专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。
  SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。 2. 使用场景
  很多框架都使用了 Java 的 SPI 机制,比如:数据库加载驱动,日志接口,以及 dubbo 的扩展实现等等。 3. SPI 和 API 有啥区别
  说到 SPI 就不得不说一下 API 了,从广义上来说它们都属于接口,而且很容易混淆。下面先用一张图说明一下:
  一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个"接口"。
  当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
  当接口存在于调用方这边时,就是 SPI ,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务,举个通俗易懂的例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)。
  实战演示
  Spring框架提供的日志服务 SLF4J 其实只是一个日志门面(接口),但是 SLF4J 的具体实现可以有几种,比如:Logback、Log4j、Log4j2 等等,而且还可以切换,在切换日志具体实现的时候我们是不需要更改项目代码的,只需要在 Maven 依赖里面修改一些 pom 依赖就好了。
  这就是依赖 SPI 机制实现的,那我们接下来就实现一个简易版本的日志框架。 1. Service Provider Interface
  新建一个 Java 项目  service-provider-interface   目录结构如下:├─.idea └─src     ├─META-INF     └─org         └─spi              └─service                 ├─Logger.java                 ├─LoggerService.java                 ├─Main.java                 └─MyServicesLoader.java
  新建 Logger 接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现。 package org.spi.service;  public interface Logger {     void info(String msg);      void debug(String msg); }
  接下来就是 LoggerService 类,这个主要是为服务使用者(调用方)提供特定功能的。如果存在疑惑的话可以先往后面继续看。 package org.spi.service;  import java.util.ArrayList; import java.util.List; import java.util.ServiceLoader;  public class LoggerService {     private static final LoggerService SERVICE = new LoggerService();      private final Logger logger;      private final List loggerList;      private LoggerService() {         ServiceLoader loader = ServiceLoader.load(Logger.class);         List list = new ArrayList<>();         for (Logger log : loader) {             list.add(log);         }         // LoggerList 是所有 ServiceProvider         loggerList = list;         if (!list.isEmpty()) {             // Logger 只取一个             logger = list.get(0);         } else {             logger = null;         }     }      public static LoggerService getService() {         return SERVICE;     }      public void info(String msg) {         if (logger == null) {             System.out.println("info 中没有发现 Logger 服务提供者");         } else {             logger.info(msg);         }     }      public void debug(String msg) {         if (loggerList.isEmpty()) {             System.out.println("debug 中没有发现 Logger 服务提供者");         }         loggerList.forEach(log -> log.debug(msg));     } }
  新建 Main 类(服务使用者,调用方),启动程序查看结果。 package org.spi.service;  public class Main {     public static void main(String[] args) {         LoggerService service = LoggerService.getService();          service.info("Hello SPI");         service.debug("Hello SPI");     } }
  程序结果:
  info 中没有发现 Logger 服务提供者
  debug 中没有发现 Logger 服务提供者
  将整个程序直接打包成 jar 包,可以直接通过 IDEA 将项目打包成一个 jar 包。
  2. Service Provider
  接下来新建一个项目用来实现 Logger 接口
  新建项目  service-provider   目录结构如下:├─.idea ├─lib │   └─service-provider-interface.jar └─src     ├─META-INF     │   └─services     │       └─org.spi.service.Logger     └─org         └─spi             └─provider                  └─Logback.java
  新建 Logback 类 package org.spi.provider;  import org.spi.service.Logger;  public class Logback implements Logger {      @Override     public void info(String msg) {         System.out.println("Logback info 的输出:" + msg);     }      @Override     public void debug(String msg) {         System.out.println("Logback debug 的输出:" + msg);     } }
  将  service-provider-interface   的 jar 导入项目中。新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中。
  再点击 OK 。
  接下来就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的。
  实现 Logger 接口,在 src 目录下新建  META-INF/services   文件夹,然后新建文件 org.spi.service.Logger   (SPI 的全类名),文件里面的内容是:org.spi.provider.Logback   (Logback 的全类名,即 SPI 的实现类的包名 + 类名)。
  这是 JDK SPI 机制 ServiceLoader 约定好的标准
  接下来同样将  service-provider   项目打包成 jar 包,这个 jar 包就是服务提供方的实现。通常我们导入 maven 的 pom 依赖就有点类似这种,只不过我们现在没有将这个 jar 包发布到 maven 公共仓库中,所以在需要使用的地方只能手动的添加到项目中。3. 效果展示
  接下来再回到  service-provider-interface   项目。
  导入  service-provider   jar 包,重新运行 Main 方法。运行结果如下:
  Logback info 的输出:Hello SPI
  Logback debug 的输出:Hello SPI
  说明导入 jar 包中的实现类生效了。
  通过使用 SPI 机制,可以看出 服务(LoggerService)和 服务提供者两者之间的耦合度非常低,如果需要替换一种实现(将 Logback 换成另外一种实现),只需要换一个 jar 包即可。这不就是 SLF4J 原理吗?
  如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个 服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的  服务实现  (service-provider) 来完成我们需要的操作。
  loggerList.forEach(log -> log.debug(msg));
  或者
  loggerList.get(1).debug(msg);
  loggerList.get(2).debug(msg);
  这里需要先理解一点:ServiceLoader 在加载具体的 服务实现 的时候会去扫描所有包下 src 目录的  META-INF/services   的内容,然后通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式得到你需要的那个 服务实现。
  3. ServiceLoader
  想要使用 Java 的 SPI 机制是需要依赖 ServiceLoader 来实现的,那么我们接下来看看 ServiceLoader 具体是怎么做的:
  ServiceLoader 是 JDK 提供的一个工具类, 位于 package java.util;  包下。A facility to load implementations of a service.
  这是 JDK 官方给的注释:一种加载服务实现的工具。
  再往下看,我们发现这个类是一个 final 类型的,所以是不可被继承修改,同时它实现了 Iterable 接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现。 public final class ServiceLoader implements Iterable{ xxx...}
  可以看到一个熟悉的常量定义:
  private static final String PREFIX = "META-INF/services/";
  下面是 load 方法:可以发现 load 方法支持两种重载后的入参; public static  ServiceLoader load(Class service) {     ClassLoader cl = Thread.currentThread().getContextClassLoader();     return ServiceLoader.load(service, cl); }  public static  ServiceLoader load(Class service,                                         ClassLoader loader) {     return new ServiceLoader<>(service, loader); }  private ServiceLoader(Class svc, ClassLoader cl) {     service = Objects.requireNonNull(svc, "Service interface cannot be null");     loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;     acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;     reload(); }  public void reload() {     providers.clear();     lookupIterator = new LazyIterator(service, loader); }
  根据代码的调用顺序,在 reload() 方法中是通过一个内部类 LazyIterator 实现的。先继续往下面看。
  ServiceLoader 实现了 Iterable 接口的方法后,具有了迭代的能力,在这个 iterator 方法被调用时,首先会在 ServiceLoader 的 Provider 缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator 中进行查找。  public Iterator iterator() {     return new Iterator() {          Iterator> knownProviders                 = providers.entrySet().iterator();          public boolean hasNext() {             if (knownProviders.hasNext())                 return true;             return lookupIterator.hasNext(); // 调用 LazyIterator          }          public S next() {             if (knownProviders.hasNext())                 return knownProviders.next().getValue();             return lookupIterator.next(); // 调用 LazyIterator          }          public void remove() {             throw new UnsupportedOperationException();         }      }; }
  在调用 LazyIterator 时,具体实现如下:  public boolean hasNext() {     if (acc == null) {         return hasNextService();     } else {         PrivilegedAction action = new PrivilegedAction() {             public Boolean run() {                 return hasNextService();             }         };         return AccessController.doPrivileged(action, acc);     } }  private boolean hasNextService() {     if (nextName != null) {         return true;     }     if (configs == null) {         try {             //通过PREFIX(META-INF/services/)和类名 获取对应的配置文件,得到具体的实现类             String fullName = PREFIX + service.getName();             if (loader == null)                 configs = ClassLoader.getSystemResources(fullName);             else                 configs = loader.getResources(fullName);         } catch (IOException x) {             fail(service, "Error locating configuration files", x);         }     }     while ((pending == null) || !pending.hasNext()) {         if (!configs.hasMoreElements()) {             return false;         }         pending = parse(service, configs.nextElement());     }     nextName = pending.next();     return true; }   public S next() {     if (acc == null) {         return nextService();     } else {         PrivilegedAction action = new PrivilegedAction() {             public S run() {                 return nextService();             }         };         return AccessController.doPrivileged(action, acc);     } }  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 }
  4. 总结
  其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在  META-INF/services/   文件下声明。
  其实 SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的反射。还有 dubbo 框架提供同样的 SPI 扩展机制。
  通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如: 遍历加载所有的实现类,这样效率还是相对较低的; 当多个 ServiceLoader 同时 load 时,会有并发问题。
  写在最后
  Freemen App是一款专注于IT程序员求职招聘的一个求职平台,旨在帮助IT技术工作者能更好更快入职及努力协调IT技术者工作和生活的关系,让工作更自由!
  本文转载自江璇Up

有人说叫老公已经过时了,现在最流行的称呼方式是什么?2016年,清华大学历史系教授彭林说喊丈夫老公,这一叫法是十分不对的,因为老公在古代指的是太监的身份。而实际上,老公一词,只在清朝指太监,早在宋元时期,已经是对丈夫的普遍称呼。近代2022年最稳的理财方式是什么?今年估计又是震荡行情,总结去年也是震荡市,坚持定投一年下来,反复做过山车,也没怎么赚钱,而且体验很差。今年一定改变去年的操作方法,一部分长期的做定投,选好标的,估值合理的,有10左家用吸尘器哪个品牌的好?我用的是飞利浦吸尘器,很好用前言前不久钣金装修好的新家,每天最烦恼的就是打扫卫生做家务。尤其是扫地来说,家里之前的那个扫把每次用完都感觉手心腾,而且像一些灰尘宝宝吃的饼干碎屑这些小要是你的头胎是女儿,当第二胎第一时间知道又是个女儿的时候,你会有什么想法?我两个女儿。头胎知道是女儿没啥感觉。准备生二宝的时候,去找个中医调了几个月,也是想要儿女双全才吃的药,老公一天吃200到300个类似六味地黄的小药丸,我吃200个左右,刚开始吃一个如果山河令和陈情令的播出时间调换一下,山河令会更火吗?不会,山河令拍的不美,赤裸裸的,没什么文化底蕴,陈情令承担着中国文化的输出,里面的画面,服饰,场景等其古文化体现的淋漓尽致,弘扬锄奸扶弱的正能量。不会超过陈情令,论cp感虽然山河令从来不化妆与长期化妆的肌肤有什么区别?关注糖小姐的奢侈品,相信我,这会是一群爱装逼爱奢侈品的艺术家们的最佳聚集地。谢邀!现在大多数女生每天出门都是要化了个美美的妆才出门的,也有的女生是不会化妆,所以总是素着。那不化妆的有什么办法可以提亮肤色吗?如何提亮肤色?嗯,这个百度了一下。皮肤暗淡无光泽,不仅令整个人看起来毫无精神,对自己的心情也是一种影响。俗话说世界上只有懒女人,没有丑女人,而肤色,虽然是天生的,但也不是不可改善的助听器双耳互通什么意思?将双耳助听器作为一个整体,协调处理单耳接收到的声音信息,从而达到精确定位提高言语清晰度等。双耳互通,双耳听觉所接受的声音响度要比单耳高一些,在阈值水平,双耳阈值好于单耳阈值约3分贝听力需要保健吗?你好,听力是需要保健的,平时少去噪音特别大的场所,也不要长时间佩戴耳机听音乐,以防引起噪音性耳聋,另外还要保持耳部清洁,耳朵不要进水,以防感染发炎。平时可以做做耳保健操等。希望我的油耳朵能戴助听器吗?你好!油耳是湿型耵聍,如果油耳听力在适配范围之内可以佩戴助听器,可以选配耳背式助听器,注意清理耳道和助听器耳塞即可。谢谢邀请,你说的右耳朵是耳朵里边的油性大吗?如果是的话,可以在经为什么河北保定,有20多个县城?在国内来说重庆市下面有40来个区县,保定只能排第二。但是在地级市来说保定那绝对是第一,石家庄排第二。但是这里说明一下,要是保定把雄安去掉,石家庄是第一。其次北京,天津,上海,哈尔滨
作家在线张祖强美丽的相知张祖强想起你,想起远方的你,想起俊逸的你,泪水润湿了眼底,思绪打湿了稿纸。与你相识,在丹桂花开的日子。望着身影的俏立,猜想眼前的你,是否是久违的知己?天空澄碧如洗,微风吹来秋思,阳借口三八节,拦路要钱,小心坏了甘孜的名声,砸了甘孜旅游的饭碗男子4分钟3次遭村民拦车要钱官方介入借口三八节,拦路要钱,小心坏了甘孜的名声,砸了甘孜旅游的饭碗。有视频显示,在3月8日妇女节当天的四川甘孜,有人驾车在路上经过一个村庄时,被一群妇别再当冤大头了,手把手教你降低手机套餐费用!套餐太贵?流量不够用?没有适合的套餐?自从4G5G普及以来,大家有没有感觉流量越来越不够用,越来越贵?尤其是信了客服的忽悠,升级套餐后发现再也降不下来的移动用户。目前39元套餐只有折叠屏手机专题报告出货量逆市增长,增量组件迎新机遇(报告出品方平安证券)一智能手机出货疲软,折叠屏手机逆市突围1。1从大屏化到全面屏,行业创新面临瓶颈大屏化是智能手机的重要发展轴心之一。2007年第一台iPhone的面市导入了触摸电信,移动,联通,线上注销全流程教程,轻松异地注销手机卡1。电信手机卡注销流程使用需要销户的手机号登录电信掌上营业厅,点击查询办理进入业务列表后,点击左侧办理,然后选择单号卡拆机进入拆机业务界面后,确认手机号码正确,点击立即办理2。联通如何让可恨的手机广告全都消失?寻找数码点评派在众多搞机老手艺中,最经久不衰的那个,机哥敢肯定是去广告。明明现在手机性能越来越强。但在无孔不入的广告面前,管你是8Gen2还是A16。都得乖乖给我看上5秒钟。老粉都春之曲闭上眼睛,似乎满园绿油油的土豆秧在随风摇曳种土豆(散文)席波雨水节之后,仍是春寒料峭,但闲不住的庄稼人却已经开始拾掇地了。看着菜地里忙活的人一天比一天多了,80岁的老父亲也有点沉不住气了,一大早就把手推车推到院外墙角的粪堆一把芹菜,一根沙拉肠,简单炒一炒,色泽鲜亮,看着有食欲,好吃大家好,我是舒晨,每天在这里分享各种家常美食,今天给大家分享清淡爽口的芹菜炒沙拉肠。芹菜一般指的是旱芹,旱芹是常用蔬菜之一,旱芹味甘苦,性凉,有降压平肝,利尿消肿,养血补虚的功效。一斤肉能包八斤肉丸饺子,饺子馆是怎么做到的?原来如此简单一斤肉能包八斤饺子,个个都是一包肉丸,饺子馆是怎么做到的?原来如此简单,请收藏。坑人的饺子,宰人的面不是说商家无良,而是说饺子和面条的利润非常大,有一个资深包饺子师傅告诉我,一斤肉春天除湿很重要!推荐几款好吃的祛湿菜,营养好,润燥更给力春天除湿很重要!推荐几款好吃的祛湿菜,营养好,润燥更给力除湿菜是一类能够有效降湿的菜肴,具有去湿化痰养阴润燥的功效,是一道特别适合梅雨季节食用的菜肴。以下是几款经典的除湿菜,附带详这菜全身是宝,很多人只吃茎,却把菜头当垃圾丢了,太可惜了35月是蒜苔大量上市的季节!当季的新蒜薹,又嫩又脆,而且价格还相当的便宜,蒜苔被称为天然青霉素,含有独特的辣素和大蒜素,具有杀菌消炎抗癌预防感冒等多种功效。很适合现在吃,为身体杀菌