add simple spi impl
This commit is contained in:
parent
10dd2ad1cf
commit
ca9f7ae7f2
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 "";
|
||||
}
|
Loading…
Reference in New Issue
Block a user