关于java:聊聊如何实现一个支持键值对的SPI

52次阅读

共计 9236 个字符,预计需要花费 24 分钟才能阅读完成。

前言

如果用过 JDK 提供的 SPI 机制的敌人,大略就会晓得它无奈按需加载。之前写过一篇文章聊聊基于 jdk 实现的 spi 如何与 spring 整合实现依赖注入。利用 spring 的依赖注入来实现 spi 按需加载,这种计划就是要借用 spring。明天咱们在聊聊另外一种实现形式,就是咱们本人手写一个

实现思路

整体思路和 jdk 实现 spi 差不多,如果对 jdk 实现的 spi 不理解,能够查看我之前写的文章 java 之 spi 机制简介。差异就是咱们在配置文件是以 key-value 的模式存在,形如

springMysql=com.github.lybgeek.dialect.mysql.SpringMysqlDialect

实现逻辑

1、约定好要进行解析的目录,比方 META-INF/services/

 private static final String SERVICE_DIRECTORY = "META-INF/services/";

2、约定好要解析的文件名命名,比方

com.github.lybgeek.dialect.SpringSqlDialect

3、约定好文件内容格局,比方

springMysql=com.github.lybgeek.dialect.mysql.SpringMysqlDialect

4、获取约定好的目录,解析文件,并将相应内容放入缓存

 /**
     * Load files under SERVICE_DIRECTORY.
     */
    private void loadDirectory(final Map<String, Class<?>> classes) {String fileName = SERVICE_DIRECTORY + clazz.getName();
        try {ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
                    : ClassLoader.getSystemResources(fileName);
            if (urls != null) {while (urls.hasMoreElements()) {URL url = urls.nextElement();
                    loadResources(classes, url);
                }
            }
        } catch (IOException t) {log.error("load extension class error {}", fileName, t);
        }
    }

    private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {try (InputStream inputStream = url.openStream()) {Properties properties = new Properties();
            properties.load(inputStream);
            properties.forEach((k, v) -> {String name = (String) k;
                String classPath = (String) v;
                if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
                    try {loadClass(classes, name, classPath);
                    } catch (ClassNotFoundException e) {throw new IllegalStateException("load extension resources error", e);
                    }
                }
            });
        } catch (IOException e) {throw new IllegalStateException("load extension resources error", e);
        }
    }

    private void loadClass(final Map<String, Class<?>> classes,
                           final String name, final String classPath) throws ClassNotFoundException {Class<?> subClass = Class.forName(classPath);
        if (!clazz.isAssignableFrom(subClass)) {throw new IllegalStateException("load extension resources error," + subClass + "subtype is not of" + clazz);
        }
        Activate annotation = subClass.getAnnotation(Activate.class);
        if (annotation == null) {throw new IllegalStateException("load extension resources error," + subClass + "with Activate annotation");
        }
        Class<?> oldClass = classes.get(name);
        if (oldClass == null) {classes.put(name, subClass);
        } else if (oldClass != subClass) {throw new IllegalStateException("load extension resources error,Duplicate class" + clazz.getName() + "name" + name + "on" + oldClass.getName() + "or" + subClass.getName());
        }
    }

5、依据 key,去缓存查找相应的类实例

 public T getActivate(final String name) {if (StringUtils.isBlank(name)) {throw new NullPointerException("get Activate name is null");
        }
        Holder<Object> objectHolder = cachedInstances.get(name);
        if (objectHolder == null) {cachedInstances.putIfAbsent(name, new Holder<>());
            objectHolder = cachedInstances.get(name);
        }
        Object value = objectHolder.getValue();
        if (value == null) {synchronized (cachedInstances) {value = objectHolder.getValue();
                if (value == null) {value = createExtension(name);
                    objectHolder.setValue(value);
                }
            }
        }
        return (T) value;
    }

外围代码

@Slf4j
@SuppressWarnings("all")
public final class ExtensionLoader<T> {

    private static final String SERVICE_DIRECTORY = "META-INF/services/";

