SPI机制 | 总字数: 3.5k | 阅读时长: 14分钟 | 浏览量: | 
什么是 SPI 机制? 
在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
SPI (Service Provider Interface)是专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口,SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。
API 与 SPI 区别 
区别 
API 
SPI 
 
 
存在位置 
API 依赖的接口位于实现者的包中 
SPI 依赖的接口在调用方的包中 
 
针对对象 
API 通常是面向最终用户或外部系统的,提供了可直接使用的功能 
SPI 更多是面向系统开发者,为他们提供一种将新服务或插件加入系统的方式 
 
目的 
API 的主要目的是提供接口供外界访问和使用特定的功能或数据 
SPI 则是为了提供一个标准,允许第三方开发者实现并插入新的服务 
 
定义方式 
API 是由开发者主动编写并公开给其他开发者使用的 
SPI 是由框架或库提供方定义的接口,供第三方开发者实现 
 
调用方式 
API 是通过直接调用接口的方法来使用功能 
SPI 是通过配置文件来指定具体的实现类,然后由框架或库自动加载和调用 
 
灵活性 
API 的实现类必须在编译时就确定,无法动态替换 
SPI 的实现类可以在运行时根据配置文件的内容进行动态加载和替换 
 
依赖关系 
API 是被调用方依赖的,即应用程序需要引入 API 所在的库才能使用其功能 
SPI 是调用方依赖的,即框架或库需要引入第三方实现类的库才能加载和调用 
 
 
SPI 优缺点 
优点 
使用 Java SPI 机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点 
不能按需加载,需要遍历所有的实现类并实例化,然后在循环中才能找到我们需要的实现类。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。 
获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。(Spring 的 BeanFactory,ApplicationContext 就要高级一些了。) 
多个并发多线程使用 ServiceLoader 类的实例是不安全的。 
 
SPI 使用场景 
数据库驱动加载接口实现类的加载:JDBC 加载不同类型数据库的驱动 
日志门面接口实现类加载:SLF4J 加载不同提供商的日志实现类 
Spring 中大量使用了 SPI:对 servlet3.0 规范对 ServletContainerInitializer 的实现、自动类型转换 Type Conversion SPI(Converter SPI、Formatter SPI)等 
Dubbo:Dubbo 中也大量使用 SPI 的方式实现框架的扩展, 不过它对 Java 提供的原生 SPI 做了封装,允许用户扩展实现 Filter 接口 
 
SPI 基本使用 
使用方法 
当服务提供者提供了接口的一种具体实现后,在 jar 包的 META-INF/services 目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名; 
接口实现按行配置,可以是多个。如果包含#号,每一行只取第一个#号前的内容。 
接口实现类所在的 jar 包放在主程序的 classpath 中; 
主程序通过 java.util.ServiceLoder 动态装载实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM; 
 
使用案例 
包结构 
 
接口和方法定义 
 
1 2 3 4 public  interface  IShout  {    void  shout () ; } 
 
1 2 3 4 5 6 7 public  class  Cat  implements  IShout  {    @Override      public  void  shout ()  {         System.out.println("miao miao" );     } } 
 
1 2 3 4 5 6 7 public  class  Dog  implements  IShout  {    @Override      public  void  shout ()  {         System.out.println("wang wang" );     } } 
 
在项目 META-INF/services 创建一个文本文件:名称为接口的“全限定名”,内容为实现类的全限定名 
 
com.example.spi.demos.service.IShout 
1 2 com.example.spi.demos.service.impl.Cat com.example.spi.demos.service.impl.Dog 
 
测试 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 public  class  Test  {    public  static  void  main (String[] args)  {         ServiceLoader<IShout> shouts = ServiceLoader.load(IShout.class);         for  (IShout s : shouts) {             s.shout();         }     } } miao miao wang wang 
 
ServiceLoader 分析 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public  final  class  ServiceLoader <S> implements  Iterable <S> {         private  static  final  String  PREFIX  =  "META-INF/services/" ; 	     private  final  Class<S> service; 	     private  final  ClassLoader loader; 	     private  final  AccessControlContext acc; 	     private  LinkedHashMap<String,S> providers = new  LinkedHashMap <>(); 	     private  LazyIterator lookupIterator;                	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)  {        return  new  ServiceLoader <>(service, loader);     }     private  ServiceLoader (Class<S> 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);     }           } 
 
