たとえば、新しいロギング フレームワーク 「スーパーロガー」 を設計しました。デフォルトでは、XML ファイルがログの構成ファイルとして使用されます と、構成ファイル解析用のインターフェースが設計されています:
package com.github.kongwu.spisamples; public interface SuperLoggerConfiguration { void configure(String configFile); }
次に、デフォルトの XML 実装:
package com.github.kongwu.spisamples; public class XMLConfiguration implements SuperLoggerConfiguration{ public void configure(String configFile){ ...... } }
次に、構成を初期化して解析するときに、この XMLConfiguration を呼び出して XML 構成ファイルを解析するだけで済みます。
package com.github.kongwu.spisamples; public class LoggerFactory { static { SuperLoggerConfiguration configuration = new XMLConfiguration(); configuration.configure(configFile); } public static getLogger(Class clazz){ ...... } }
このようにして、基本モデルが完成します。問題ありません。ただし、解析機能をカスタマイズ・拡張・書き換えたい場合、エントリコードの再定義やLoggerFactoryの書き換えが必要となるため、拡張性があまり良くなく、柔軟性が低く煩雑です。
たとえば、ユーザーが yml ファイルをログ構成ファイルとして追加したい場合、必要なのは新しい YAMLConfiguration を作成して SuperLoggerConfiguration を実装することだけです。しかし...それを注入する方法、LoggerFactory で新しく作成された YAMLConfiguration を使用する方法は? LoggerFactoryまで書き換えられている可能性はありますか?
SPI 機構を使用すると、この問題は非常に単純になり、このエントリの拡張機能は簡単に完了できます。
まず、JDK の SPI メカニズムを使用して上記のスケーラビリティの問題を解決する方法を見てみましょう。
JDK は SPI 関数を提供し、コア クラスは java.util.ServiceLoader です。その機能は、「META-INF/services/」配下にある複数の設定実装ファイルをクラス名を通じて取得することです。
上記の拡張問題を解決するために、META-INF/services/
に com.github.kongwu.spisamples.SuperLoggerConfiguration
ファイルを作成します (いいえ接尾辞)。ファイル内のコードは 1 行のみで、これがデフォルトの com.github.kongwu.spisamples.XMLConfiguration
です (改行で区切って 1 つのファイルに複数の実装を記述することもできることに注意してください)
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration: com.github.kongwu.spisamples.XMLConfiguration
次に、ServiceLoader を通じて SPI メカニズム構成の実装クラスを取得します。
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator(); SuperLoggerConfiguration configuration; while(iterator.hasNext()) { //加载并初始化实现类 configuration = iterator.next(); } //对最后一个configuration类调用configure方法 configuration.configure(configFile);
最後に、LoggerFactory の初期化構成メソッドを現在の SPI メソッドに調整します。
package com.github.kongwu.spisamples; public class LoggerFactory { static { ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class); Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator(); SuperLoggerConfiguration configuration; while(iterator.hasNext()) { configuration = iterator.next();//加载并初始化实现类 } configuration.configure(configFile); } public static getLogger(Class clazz){ ...... } }
「待て、なぜここでイテレーターが使われているの? インスタンスを 1 つだけ取得する get のようなメソッドではなく?」
想像してみてください。 fix get メソッドを実行すると、取得されるのは固定インスタンスですが、SPI の意味は何ですか?
SPI の目的は、スケーラビリティを強化することです。固定構成を抽出し、SPI メカニズムを通じて構成します。その場合、通常はデフォルトの設定があり、その後 SPI ファイルを通じてさまざまな実装が設定されるため、1 つのインターフェイスに複数の実装があるという問題が発生します。複数の実装が見つかった場合、どの実装が最終インスタンスとして使用されますか?
したがって、ここではイテレータを使用してすべての実装クラス構成を取得します。デフォルトの SuperLoggerConfiguration 実装が "super-logger" パッケージに追加されました。
YAML 構成をサポートするには、ユーザー/ユーザー コードに YAMLConfiguration SPI 構成を追加します。
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration: com.github.kongwu.spisamples.ext.YAMLConfiguration
この時点では、イテレータを通じて取得されます。メソッド 2 つの構成実装クラスがあります。デフォルトの XMLConfiguration と、拡張した YAMLConfiguration です。
上記のロードされたコードでは、反復子を走査し、最後まで最後の実装構成を最終インスタンスとして使用します。
「ちょっと待ってください? 最後のものですか? 最後のものをどうやって数えますか?」
ユーザー/ユーザー定義の YAMLConfiguration は必ず最後のものですか?
これは実際には必ずしも当てはまりません。実行時の ClassPath 構成によって異なります。前にロードされた jar は当然前にあり、最後の jar の jar は当然後ろにあります。したがって、 "ユーザーのパッケージがクラスパス内でスーパーロガー パッケージよりも後ろにある場合、それは最後の位置になります。ユーザーのパッケージが前にある場合、いわゆる最後のパッケージが依然としてデフォルトの XMLConfiguration です。 》
たとえば、プログラムの起動スクリプトが次の場合:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main
デフォルトの XMLConfiguration SPI 構成は super-logger にあります。 jar
、拡張 YAMLConfiguration SPI 構成ファイルは main.jar
にあり、イテレータによって取得される最後の要素は YAMLConfiguration である必要があります。
しかし、クラスパスの順序が逆だったらどうなるでしょうか? main.jar が前部、super-logger.jar が後部
java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main
このようにして、反復子によって取得された最後の要素がデフォルトの XMLConfiguration になります。JDK SPI を使用するのは意味がありません。取得されたものは最初のものですか、それともデフォルトの XMLConfiguration ですか。
ロード順序 (クラスパス) はユーザーによって指定されるため、最初にロードするか最後にロードするかに関係なく、ユーザー定義の構成がロードされない可能性があります。
"つまり、これは JDK SPI メカニズムの欠点でもあります。どの実装がロードされているかを確認することは不可能であり、指定された実装をロードすることも不可能です。ClassPath の順序のみに依存する非常に不正確な方法です。」
Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。
Dubbo 中实现了一套新的 SPI 机制,功能更强大,也更复杂一些。相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下,配置内容如下(以下demo来自dubbo官方文档)。
optimusPrime = org.apache.spi.OptimusPrime bumblebee = org.apache.spi.Bumblebee
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。另外在使用时还需要在接口上标注 @SPI 注解。
下面来演示 Dubbo SPI 的用法:
@SPI public interface Robot { void sayHello(); } public class OptimusPrime implements Robot { @Override public void sayHello(){ System.out.println("Hello, I am Optimus Prime."); } } public class Bumblebee implements Robot { @Override public void sayHello(){ System.out.println("Hello, I am Bumblebee."); } } public class DubboSPITest { @Test public void sayHello() throws Exception { ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class); Robot optimusPrime = extensionLoader.getExtension("optimusPrime"); optimusPrime.sayHello(); Robot bumblebee = extensionLoader.getExtension("bumblebee"); bumblebee.sayHello(); } }
「Dubbo SPI 和 JDK SPI 最大的区别就在于支持“别名”」,可以通过某个扩展点的别名来获取固定的扩展点。就像上面的例子中,我可以获取 Robot 多个 SPI 实现中别名为“optimusPrime”的实现,也可以获取别名为“bumblebee”的实现,这个功能非常有用!
通过 @SPI 注解的 value 属性,还可以默认一个“别名”的实现。比如在Dubbo 中,默认的是Dubbo 私有协议:「dubbo protocol - dubbo://」**
来看看Dubbo中协议的接口:
@SPI("dubbo") public interface Protocol { ...... }
在 Protocol 接口上,增加了一个 @SPI 注解,而注解的 value 值为 Dubbo ,通过 SPI 获取实现时就会获取 Protocol SPI 配置中别名为dubbo的那个实现,com.alibaba.dubbo.rpc.Protocol
文件如下:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper mock=com.alibaba.dubbo.rpc.support.MockProtocol dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol com.alibaba.dubbo.rpc.protocol.http.HttpProtocol com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol registry=com.alibaba.dubbo.registry.integration.RegistryProtocol qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
然后只需要通过getDefaultExtension,就可以获取到 @SPI 注解上value对应的那个扩展实现了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension(); //protocol: DubboProtocol
还有一个 Adaptive 的机制,虽然非常灵活,但……用法并不是很“优雅”,这里就不介绍了
Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,然后加载外部的(external),按优先级顺序加载,「如果遇到重复就跳过不会加载」了。
所以如果想靠classpath加载顺序去覆盖内置的扩展,也是个不太理智的做法,原因同上 - 加载顺序不严谨
Spring 的 SPI 配置文件是一个固定的文件 - META-INF/spring.factories
,功能上和 JDK 的类似,每个接口可以有多个扩展实现,使用起来非常简单:
//获取所有factories文件中配置的LoggingSystemFactory List<LoggingSystemFactory>> factories = SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
下面是一段 Spring Boot 中 spring.factories 的配置
# Logging Systems org.springframework.boot.logging.LoggingSystemFactory=\ org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\ org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\ org.springframework.boot.logging.java.JavaLoggingSystem.Factory # PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader # ConfigData Location Resolvers org.springframework.boot.context.config.ConfigDataLocationResolver=\ org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\ org.springframework.boot.context.config.StandardConfigDataLocationResolver ......
Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩展配置,是用一个文件好,还是每个单独一个文件好这个,这个问题就见仁见智了(个人喜欢 Spring 这种,干净利落)。
Spring的SPI 虽然属于spring-framework(core),但是目前主要用在spring boot中……
和前面两种 SPI 机制一样,Spring 也是支持 ClassPath 中存在多个 spring.factories 文件的,加载时会按照 classpath 的顺序依次加载这些 spring.factories 文件,添加到一个 ArrayList 中。由于没有别名,所以也没有去重的概念,有多少就添加多少。
但由于 Spring 的 SPI 主要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载项目中的文件,而不是依赖包中的文件。所以如果在你的项目中定义个spring.factories文件,那么你项目中的文件会被第一个加载,得到的Factories中,项目中spring.factories里配置的那个实现类也会排在第一个
如果我们要扩展某个接口的话,只需要在你的项目(spring boot)里新建一个META-INF/spring.factories
文件,「只添加你要的那个配置,不要完整的复制一遍 Spring Boot 的 spring.factories 文件然后修改」**
比如我只想添加一个新的 LoggingSystemFactory 实现,那么我只需要新建一个META-INF/spring.factories
文件,而不是完整的复制+修改:
org.springframework.boot.logging.LoggingSystemFactory=\ com.example.log4j2demo.Log4J2LoggingSystem.Factory
JDK SPI
DUBBO SPI
Spring SPI
文件方式 |
每个扩展点单独一个文件 |
每个扩展点单独一个文件 |
所有的扩展点在一个文件 |
获取某个固定的实现 |
サポートされていません。すべての実装を順番に取得することしかできません。 |
「エイリアス」の概念があり、拡張機能の固定実装を取得できます。名前ごとにポイントします。Dubbo SPI のアノテーションを使用すると非常に便利です。 |
はサポートされていないため、すべての実装を順番に取得することしかできません。ただし、Spring Boot ClassLoader はユーザー コード内のファイルのロードを優先するため、ユーザー定義の spring.factoires ファイルが最初になるようにすることができ、最初の Factory # を取得することでカスタム拡張子を固定的に取得できます。 |
なし | ディレクトリを介した Dubbo 内での依存関係注入をサポートします。 Dubbo の組み込みを区別します。 SPI と外部 SPI、最初に内部 SPI をロードし、内部 SPI が最も高い優先順位を持つようにします | #None | |
## 記事とサードパーティの情報が十分に充実している | ドキュメントとサードパーティの情報が十分に充実している | ドキュメントは十分に豊富ではありませんが、機能が少ないため、使用は非常に簡単です。 | |
#なし |
なし |
IDE 構文プロンプトによる完璧なサポート | #3 つの SPI メカニズムの比較 この中では、JDK の組み込みメカニズムが最も弱いですが、JDK に組み込まれているため、特定のアプリケーション シナリオが存在します。追加の依存関係が不要である; Dubbo が最も豊富な機能を持っているが、機構が少し複雑であり、Dubbo でしか使用できない; は完全に独立したモジュールとみなすことができない; Spring の機能はこれらとほぼ同じである最大の違いは、すべての拡張ポイントが spring.factories ファイルに記述されていることですが、これも改良点であり、IDEA は構文プロンプトを完全にサポートしています。 |
以上がJava Spring Dubbo の 3 つの SPI メカニズムの違いは何ですかの詳細内容です。詳細については、PHP 中国語 Web サイトの他の関連記事を参照してください。