add simple spi impl
This commit is contained in:
379
src/main/java/io/github/ehlxr/extension/ExtensionLoader.java
Normal file
379
src/main/java/io/github/ehlxr/extension/ExtensionLoader.java
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright © 2020 xrv <xrg@live.com>
|
||||||
|
*
|
||||||
|
* 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<T> {
|
||||||
|
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<Class<?>, ExtensionLoader<?>> EXTENSION_LOADERS = new ConcurrentHashMap<>();
|
||||||
|
private static final ConcurrentMap<Class<?>, Object> EXTENSION_INSTANCES = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private final Class<T> type;
|
||||||
|
private final ConcurrentMap<String, Holder<Object>> cachedInstances = new ConcurrentHashMap<>();
|
||||||
|
private final ConcurrentMap<Class<?>, String> cachedNames = new ConcurrentHashMap<>();
|
||||||
|
private final Holder<Map<String, Class<?>>> cachedClasses = new Holder<>();
|
||||||
|
private final Map<String, IllegalStateException> exceptions = new ConcurrentHashMap<>();
|
||||||
|
private String defaultExtension;
|
||||||
|
|
||||||
|
private ExtensionLoader(Class<T> type) {
|
||||||
|
this.type = type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link ExtensionLoader} 的工厂方法。
|
||||||
|
*
|
||||||
|
* @param type 扩展点接口类型
|
||||||
|
* @param <T> 扩展点类型
|
||||||
|
* @return {@link ExtensionLoader} 实例
|
||||||
|
* @throws IllegalArgumentException 参数为 <code>null</code>;
|
||||||
|
* 或是扩展点接口上没有 {@link SPI} 注解。
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> 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<T> loader = (ExtensionLoader<T>) EXTENSION_LOADERS.get(type);
|
||||||
|
if (loader == null) {
|
||||||
|
EXTENSION_LOADERS.putIfAbsent(type, new ExtensionLoader<>(type));
|
||||||
|
loader = (ExtensionLoader<T>) 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<Object> 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<String> getSupportedExtensions() {
|
||||||
|
Map<String, Class<?>> 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<String, Class<?>> getExtensionClasses() {
|
||||||
|
Map<String, Class<?>> 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<String, IllegalStateException> 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<String, IllegalStateException> 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<String, Class<?>> extensionClasses = new HashMap<>();
|
||||||
|
loadFile(extensionClasses, EXTENSIONS_DIRECTORY);
|
||||||
|
loadFile(extensionClasses, EXTENSIONS_INTERNAL_DIRECTORY);
|
||||||
|
cachedClasses.set(extensionClasses);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadFile(Map<String, Class<?>> extensionClasses, String dir) {
|
||||||
|
String fileName = dir + type.getName();
|
||||||
|
try {
|
||||||
|
Enumeration<URL> 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<? extends T> 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 <code>T</code>.
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unused")
|
||||||
|
private static final class Holder<T> {
|
||||||
|
/**
|
||||||
|
* The value contained in the holder.
|
||||||
|
*/
|
||||||
|
private volatile T value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new holder with a <code>null</code> 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
48
src/main/java/io/github/ehlxr/extension/SPI.java
Normal file
48
src/main/java/io/github/ehlxr/extension/SPI.java
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/*
|
||||||
|
* The MIT License (MIT)
|
||||||
|
*
|
||||||
|
* Copyright © 2020 xrv <xrg@live.com>
|
||||||
|
*
|
||||||
|
* 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.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 把一个接口标识成扩展点。
|
||||||
|
* <p>
|
||||||
|
* 没有此注释的接口 {@link ExtensionLoader} 会拒绝接管
|
||||||
|
*
|
||||||
|
* @author ehlxr
|
||||||
|
* @see ExtensionLoader
|
||||||
|
*/
|
||||||
|
@Documented
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Target({ElementType.TYPE})
|
||||||
|
public @interface SPI {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* the default extension name.
|
||||||
|
*/
|
||||||
|
String value() default "";
|
||||||
|
}
|
Reference in New Issue
Block a user