之前我们有使用过dubbo的SPI机制,并且java也有自己的SPI机制
java的SPI不能按需加载,没有优先级,只能空参构造;dubbo的SPI和自己深度结合,无法单独使用。我们需要一个满足以下特性的SPI机制
- 和其它SPI机制类似,支持按需加载
- 可以将实现类和其优先级定义在一起,可以按优先级加载,也可以全部加载
- 在某种程度上支持参数构造,因为可能某一接口的全部实现类都依赖相同的运行时参数
说明
使用java类似的定义方案,但是新增一个优先级,使用逗号分隔:优先级是一个数字,默认加载最大数字的实现类
比如对于plume项目,假设有三个模块:main、custom、adapter
- main是启动模块,依赖custom
- custom里是一些组件的默认实现
- adapter里可能有一些增强实现,但不一定会引用
main在启动时候会使用一个自定义的BeanPostProcessor
,在custom和adapter里都有实现
如何实现在adapter引用时就调用adapter里的BeanPostProcessor
呢
- 首先在custom和adapter的resources目录添加一个目录
/META-INF/plume
并添加文件org.springframework.beans.factory.config.BeanPostProcessor
- 在custom的上述接口文件里新增:
io.cana.plume.custom.initial.BootBeanPostProcessor,20
- 在adapter的上述接口文件里新增:
io.cana.plume.adapter.initial.BootBeanPostProcessor,100
- 在main里获取实现类时使用
constructImplementObject("/META-INF/plume/org.springframework.beans.factory.config.BeanPostProcessor")
即可
读取和解析
首先肯定要读取到所有针对当前接口的定义文件,使用ClassLoader
既可
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
private static Enumeration<URL> loadResourceUrl(String resourceName) throws IOException { return Thread.currentThread().getContextClassLoader().getResources(resourceName); }
private static void parseResourceUrl(TreeMap<Integer, String> names, URL url) throws IOException { try (final InputStream in = url.openStream(); final BufferedReader br = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { int lc = 1; while ((lc = parseResourceInnerLine(names, br, lc)) >= 0) ; } }
|
parseResourceInnerLine
是对每个文件里的每一行解析,首先要去掉#后边的注释部分,然后按逗号分隔
分隔之后拿到的类全名如果包含Character.isJavaIdentifierPart
和点之外的字符则认为文件错误,不继续进行解析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
|
private static int parseResourceInnerLine(TreeMap<Integer, String> names, BufferedReader br, int lc) throws IOException { int p = 0; String ln = br.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) { final int idx = ln.indexOf(','); if (idx >= 0) { p = Integer.parseInt(ln.substring(idx + 1).trim()); ln = ln.substring(0, idx).trim(); n = ln.length(); } if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0)) { log.error("Illegal configuration-file syntax: {}", ln); return -1; } int cp = ln.codePointAt(0); if (!Character.isJavaIdentifierStart(cp)) { log.error("Illegal provider-class name: " + ln); return -1; }
for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) { cp = ln.codePointAt(i); if (!Character.isJavaIdentifierPart(cp) && (cp != '.')) { log.error("Illegal provider-class name: " + ln); return -1; } } if (!names.containsValue(ln)) { names.put(p, ln); } } return lc + 1; }
|
按需加载
拿到的TreeMap里只有实现类的类全名
- 按需要加载只要实例化优先级最高的即可,即只需要lastEntry
- 全部加载只需要返回整个TreeMap,由用户手动实例化
- 如果有构造的实际参数,就能拿到对应的构造方法从而实现参数构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
|
@SuppressWarnings("unchecked") public static <T> T constructImplementObject(String interfaceName, Object... constructorParams) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException { if (null == constructorParams || constructorParams.length == 0) { return (T) loadImplementClass(interfaceName).newInstance(); }
final Class<?>[] constructorClasses = new Class[constructorParams.length]; for (int i = 0; i < constructorParams.length; i++) { final Object param = constructorParams[i]; constructorClasses[i] = param.getClass(); } final Constructor<T> clazz = loadImplementConstructor(interfaceName, constructorClasses); return clazz.newInstance(constructorParams); }
@SuppressWarnings("unchecked") public static <T> Constructor<T> loadImplementConstructor(String interfaceName, Class<?>... types) throws ClassNotFoundException, NoSuchMethodException { final Class<?> clazz = loadImplementClass(interfaceName); return (Constructor<T>) clazz.getDeclaredConstructor(types); }
public static Class<?> loadImplementClass(String interfaceName) throws ClassNotFoundException { return Class.forName(loadImplementName(interfaceName), false, Thread.currentThread().getContextClassLoader()); }
|
使用限制
constructImplementObject
实际需要输入的是文件相对于classpath的完整路径,所以最好在封装一次
- 如果定义了两个一样的优先级。因为TreeMap只会保留一个,而且不确定是哪一个
- 构造参数不能使用基础类型,如果要用只能使用包装类。因为Object传递参数时会被自动装箱