应用程序调用 ServiceLoader.load 方法 
 
ServiceLoader.load(Class < S > service) 方法内先创建一个新的 ServiceLoader,并实例化该类中的成员变量,包括:
loader(ClassLoader 类型,类加载器) 
acc(AccessControlContext 类型,访问控制器) 
providers(LinkedHashMap <String,S> 类型,用于缓存加载成功的类) 
lookupIterator(实现迭代器功能) 
 
ServiceLoader 的构造方法内会调用 reload 方法,来清理缓存,初始化 LazyIterator,注意此处是 Lazy,也就懒加载,此时并不会去加载文件下的内容
 
当遍历器被遍历时,才会去读取配置文件,应用程序通过迭代器接口获取对象实例
 
 
ServiceLoader 先判断成员变量 providers 对象中(LinkedHashMap <String,S> 类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public  Iterator<S> iterator ()  {    return  new  Iterator <S>() {         Iterator<Map.Entry<String, S>> knownProviders = providers.entrySet().iterator();         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 ();         }     }; } 
 
读取 META-INF/services 下的配置文件,获得所有能被实例化的类的名称,ServiceLoader 可以跨越 jar 包获取 META-INF 下的配置文件 
 
核心代码如下(即 ServiceLoader 扫描了所有 jar 包下的配置文件。然后通过解析全限定名获得,并在遍历时通过 Class.forName 进行实例化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 private  class  LazyIterator  implements  Iterator <S> {    Class<S> service;     ClassLoader loader;     Enumeration<URL> configs = null ;     Iterator<String> pending = null ;     String  nextName  =  null ;     private  LazyIterator (Class<S> service, ClassLoader loader)  {         this .service = service;         this .loader = loader;     }     private  boolean  hasNextService ()  {         if  (nextName != null ) {             return  true ;         }         if  (configs == null ) {             try  {                                  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 ;     }     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 ();               }     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);         }     }     public  S next ()  {         if  (acc == null ) {             return  nextService();         } else  {             PrivilegedAction<S> action = new  PrivilegedAction <S>() {                 public  S run ()  { return  nextService(); }             };             return  AccessController.doPrivileged(action, acc);         }     }     public  void  remove ()  {         throw  new  UnsupportedOperationException ();     } } 
 
自定义 ServiceLoader 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 public  class  MyServiceLoader <S> {         private  final  Class<S> service;          private  final  List<S> providers = new  ArrayList <>();          private  final  ClassLoader classLoader;          public  static  <S> MyServiceLoader<S> load (Class<S> service)  {         return  new  MyServiceLoader <>(service);     }          private  MyServiceLoader (Class<S> service)  {         this .service = service;         this .classLoader = Thread.currentThread().getContextClassLoader();         doLoad();     }          private  void  doLoad ()  {         try  {                          Enumeration<URL> urls = classLoader.getResources("META-INF/services/"  + service.getName());                          while  (urls.hasMoreElements()) {                                  URL  url  =  urls.nextElement();                 System.out.println("File = "  + url.getPath());                                  URLConnection  urlConnection  =  url.openConnection();                 urlConnection.setUseCaches(false );                                  InputStream  inputStream  =  urlConnection.getInputStream();                                  BufferedReader  bufferedReader  =  new  BufferedReader (new  InputStreamReader (inputStream));                                  String  className  =  bufferedReader.readLine();                 while  (className != null ) {                                          Class<?> clazz = Class.forName(className, false , classLoader);                                          if  (service.isAssignableFrom(clazz)) {                         Constructor<? extends  S > constructor = (Constructor<? extends  S >) clazz.getConstructor();                         S  instance  =  constructor.newInstance();                                                  providers.add(instance);                     }                                          className = bufferedReader.readLine();                 }             }         } catch  (Exception e) {             System.out.println("读取文件异常。。。" );         }     }          public  List<S> getProviders ()  {         return  providers;     } } 
 
