持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情
思路简概
由于JDK自带的SPI无法做到按需加载,所以考虑自己实现一个能按需加载的ServiceLoader类。
- 配置文件格式,形如:key=java.lang.String 格式
- 参考JDK的SPI实现,更改其中对配置文件的解析操作,JDK中解析配置文件时会将得到的所有实现类的全限定名放到一个ArrayList集合中,最后再通过类加载器、反射等操作创建Class对象、实例化对象。而我们所要做的就是在解析配置文件时只找到key相等的实现类。
项目已经放到我的Gitee仓库下了,需要的小伙伴可以去下载:刷刷/shuashua-blog/按需加载的SPI/ServiceLoader
具体实现
load方法
不指定类加载器的load方法
public static <S> S load(Class<S> clazz,String key) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return load(clazz, cl, key);
}
指定类加载器的load方法
public static <S> S load(Class<S> clazz,ClassLoader loader,String key){
Objects.requireNonNull(clazz, "key cannot be null");
Objects.requireNonNull(clazz, "Service interface cannot be null");
String fullName = DIR_PREFIX + clazz.getName();
Enumeration<URL> enumeration = null;
try {
enumeration = loader.getResources(fullName);
} catch (IOException e) {
fail(clazz, "Error locating configuration files", e);
}
if (enumeration.hasMoreElements()) {
URL url = enumeration.nextElement();
// 从配置文件中找出与key相等的实现类
String targetClazzName = parseAndFindKey(clazz, url, key);
if(targetClazzName==null || "".equals(targetClazzName)){
fail(clazz,fullName+" does not have a matching key or bad value");
}
return createInstance(clazz,loader,targetClazzName);
}
fail(clazz,fullName+" has no contents");
throw new Error(); // This cannot happen
}
配置文件的解析
parseAndFindKey(...)方法,解析配置文件,找到与key相等的实现类的全限定名,找不到则返回null
private static String parseAndFindKey(Class<?> service, URL u,String key) throws ServiceConfigurationError {
InputStream in = null;
BufferedReader r = null;
StringBuilder targetClazz = new StringBuilder();
try {
in = u.openStream();
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, key,targetClazz)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return targetClazz.toString();
}
parseLine(...)方法,解析配置文件中的某一行,如果该行格式符合规则,且key相等,则返回-2(意为找到实现类)。
private static int parseLine(Class<?> service, URL u, BufferedReader r, int lc, String key,StringBuilder targetClazz) 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(); // 去除头尾空白字符
String[] split = ln.split("=");
if(split.length>2){
fail(service,u,lc,"Illegal configuration-file syntax");
}
if(key.equals(split[0])){ // 如果本行就是要找的目标行
ln=split[1];
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);
}
targetClazz.append(ln);
return -2;
}
}
return lc + 1;
}
测试
定义接口和实现类
接口
public interface DemoInterface {
void sayHello();
}
实现类-01
public class Chinese implements DemoInterface{
@Override
public void sayHello() {
System.out.println("你好");
}
}
实现类-02
public class English implements DemoInterface {
@Override
public void sayHello() {
System.out.println("hello");
}
}
添加配置文件
测试运行
public class Main {
public static void main(String[] args) {
DemoInterface chinese = MyServiceLoader.load(DemoInterface.class, "chinese");
chinese.sayHello(); // 你好
DemoInterface english = MyServiceLoader.load(DemoInterface.class, "english");
english.sayHello(); // hello
}
}