diff --git a/src/main/java/io/github/ehlxr/extension/ExtensionLoader.java b/src/main/java/io/github/ehlxr/extension/ExtensionLoader.java new file mode 100644 index 0000000..05f963c --- /dev/null +++ b/src/main/java/io/github/ehlxr/extension/ExtensionLoader.java @@ -0,0 +1,379 @@ +/* + * The MIT License (MIT) + * + * Copyright © 2020 xrv + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package io.github.ehlxr.extension; + +import io.netty.util.internal.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.BufferedReader; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.io.StringWriter; +import java.net.URL; +import java.nio.charset.StandardCharsets; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.regex.Pattern; + +/** + * 加载和管理扩展(简化版 Dubbo SPI) + * + * @author ehlxr + */ +@SuppressWarnings("unused") +public class ExtensionLoader { + private static final Logger logger = LoggerFactory.getLogger(ExtensionLoader.class); + + private static final String EXTENSIONS_DIRECTORY = "META-INF/extensions/"; + private static final String EXTENSIONS_INTERNAL_DIRECTORY = "META-INF/extensions/internal/"; + + private static final Pattern NAME_SEPARATOR = Pattern.compile("\\s*,+\\s*"); + + private static final ConcurrentMap, ExtensionLoader> EXTENSION_LOADERS = new ConcurrentHashMap<>(); + private static final ConcurrentMap, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>(); + + private final Class type; + private final ConcurrentMap> cachedInstances = new ConcurrentHashMap<>(); + private final ConcurrentMap, String> cachedNames = new ConcurrentHashMap<>(); + private final Holder>> cachedClasses = new Holder<>(); + private final Map exceptions = new ConcurrentHashMap<>(); + private String defaultExtension; + + private ExtensionLoader(Class type) { + this.type = type; + } + + /** + * {@link ExtensionLoader} 的工厂方法。 + * + * @param type 扩展点接口类型 + * @param 扩展点类型 + * @return {@link ExtensionLoader} 实例 + * @throws IllegalArgumentException 参数为 null; + * 或是扩展点接口上没有 {@link SPI} 注解。 + */ + @SuppressWarnings("unchecked") + public static ExtensionLoader getExtensionLoader(Class type) { + if (type == null) { + throw new IllegalArgumentException("SPI type == null"); + } + if (!type.isInterface()) { + throw new IllegalArgumentException("SPI type(" + type.getName() + ") is not interface!"); + } + if (!type.isAnnotationPresent(SPI.class)) { + throw new IllegalArgumentException("type(" + type.getName() + + ") is not a extension, because WITHOUT @SPI Annotation!"); + } + + ExtensionLoader loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + if (loader == null) { + EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<>(type)); + loader = (ExtensionLoader) EXTENSION_LOADERS.get(type); + } + return loader; + } + + /** + * Get the String of Throwable, like the output of {@link Throwable#printStackTrace()}. + * + * @param throwable the input throwable. + */ + private static String throwable2String(Throwable throwable) { + StringWriter w = new StringWriter(1024); + try (PrintWriter p = new PrintWriter(w)) { + throwable.printStackTrace(p); + return w.toString(); + } + } + + public T getExtension(String name) { + if (StringUtil.isNullOrEmpty(name)) { + throw new IllegalArgumentException("SPI name == null"); + } + + Holder holder = cachedInstances.get(name); + if (holder == null) { + cachedInstances.putIfAbsent(name, new Holder<>()); + holder = cachedInstances.get(name); + } + + Object instance = holder.get(); + if (instance == null) { + synchronized (cachedInstances) { + instance = holder.get(); + if (instance == null) { + instance = createExtension(name); + holder.set(instance); + } + } + } + //noinspection unchecked + return (T) instance; + } + + /** + * 返回缺省的扩展。 + * + * @throws IllegalStateException 指定的扩展没有设置缺省扩展点 + */ + public T getDefaultExtension() { + loadExtensionClasses0(); + + if (null == defaultExtension || defaultExtension.length() == 0) { + throw new IllegalStateException("No default extension on extension " + type.getName()); + } + return getExtension(defaultExtension); + } + + /** + * 获取扩展点实现的所有扩展点名。 + * + * @since 0.1.0 + */ + public Set getSupportedExtensions() { + Map> classes = getExtensionClasses(); + return Collections.unmodifiableSet(new HashSet<>(classes.keySet())); + } + + public String getExtensionName(Class spi) { + getExtensionClasses(); + return cachedNames.get(spi); + } + + private T createExtension(String name) { + Class clazz = getExtensionClasses().get(name); + if (clazz == null) { + throw findExtensionClassLoadException(name); + } + try { + //noinspection unchecked + T instance = (T) EXTENSION_INSTANCES.get(clazz); + if (instance == null) { + EXTENSION_INSTANCES.putIfAbsent(clazz, clazz.newInstance()); + //noinspection unchecked + instance = (T) EXTENSION_INSTANCES.get(clazz); + } + + return instance; + } catch (Throwable t) { + throw new IllegalStateException("SPI instance(name: " + name + ", class: " + + type + ") could not be instantiated: " + t.getMessage(), t); + } + } + + private Map> getExtensionClasses() { + Map> classes = cachedClasses.get(); + if (classes == null) { + synchronized (cachedClasses) { + classes = cachedClasses.get(); + if (classes == null) { + loadExtensionClasses0(); + classes = cachedClasses.get(); + } + } + } + return classes; + } + + private IllegalStateException findExtensionClassLoadException(String name) { + for (Map.Entry entry : exceptions.entrySet()) { + if (entry.getKey().toLowerCase().contains(name.toLowerCase())) { + return entry.getValue(); + } + } + + int i = 1; + StringBuilder buf = new StringBuilder("No such extension " + type.getName() + " by name " + name); + for (Map.Entry entry : exceptions.entrySet()) { + if (i == 1) { + buf.append(", possible causes: "); + } + + buf.append("\r\n("); + buf.append(i++); + buf.append(") "); + buf.append(entry.getKey()); + buf.append(":\r\n"); + buf.append(throwable2String(entry.getValue())); + } + return new IllegalStateException(buf.toString()); + } + + private void loadExtensionClasses0() { + final SPI annotation = type.getAnnotation(SPI.class); + if (annotation != null) { + String value = annotation.value(); + if ((value = value.trim()).length() > 0) { + String[] names = NAME_SEPARATOR.split(value); + if (names.length > 1) { + throw new IllegalStateException("more than 1 default extension name on extension " + + type.getName() + ": " + Arrays.toString(names)); + } + if (names.length == 1 && names[0].trim().length() > 0) { + defaultExtension = names[0].trim(); + } + } + } + + Map> extensionClasses = new HashMap<>(); + loadFile(extensionClasses, EXTENSIONS_DIRECTORY); + loadFile(extensionClasses, EXTENSIONS_INTERNAL_DIRECTORY); + cachedClasses.set(extensionClasses); + } + + private void loadFile(Map> extensionClasses, String dir) { + String fileName = dir + type.getName(); + try { + Enumeration urls; + ClassLoader classLoader = ExtensionLoader.class.getClassLoader(); + if (classLoader != null) { + urls = classLoader.getResources(fileName); + } else { + urls = ClassLoader.getSystemResources(fileName); + } + + if (urls != null) { + while (urls.hasMoreElements()) { + URL url = urls.nextElement(); + + try ( + BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)) + ) { + String line; + while ((line = reader.readLine()) != null) { + // delete comments + final int ci = line.indexOf('#'); + if (ci >= 0) { + line = line.substring(0, ci); + } + line = line.trim(); + if (line.length() == 0) { + continue; + } + + try { + String name = null; + int i = line.indexOf('='); + + if (i > 0) { + name = line.substring(0, i).trim(); + line = line.substring(i + 1).trim(); + } + + if (line.length() > 0) { + Class clazz = Class.forName(line, true, classLoader).asSubclass(type); + if (!type.isAssignableFrom(clazz)) { + throw new IllegalStateException("Error when load extension class(interface: " + + type.getName() + ", class line: " + clazz.getName() + "), class " + + clazz.getName() + "is not subtype of interface."); + } + + if (name == null || name.length() == 0) { + // clazz: xxx.xxx.ZfyAPI + // type: xxx.xxx.API + // -> name: zfy + if (clazz.getSimpleName().length() > type.getSimpleName().length() + && clazz.getSimpleName().endsWith(type.getSimpleName())) { + name = clazz.getSimpleName().substring(0, clazz.getSimpleName().length() + - type.getSimpleName().length()).toLowerCase(); + } else { + throw new IllegalStateException("No such extension name for the class " + + clazz.getName() + " in the config " + url); + } + } + + + if (!cachedNames.containsKey(clazz)) { + cachedNames.put(clazz, name); + } + + Class c = extensionClasses.get(name); + if (c == null) { + extensionClasses.put(name, clazz); + } else if (c != clazz) { + throw new IllegalStateException("Duplicate extension " + + type.getName() + " name " + name + " on " + c.getName() + + " and " + clazz.getName()); + } + } + } catch (Throwable t) { + IllegalStateException e = new IllegalStateException("Failed to load extension class(interface: " + + type + ", class line: " + line + ") in " + url + ", cause: " + t.getMessage(), t); + exceptions.put(line, e); + } + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", class file: " + url + ") in " + url, t); + } + } + } + } catch (Throwable t) { + logger.error("Exception when load extension class(interface: " + + type + ", description file: " + fileName + ").", t); + } + } + + @Override + public String toString() { + return this.getClass().getName() + "[" + type.getName() + "]"; + } + + /** + * Holds a value of type T. + */ + @SuppressWarnings("unused") + private static final class Holder { + /** + * The value contained in the holder. + */ + private volatile T value; + + /** + * Creates a new holder with a null value. + */ + Holder() { + } + + /** + * Create a new holder with the specified value. + * + * @param value The value to be stored in the holder. + */ + public Holder(T value) { + this.value = value; + } + + public T get() { + return value; + } + + public void set(T value) { + this.value = value; + } + } +} diff --git a/src/main/java/io/github/ehlxr/extension/SPI.java b/src/main/java/io/github/ehlxr/extension/SPI.java new file mode 100644 index 0000000..69fdfb5 --- /dev/null +++ b/src/main/java/io/github/ehlxr/extension/SPI.java @@ -0,0 +1,48 @@ +/* + * The MIT License (MIT) + * + * Copyright © 2020 xrv + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +package io.github.ehlxr.extension; + +import io.github.ehlxr.did.extension.ExtensionLoader; + +import java.lang.annotation.*; + +/** + * 把一个接口标识成扩展点。 + *

+ * 没有此注释的接口 {@link ExtensionLoader} 会拒绝接管 + * + * @author ehlxr + * @see ExtensionLoader + */ +@Documented +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.TYPE}) +public @interface SPI { + + /** + * the default extension name. + */ + String value() default ""; +}