通过 URL 工具类从 jar 包的 /META-INF/services 目录下面找到对应的文件, 
读取这个文件的名称找到对应的 spi 接口, 
通过 InputStream 流将文件里面的具体实现类的全类名读取出来, 
根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象, 
将构造出来的实例对象添加到 Providers 的列表中。 
 
1 2 3 4 5 6 public  static  void  main (String[] args)  {    MyServiceLoader<IShout> shouts = MyServiceLoader.load(IShout.class);     for  (IShout s : shouts.getProviders()) {         s.shout();     } } 
 
SpringBoot 使用 SPI 
使用方法 
Spring Boot 有一个与 SPI 相似的机制,但它并不完全等同于 Java 标准 SPI。
Spring Boot 的自动配置机制主要依赖于 spring.factories 文件。这个文件可以在多个 jar 中存在,并且 Spring Boot 会加载所有可见的 spring.factories 文件。我们可以在这个文件中声明一系列的自动配置类,这样当满足某些条件时,这些配置类会自动被 Spring Boot 应用。
spring.factories 文件中的条目键和值之间不能有换行,即 key = value 形式的结构必须在同一行开始。但是,如果有多个值需要列出(如多个实现类),并且这些值是逗号分隔的,那么可以使用反斜杠(\)来换行。spring.factories 的名称是约定俗成的。如果试图使用一个不同的文件名,那么 Spring Boot 的自动配置机制将不会识别它。
使用案例 
包结构 
 
接口和方法定义 
 
1 2 3 public  interface  MessageService  {    String getMessage () ; } 
 
1 2 3 4 5 6 7 public  class  HelloMessageService  implements  MessageService  {    @Override      public  String getMessage ()  {         return  "Hello from HelloMessageService!" ;     } } 
 
1 2 3 4 5 6 7 public  class  HiMessageService  implements  MessageService  {    @Override      public  String getMessage ()  {         return  "Hi from HiMessageService!" ;     } } 
 
在项目 META-INF 创建一个 spring.factories 文件 
 
1 2 com.example.spi.demos.service.MessageService =com.example.spi.demos.service.impl.HelloMessageService,\   com.example.spi.demos.service.impl.HiMessageService 
 
测试 
 
1 2 3 4 5 6 7 8 9 10 11 12 13 public  class  Test  {    public  static  void  main (String[] args)  {         List<MessageService> services = SpringFactoriesLoader.loadFactories(MessageService.class, null );         for  (MessageService service : services) {             System.out.println(service.getMessage());         }     } } Hello from HelloMessageService! Hi from HiMessageService! 
 
SpringFactoriesLoader 分析 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public  final  class  SpringFactoriesLoader  {         public  static  final  String  FACTORIES_RESOURCE_LOCATION  =  "META-INF/spring.factories" ;     private  static  final  Log  logger  =  LogFactory.getLog(SpringFactoriesLoader.class);     private  static  final  Map<ClassLoader, MultiValueMap<String, String>> cache = new  ConcurrentReferenceHashMap <>();     private  static  Map<String, List<String>> loadSpringFactories (@Nullable  ClassLoader classLoader)  {         MultiValueMap<String, String> result = cache.get(classLoader);         if  (result != null ) {             return  result;         }         try  {                          Enumeration<URL> urls = (classLoader != null  ?                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));             result = new  LinkedMultiValueMap <>();                          while  (urls.hasMoreElements()) {                 URL  url  =  urls.nextElement();                 UrlResource  resource  =  new  UrlResource (url);                 Properties  properties  =  PropertiesLoaderUtils.loadProperties(resource);                                  for  (Map.Entry<?, ?> entry : properties.entrySet()) {                 String  factoryClassName  =  ((String) entry.getKey()).trim();                                  for  (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {                         result.add(factoryClassName, factoryName.trim());                 	}                 }             }                          cache.put(classLoader, result);             return  result;         } catch  (IOException ex) {             throw  new  IllegalArgumentException ("Unable to load factories from location ["  +                     FACTORIES_RESOURCE_LOCATION + "]" , ex);         }     } }