JDK SPI、Spring SPI、Dubbo SPIの3つの仕組みの詳細と進化
SPIの仕組み
Java SPI(Service Provider Interface)
これは、実行時にアプリケーションにサービス プロバイダーを動的にロードおよび拡張するために JDK によって提供されるサービス検出メカニズムです。
SPI の本質は、ファイル内のインターフェイス実装クラスの完全修飾名を構成することであり、サービス ローダーは構成ファイルを読み取り、実装クラスをロードします。このようにして、実行時に実装クラスをインターフェースに対して動的に置き換えることができます。この機能により、SPI メカニズムを通じてプログラムに拡張機能を簡単に提供できます。
【例】 Java でインタフェースを定義し
java.sql.Driver
、実装クラスに関係なく直接使用します。特定の実装クラスは、SPI メカニズムを通じてロードされます。java.sql.Driver
ドライバーには、JDBC、ODBC などが含まれます。どのドライバー Jar パッケージをインポートすると、 Jar パッケージの META-INF/services ディレクトリにファイルが存在します。このファイルには、現在の Jar のインターフェイスを実装するjava.sql.Driver
実装クラスの完全修飾名が保存されます。パッケージも
サービス プロバイダーがインターフェイス実装を提供する場合、classpath下的META-INF/services/
サービス インターフェイスにちなんで名付けられたファイルをディレクトリ内に作成する必要があります。このファイルの内容はインターフェイスの特定の実装クラスです。META-INF/services/
他のプログラムがこのサービスを必要とする場合、 jar パッケージ内に構成ファイルが含まれています (通常、jar パッケージは依存関係として使用されます)。インスタンス化をロードすると、サービスを使用できるようになります。
JDK でサービスの実装を検索するためのツール クラスは次のとおりですjava.util.ServiceLoader
。
SPI機構の応用
JDBC にドライバーをロードする
1. JDBCインターフェース定義
まず第一に、インターフェースは java で定義されておりjava.sql.Driver
、特定の実装はなく、特定の実装はさまざまなメーカーによって提供されています。
2.mysqlの実装
mysql の jar パッケージ内にディレクトリがmysql-connector-java-6.0.6.jar
あり、META-INF/services
その中に という名前のjava.sql.Driver
ファイルがありcom.mysql.cj.jdbc.Driver
、そのファイルの内容は Java で定義されたインターフェースの実装です。
3.postgresqlの実装
同じ構成ファイルは同じjarpostgresql
パッケージpostgresql-42.0.0.jar
内にもあり、ファイルの内容はorg.postgresql.Driver
、これがpostgresql
Java の実装であることを示していますjava.sql.Driver
。
4. ソースコードの実装
ドライバーの検索は実際には にありDriverManager
、DriverManager
これは Java で実装され、データベース接続を取得するために使用されます。DriverManager
には、次のような静的コード ブロックがあります。
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
インスタンス化ドライバーが読み込まれていることを確認し、loadInitialDrivers
メソッドを確認します。
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//使用SPI的ServiceLoader来加载接口的实现
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
上記のコードの主な手順は次のとおりです。
1. システム変数からドライバーの定義を取得します。
2. SPI を使用してドライバーの実装を取得します。
3. SPI を使用して取得した特定の実装をトラバースし、各実装クラスをインスタンス化します。
4. 最初の手順で取得したドライバー リストに従って、特定の実装クラスをインスタンス化します。
主にステップ 2 と 3 に焦点を当てます。これら 2 つのステップは SPI の使用です。最初に、SPI を使用してドライバー実装を取得する 2 番目のステップを見てください。対応するコードは次のとおりです。
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
META-INF/services
ディレクトリ内の設定ファイルの検索や、特定の実装クラスの読み込みは行われず、インターフェイス タイプとクラス ローダーをカプセル化し、イテレータを初期化するだけです 。次に 3 番目のステップに進み、SPI を使用して取得した特定の実装をトラバースし、各実装クラスをインスタンス化します。対応するコードは次のとおりです。
//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历所有的驱动实现
while(driversIterator.hasNext()) {
driversIterator.next();
}
トラバースするときは、最初にdriversIterator.hasNext()
メソッドを呼び出します。ここでは、ディレクトリclasspath
内のすべてのファイルと jar パッケージを検索し、ファイル内の実装クラスの名前を見つけます。この時点では、特定の実装クラスはインスタンス化されません (特定のソース コードの実装)の下にあります)。META-INF/services
java.sql.Driver
ServiceLoader
driversIterator.next();
次にメソッドを 呼び出すと、ドライバー名に従って各実装クラスがインスタンス化されます。これでドライバーが見つかり、インスタンス化されました。
春のSPI
springboot
自動配線プロセス 中に、META-INF/spring.factories
ファイルは最終的にロードされ、ロードのプロセスはSpringFactoriesLoader
ロードによって実行されます。CLASSPATH
以下の各 Jar パッケージからすべての構成ファイルを検索しMETA-INF/spring.factories
、properties
ファイルを解析し、指定された名前の構成を見つけて戻ります。ClassPath
実際には、パスの下を検索するだけでなく、パスの下のすべての Jar パッケージもスキャンしますが、このファイルは次の JAR パッケージClasspath
内にのみ存在する注意してください。
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// spring.factories文件的格式为:key=value1,value2,value3
// 从所有的jar包中找到META-INF/spring.factories文件
// 然后从文件中解析出key=factoryClass类名称的所有value值
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 取得资源文件的URL
Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
List<String> result = new ArrayList<String>();
// 遍历所有的URL
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 根据资源文件URL解析properties文件,得到对应的一组@Configuration类
Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
String factoryClassNames = properties.getProperty(factoryClassName);
// 组装数据,并返回
result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
}
return result;
}
以下は段落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
では、すべての設定を固定ファイルに入れて、多数のファイルを設定する手間を省きます。複数のインターフェースの拡張構成については、1 つのファイルを使用する方が良いか、別々のファイルを使用する方が良いかは意見の問題です。
Spring的SPI
それはそれに属しますが、主に...spring-framework(core)
で使用されます。spring boot
Spring SPI
サポートにはClassPath
複数のファイルもあり、これらのファイルはロード順序に従って順番にロードされ、1 つの .html に追加されます。エイリアスがないため重複排除の概念がなく、必要なだけ追加できます。spring.factories
classpath
spring.factories
ArrayList
ただしSpring
、 はSPI
主に で使用されるためSpring Boot
、Spring Boot
ではClassLoader
パッケージ内のファイルに依存するのではなく、プロジェクト内のファイルのロードが優先されます。したがって、プロジェクト内でファイルを定義するspring.factories
と、プロジェクト内のファイルが最初にロードされ、Factories
プロジェクト内で構成された実装クラスspring.factories
も最初にランク付けされます。
如果我们要扩展某个接口的话,只需要在你的项目里新建一个META-INF/spring.factories
文件,只添加你要的那个配置。
Dubbo SPI
Dubbo
就是通过SPI
机制加载所有的组件。不过,Dubbo
并未使用 Java 原生的SPI
机制,而是对其进行了增强,使其能够更好的满足需求。在Dubbo
中,SPI
是一个非常重要的模块。基于SPI
,我们可以很容易的对Dubbo
进行拓展。
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
对应的那个扩展实现了。
SPI深入理解
API与SPI的区别
API(Application Programming Interface)
和SPI(Service Provider Interface)
是两种不同的概念,API是软件组件之间的接口规范,用于定义交互方式和通信协议,以便于开发者使用和集成组件。而SPI是一种服务发现机制,用于动态加载和扩展应用程序中的服务提供者,允许通过插件式的方式添加和替换功能实现。API是软件开发中常见的概念,而SPI则是特定于服务发现和扩展的机制。
API(应用程序编程接口):
API是一组定义了软件组件之间交互方式和通信协议的接口。
API提供了一系列的函数、方法、类、协议等,用于让开发者能够与某个软件库、框架或平台进行交互。
API定义了外部组件与提供者之间的约定和规范,以便于开发者可以使用和集成这些组件来实现特定的功能。
API通常由供应商或平台提供,并且在软件开发中广泛使用,以简化开发者的工作,提供特定功能和服务的访问途径。
SPI(服务提供者接口):
SPI是一种服务发现机制,用于在运行时动态加载和扩展应用程序中的服务提供者。
SPI允许开发者定义服务接口,然后通过服务提供者实现该接口,并在运行时通过SPI机制动态发现和加载实现。
SPI通过在类路径下的META-INF/services目录中的配置文件中指定实现类的方式,使得应用程序可以通过插件式的方式添加、替换和扩展功能。
SPI提供了一种松耦合的方式,允许应用程序在不修改源代码的情况下,通过添加新的服务提供者实现来扩展功能。
ServiceLoader
//ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者
public final class ServiceLoader<S>
implements Iterable<S>
{
//查找配置文件的目录
private static final String PREFIX = "META-INF/services/";
//表示要被加载的服务的类或接口
private final Class<S> service;
//这个ClassLoader用来定位,加载,实例化服务提供者
private final ClassLoader loader;
// 访问控制上下文
private final AccessControlContext acc;
// 缓存已经被实例化的服务提供者,按照实例化的顺序存储
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 迭代器
private LazyIterator lookupIterator;
//重新加载,就相当于重新创建ServiceLoader了,用于新的服务提供者安装到正在运行的Java虚拟机中的情况。
public void reload() {
//清空缓存中所有已实例化的服务提供者
providers.clear();
//新建一个迭代器,该迭代器会从头查找和实例化服务提供者
lookupIterator = new LazyIterator(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();
}
//解析失败处理的方法
private static void fail(Class<?> service, String msg, Throwable cause)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg,
cause);
}
private static void fail(Class<?> service, String msg)
throws ServiceConfigurationError
{
throw new ServiceConfigurationError(service.getName() + ": " + msg);
}
private static void fail(Class<?> service, URL u, int line, String msg)
throws ServiceConfigurationError
{
fail(service, u + ":" + line + ": " + msg);
}
//解析服务提供者配置文件中的一行
//首先去掉注释校验,然后保存
//返回下一行行号
//重复的配置项和已经被实例化的配置项不会被保存
private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
List<String> names)
throws IOException, ServiceConfigurationError
{
//读取一行
String ln = r.readLine();
if (ln == null) {
return -1;
}
//#号代表注释行
int ci = ln.indexOf('#');
if (ci >= 0) ln = ln.substring(0, ci);
ln = ln.trim();
int n = ln.length();
if (n != 0) {
if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
fail(service, u, lc, "Illegal configuration-file syntax");
int cp = ln.codePointAt(0);
if (!Character.isJavaIdentifierStart(cp))
fail(service, u, lc, "Illegal provider-class name: " + ln);
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
cp = ln.codePointAt(i);
if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
fail(service, u, lc, "Illegal provider-class name: " + ln);
}
if (!providers.containsKey(ln) && !names.contains(ln))
names.add(ln);
}
return lc + 1;
}
//解析配置文件,解析指定的url配置文件
//使用parseLine方法进行解析,未被实例化的服务提供者会被保存到缓存中去
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
}
return names.iterator();
}
//服务提供者查找的迭代器
private class LazyIterator
implements Iterator<S>
{
Class<S> service;//服务提供者接口
ClassLoader loader;//类加载器
Enumeration<URL> configs = null;//保存实现类的url
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);
}
}
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);
}
if (!service.isAssignableFrom(c)) {
fail(service, "Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
}
}
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();
}
}
//获取迭代器
//返回遍历服务提供者的迭代器
//以懒加载的方式加载可用的服务提供者
//懒加载的实现是:解析配置文件和实例化服务提供者的工作由迭代器本身完成
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();
}
};
}
//为指定的服务使用指定的类加载器来创建一个ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
//使用线程上下文的类加载器来创建ServiceLoader
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
//使用扩展类加载器为指定的服务创建ServiceLoader
//只能找到并加载已经安装到当前Java虚拟机中的服务提供者,应用程序类路径中的服务提供者将被忽略
public static <S> ServiceLoader<S> loadInstalled(Class<S> service) {
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
public String toString() {
return "java.util.ServiceLoader[" + service.getName() + "]";
}
}
首先,ServiceLoader
实现了Iterable
接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext
和next
方法。这里主要都是调用的lookupIterator
的相应hasNext
和next
方法,lookupIterator
是懒加载迭代器。
其次,LazyIterator
中的hasNext
方法,静态变量PREFIX
就是META-INF/services/
目录,这也就是为什么需要在classpath
下的META-INF/services/
目录里创建一个以服务接口命名的文件。
最后,通过反射方法Class.forName()
加载类对象,并用newInstance
方法将类实例化,并把实例化后的类缓存到providers
对象中,(LinkedHashMap<String,S>
类型)然后返回实例对象。
ServiceLoader
したがって、インスタンス化の代わりに、構成ファイル内の特定の実装を読み取り、それをインスタンス化していることが わかります。代わりに、イテレータがトラバースに使用されると、対応する構成ファイルが解析のためにロードされます。hasNext メソッドが呼び出されるとき、構成ファイルは解析のためにロードされます。メソッドが呼び出されると、インスタンス化されてキャッシュされますnext
。すべての構成ファイルは 1 回だけロードされ、サービス プロバイダーは 1 回だけインスタンス化されます。構成ファイルのリロードを使用できますreload
。
JDK SPI、Spring SPI、Dubbo SPI の包括的な比較
JDK SPI | ダボSPI | 春のSPI | |
---|---|---|---|
ファイル形式 | 拡張ポイントごとに個別のファイル | 拡張ポイントごとに個別のファイル | すべての拡張ポイントを 1 つのファイルに |
修正された実装を取得する | サポートされていません。すべての実装を順番に取得することしかできません | 「エイリアス」の概念を使用すると、名前を通じて拡張ポイントの固定実装を取得できます。これは、Dubbo SPI アノテーションと連携するのに非常に便利です。 | サポートされていません。すべての実装は順番にのみ取得できます。ただし、Spring Boot ClassLoader はユーザー コード内のファイルを最初にロードするため、ユーザー定義の spring.factoires ファイルが最初であることが保証され、最初のファクトリを取得することでカスタム拡張機能を修正できます。 |
他の | なし | Dubbo 内での依存関係の注入をサポートし、ディレクトリを通じて Dubbo の組み込み SPI と外部 SPI を区別し、内部 SPI を最初にロードして内部 SPI の最高の優先順位を確保します。 | なし |
ドキュメントの完全性 | 記事とサードパーティのデータが十分に豊富 | ドキュメントとサードパーティ情報が十分に充実している | ドキュメントは十分ではありませんが、機能が少ないため、使い方は非常に簡単です。 |
IDEのサポート | なし | なし | IDEA は構文ヒントを使用してこれを完全にサポートしています |