我正在尝试通过在其上编写包装器并根据我的需要仅使用所需的 API 来封装第三方库。
为了实现这一目标,我创建了这个包装器项目作为一个独立项目,其中包含第三方库的所有依赖项。
第三方库将来可能会更换为更好的库,因此需要很大程度的解耦。
在这样做时,我遇到了第三方库的一些服务提供者接口,这些接口是使用库中某处的 java ServiceLoader 加载的。
我希望我的包装项目的用户完全不知道正在使用的第三方库。
为了实现这一目标,我必须编写我的包装项目,这样用户就不需要依赖第三方。
但是SPI通常需要在项目的META-INF/services/{SPIName}文件中进行配置,最终会导致第三方知识泄露。
如何封装这些第三方 SPI,以便最终用户完全不知道第三方库?
这种情况有专业术语吗?或者是否有现有的解决方案或设计模式?
在用尽网上搜索后,我尝试了一些方法
第三方SPI
package com.thirdParty.library;
public interface IDataCollector {
String getStudentName();
Integer getMarks();
}
加载该 SPI 的所有服务提供者的某个类。请注意,在实际的第三方中,它是多么复杂。
public class RankCalculator {
private static final Map<Integer, String> ranks = new TreeMap<>();
public RankCalculator () {
ServiceLoader<IDataCollector> dataCollectorLoader = ServiceLoader.load(IDataCollector.class);
for (IDataCollector dataCollector: dataCollectorLoader) {
ranks.put(dataCollector.getMarks(), dataCollector.getStudentName());
}
}
public Integer getRank (String name) {
Integer position = ranks.size();
for (Map.Entry<Integer, String> entry: ranks.entrySet()) {
if (entry.getValue().equals(name)) {
return position;
}
position --;
}
return -1;
}
}
Wrapper-Project:我计划在
IdataCollector
的包装器中编写一个接口,然后实现该接口,我也可以在 META-INF/services 中注册它。此外,我想我应该向最终用户公开 IWrapDataCollector,他们将使用此接口注册他们的 META-INF/服务。事情是这样的:
package com.myWrapper.test;
import com.thirdParty.library.IDataCollector;
public interface IWrapDataCollector extends IDataCollector {
default Integer getMarksWrapper() {
return null;
}
default String getStudentsNameWrapper() {
return null;
}
@Override
default String getStudentName() {
return getStudentsNameWrapper();
}
@Override
default Integer getMarks() {
return getMarksWrapper();
}
}
META-INF/services/com.thirdParty.library.IDataCollector
,条目为com.myWrapper.test.IWrapDataCollector
,但我很快意识到 ServiceLoader 只能加载具体的类,而 IWrapDataCollector
与我放弃这个想法相差甚远。这个梦想失败后,
我尝试(反对所有文档建议)编写一个具体的实现作为我的 SPI
package com.myWrapper.test;
import com.thirdParty.library.IDataCollector;
public class IWrapDataCollector implements IDataCollector {
public Integer getMarksWrapper() {
return null;
}
public String getStudentsNameWrapper() {
return null;
}
@Override
public final String getStudentName() {
return getStudentsNameWrapper();
}
@Override
public final Integer getMarks() {
return getMarksWrapper();
}
}
我扩展了这个具体类并编写了 2 个实现来测试这个想法。请注意,我目前已在我的最终用户项目中添加了第三方依赖项,并计划在这个想法得以实施后将其删除。
然而,我再次意识到这个想法是多么愚蠢。这个类肯定得到了,但只有这个类被加载,这实际上是正确的,因为我已经注册了这个类,并且我为此得到了一个当之无愧的 NullPointerException 。
再次,我怎样才能实现这一点,并通过将其保留在包装项目中来从用户项目中删除第三方的依赖关系。
如果您在自己的接口+实现中使用
extends ThirdPartyXYZ
,您将不会将您的SPI与第三方解耦。您的接口必须清除第三方调用,并且实现调用仅委托给第三方调用。然后,当您删除第 3 方时,您将不需要完全更改您的界面和客户端代码。
所以你需要一个干净的服务界面
IWrapDataCollector
:
package com.myWrapper.test;
public interface IWrapDataCollector {
String getStudentName();
Integer getMarks();
... reproduce all calls needed for the service, no 3rd party classes
}
为您的包装器添加一个实现。第一次尝试时,只需停止所有呼叫,并且不要呼叫第三方:
package com.myWrapper.impl;
// TEST WITHOUT CALLS TO THIRD PARTY:
public class MyDataCollector implements IWrapDataCollector
/* MUST HAVE public no-args constructor */
public MyDataCollector() {}
String getStudentName() { return "dummy value"; }
Integer getMarks() { return 1; }
... etc
将一个文件添加到 jar
META-INF/services/com.myWrapper.test.IWrapDataCollector
,其中有一行引用您的包装器:
com.myWrapper.impl.MyDataCollector
然后您应该能够使用以下方法测试您的空白实现:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);
如果有效,请通过委托给第 3 方实例的调用重新实现
MyDataCollector
。这将通过第 3 方 API 的服务负载调用来访问第三方:
public class MyDataCollector implements IWrapDataCollector
IThirdParty delegate = ServiceLoader.load(IThirdParty.class);
public MyDataCollector() {}
String getStudentName() {
return delegate.getStudentName();
}
... etc
您自己的客户应该仍然可以使用:
IWrapDataCollector impl = ServiceLoader.load(IWrapDataCollector.class);