    private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();

    private final Class<T> clazz;

    private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();

    private final Map<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();

    private final Map<Class<?>, Object> ActivateInstances = new ConcurrentHashMap<>();

    private String cachedDefaultName;

    /**
     * Instantiates a new Extension loader.
     *
     * @param clazz the clazz.
     */
    private ExtensionLoader(final Class<T> clazz) {
        this.clazz = clazz;
        if (clazz != ExtensionFactory.class) {ExtensionLoader.getExtensionLoader(ExtensionFactory.class).getExtensionClasses();}
    }

    /**
     * Gets extension loader.
     *
     * @param <T>   the type parameter
     * @param clazz the clazz
     * @return the extension loader.
     */
    public static <T> ExtensionLoader<T> getExtensionLoader(final Class<T> clazz) {if (clazz == null) {throw new NullPointerException("extension clazz is null");
        }
        if (!clazz.isInterface()) {throw new IllegalArgumentException("extension clazz (" + clazz + ") is not interface!");
        }
        if (!clazz.isAnnotationPresent(SPI.class)) {throw new IllegalArgumentException("extension clazz (" + clazz + ") without @" + SPI.class + "Annotation");
        }
        ExtensionLoader<T> extensionLoader = (ExtensionLoader<T>) LOADERS.get(clazz);
        if (extensionLoader != null) {return extensionLoader;}
        LOADERS.putIfAbsent(clazz, new ExtensionLoader<>(clazz));
        return (ExtensionLoader<T>) LOADERS.get(clazz);
    }

    /**
     * Gets default Activate.
     *
     * @return the default Activate.
     */
    public T getDefaultActivate() {getExtensionClasses();
        if (StringUtils.isBlank(cachedDefaultName)) {return null;}
        return getActivate(cachedDefaultName);
    }

    /**
     * Gets Activate.
     *
     * @param name the name
     * @return the Activate.
     */
    public T getActivate(final String name) {if (StringUtils.isBlank(name)) {throw new NullPointerException("get Activate name is null");
        }
        Holder<Object> objectHolder = cachedInstances.get(name);
        if (objectHolder == null) {cachedInstances.putIfAbsent(name, new Holder<>());
            objectHolder = cachedInstances.get(name);
        }
        Object value = objectHolder.getValue();
        if (value == null) {synchronized (cachedInstances) {value = objectHolder.getValue();
                if (value == null) {value = createExtension(name);
                    objectHolder.setValue(value);
                }
            }
        }
        return (T) value;
    }

    public Set<String> getSupportedExtensions() {Map<String, Class<?>> clazzes = getExtensionClasses();
        return Collections.unmodifiableSet(new TreeSet<>(clazzes.keySet()));
    }

    @SuppressWarnings("unchecked")
    private T createExtension(final String name) {Class<?> aClass = getExtensionClasses().get(name);
        if (aClass == null) {throw new IllegalArgumentException("name is error");
        }
        Object o = ActivateInstances.get(aClass);
        if (o == null) {
            try {ActivateInstances.putIfAbsent(aClass, aClass.newInstance());
                o = ActivateInstances.get(aClass);
            } catch (InstantiationException | IllegalAccessException e) {
                throw new IllegalStateException("Extension instance(name:" + name + ", class:"
                        + aClass + ")  could not be instantiated:" + e.getMessage(), e);

            }
        }
        return (T) o;
    }

    /**
     * Gets extension classes.
     *
     * @return the extension classes
     */
    public Map<String, Class<?>> getExtensionClasses() {Map<String, Class<?>> classes = cachedClasses.getValue();
        if (classes == null) {synchronized (cachedClasses) {classes = cachedClasses.getValue();
                if (classes == null) {classes = loadExtensionClass();
                    cachedClasses.setValue(classes);
                }
            }
        }
        return classes;
    }

