【SPI】定制按需加载的SPI

283 阅读2分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

思路简概

由于JDK自带的SPI无法做到按需加载,所以考虑自己实现一个能按需加载的ServiceLoader类。

  1. 配置文件格式,形如:key=java.lang.String 格式

  1. 参考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
    }
}