    private Map<String, Class<?>> loadExtensionClass() {SPI annotation = clazz.getAnnotation(SPI.class);
        if (annotation != null) {String value = annotation.value();
            if (StringUtils.isNotBlank(value)) {cachedDefaultName = value;}
        }
        Map<String, Class<?>> classes = new HashMap<>(16);
        loadDirectory(classes);
        return classes;
    }

    /**
     * Load files under SERVICE_DIRECTORY.
     */
    private void loadDirectory(final Map<String, Class<?>> classes) {String fileName = SERVICE_DIRECTORY + clazz.getName();
        try {ClassLoader classLoader = ExtensionLoader.class.getClassLoader();
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources(fileName)
                    : ClassLoader.getSystemResources(fileName);
            if (urls != null) {while (urls.hasMoreElements()) {URL url = urls.nextElement();
                    loadResources(classes, url);
                }
            }
        } catch (IOException t) {log.error("load extension class error {}", fileName, t);
        }
    }

    private void loadResources(final Map<String, Class<?>> classes, final URL url) throws IOException {try (InputStream inputStream = url.openStream()) {Properties properties = new Properties();
            properties.load(inputStream);
            properties.forEach((k, v) -> {String name = (String) k;
                String classPath = (String) v;
                if (StringUtils.isNotBlank(name) && StringUtils.isNotBlank(classPath)) {
                    try {loadClass(classes, name, classPath);
                    } catch (ClassNotFoundException e) {throw new IllegalStateException("load extension resources error", e);
                    }
                }
            });
        } catch (IOException e) {throw new IllegalStateException("load extension resources error", e);
        }
    }

    private void loadClass(final Map<String, Class<?>> classes,
                           final String name, final String classPath) throws ClassNotFoundException {Class<?> subClass = Class.forName(classPath);
        if (!clazz.isAssignableFrom(subClass)) {throw new IllegalStateException("load extension resources error," + subClass + "subtype is not of" + clazz);
        }
        Activate annotation = subClass.getAnnotation(Activate.class);
        if (annotation == null) {throw new IllegalStateException("load extension resources error," + subClass + "with Activate annotation");
        }
        Class<?> oldClass = classes.get(name);
        if (oldClass == null) {classes.put(name, subClass);
        } else if (oldClass != subClass) {throw new IllegalStateException("load extension resources error,Duplicate class" + clazz.getName() + "name" + name + "on" + oldClass.getName() + "or" + subClass.getName());
        }
    }




    /**
     * The type Holder.
     *
     * @param <T> the type parameter.
     */
    public static class Holder<T> {

        private volatile T value;

        /**
         * Gets value.
         *
         * @return the value
         */
        public T getValue() {return value;}

        /**
         * Sets value.
         *
         * @param value the value
         */
        public void setValue(final T value) {this.value = value;}
    }
}

应用示例

1、定义服务接口

@SPI("mysql")
public interface SqlDialect {String dialect();

}

2、定义具体实现类

@Activate
public class MysqlDialect implements SqlDialect {
    @Override
    public String dialect() {return "mysql";}


}
@Activate
public class OracleDialect implements SqlDialect {
    @Override
    public String dialect() {return "oracle";}


}

3、src/main/resources/ 下建设 /META-INF/services 目录,新增一个以接口命名的文件

4、接口命名的文件填入如下内容


5、加载服务类

   SqlDialect sqlDialect = ExtensionLoader.getExtensionLoader(SqlDialect.class).getActivate("mysql");

6、测试

  @Test
    public void testSpi(){SqlDialect sqlDialect = ExtensionLoader.getExtensionLoader(SqlDialect.class).getActivate("mysql");
        Assert.assertEquals("mysql",sqlDialect.dialect());
    }

总结

如果有用过 dubbo 的 spi 的敌人,就会发现下面实现的思路基本上就是 dubbo 的 spi 简化版。如果是有理解过 shenyu 网关的 spi 机制的敌人,就会发现下面的实现思路和 shenyu 网关基本上是一样了。

demo 链接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-spi-enhance/springboot-spi-framework

正文完
 0