Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: cache SPI extension classes #27

Merged
merged 1 commit into from
Jul 25, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
195 changes: 89 additions & 106 deletions common/src/main/java/de/xab/porter/common/spi/ExtensionLoader.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
package de.xab.porter.common.spi;

import de.xab.porter.api.exception.PorterException;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
Expand All @@ -15,163 +13,148 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static de.xab.porter.common.util.Strings.notNullOrEmpty;

/**
* a extension loader can load any implements of one service, which is describe in resources/extensions.
* ExtensionLoader load class using current class loader, construct new instance and inject type for consuming
* <p>
* extension must have these features:
* implement at least one service,
* registered in resources/extensions
* registered in resources/META-INF/porter
* <p>
* any service must opens to module {@link de.xab.porter.common}
* any extensions must opens to module {@link de.xab.porter.common}
* <p>
* comments after # will be ignored
*/
public class ExtensionLoader {
public class ExtensionLoader<T> {
private static final String FOLDER = "META-INF/porter/";
private static final Map<Class<?>, Map<String, Class<?>>> EXTENSION_HOLDER = new ConcurrentHashMap<>();
private static final Map<Class<?>, ExtensionLoader<?>> LOADERS = new ConcurrentHashMap<>();
private final Map<String, Class<T>> extensions = new ConcurrentHashMap<>();
private Class<T> service;

public static ExtensionLoader getExtensionLoader() {
return new ExtensionLoader();
public static <T> ExtensionLoader<T> getExtensionLoader(Class<T> service) {
if (service == null) {
throw new IllegalArgumentException("service is null");
}
if (!service.isInterface()) {
throw new IllegalArgumentException(String.format("service %s is not a interface", service));
}
ExtensionLoader<T> loader = (ExtensionLoader<T>) LOADERS.get(service);
if (loader == null) {
LOADERS.putIfAbsent(service, new ExtensionLoader<T>());
loader = (ExtensionLoader<T>) LOADERS.get(service);
loader.service = service;
}
return loader;
}

public <T> T loadExtension(String type, Class<T> clazz) {
final Class<?> driverClass = loadClass(type, clazz);
if (!driverClass.getModule().isOpen(driverClass.getPackageName(), this.getClass().getModule())) {
throw new PorterException(String.format(
"cannot access class %s at %s", driverClass.getName(), this.getClass().getModule()));
public T loadExtension(String type) {
Class<T> clazz = loadExtensionClass(type);
if (!clazz.getModule().isOpen(clazz.getPackageName(), this.getClass().getModule())) {
throw new RuntimeException(String.format(
"cannot access class %s at %s", clazz.getName(), this.getClass().getModule()));
}
try {
final T instance = (T) driverClass.getConstructor().newInstance();
T instance = clazz.getConstructor().newInstance();
injectExtension(instance, type);
return instance;
} catch (InstantiationException | IllegalAccessException
| InvocationTargetException | NoSuchMethodException e) {
throw new PorterException("extension construct failed", e);
} catch (InstantiationException | NoSuchMethodException
| IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(String.format("cannot create extension %s of %s", type, this.service), e);
}
}

/**
* load Class of extensions. PorterException will be thrown as load failed
*
* @param type exactly type of extension
* @param clazz Class of service
* @return Class of extension
*/
public Class<?> loadClass(String type, Class<?> clazz) {
Class<?> subClass = EXTENSION_HOLDER.computeIfAbsent(clazz, ignored -> new ConcurrentHashMap<>()).get(type);
if (subClass != null) {
return subClass;
}
final String fileName = FOLDER + clazz.getName();
final Enumeration<URL> urls;
final ClassLoader cl = findClassLoader(clazz);
try {
urls = cl.getResources(fileName);
} catch (IOException e) {
throw new PorterException(String.format("no implement(s) of %s found in %s", clazz.getName(), fileName));
private Class<T> loadExtensionClass(String type) {
Class<T> extensionClass = this.extensions.get(type);
ClassLoader classLoader = findClassLoader(this.service);
if (extensionClass == null) {
synchronized (this.extensions) {
extensionClass = this.extensions.get(type);
if (extensionClass == null) {
String extensionName = null;
try {
extensionName = findExtensionName(classLoader, type);
extensionClass = (Class<T>) classLoader.loadClass(extensionName);
} catch (IOException e) {
throw new TypeNotPresentException(type, e);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(
String.format("cannot load class %s for %s: %s", extensionName, type, this.service));
}
if (!implementedInterface(extensionClass, this.service)) {
throw new IllegalStateException(extensionName + " not implemented " + this.service);
}
this.extensions.put(type, extensionClass);
}
}
}
return extensionClass;
}

private String findExtensionName(ClassLoader classLoader, String type) throws IOException {
Enumeration<URL> urls;
String resourceFolder = FOLDER + this.service.getName();
urls = classLoader.getResources(resourceFolder);
while (urls != null && urls.hasMoreElements()) {
final URL url = urls.nextElement();
URL url = urls.nextElement();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(
url.openStream(), StandardCharsets.UTF_8))) {
String line;
final Map<String, Class<?>> specifyClassMap = EXTENSION_HOLDER.get(clazz);
while ((line = reader.readLine()) != null) {
Map.Entry<String, String> entry = parseTypeAndClass(line, clazz);
String driverType = entry.getKey();
String className = entry.getValue();
if (notNullOrEmpty(className)) {
subClass = loadClassByName(cl, clazz, className);
specifyClassMap.putIfAbsent(driverType, subClass);
EXTENSION_HOLDER.put(clazz, specifyClassMap);
if (type.equals(driverType)) {
return subClass;
}
Map.Entry<String, String> tuple = parseLine(line);
if (tuple == null) {
continue;
}
String extensionType = tuple.getKey();
String extensionName = tuple.getValue();
if (type.equals(extensionType)) {
return extensionName;
}
}
} catch (IOException e) {
throw new PorterException(String.format("load extension class %s failed", clazz.getName()), e);
}
}
throw new PorterException(String.format("type `%s` of extension %s not found", type, clazz.getName()));
throw new IOException("no appropriate type found for " + this.service);
}

private Map.Entry<String, String> parseTypeAndClass(String line, Class<?> clazz) {
String driverType;
String className;
String newLine = line;
final int ci = newLine.indexOf('#');
private Map.Entry<String, String> parseLine(String origin) {
String extensionType;
String extensionName;
String line = origin;
int ci = line.indexOf('#');
if (ci >= 0) {
newLine = newLine.substring(0, ci);
line = line.substring(0, ci);
}
newLine = newLine.trim();
if (newLine.length() > 0) {
final String[] split = newLine.split("=");
line = line.trim();
if (line.length() > 0) {
final String[] split = line.split("=");
if (split.length == 2) {
driverType = split[0];
className = split[1];
return Map.entry(driverType, className);
}
}
throw new PorterException(String.format(
"parse extension %s failed. expected `type=foo.bar.Extension`, got %s", clazz.getSimpleName(), line));
}

/**
* load class by given Class name
*
* @param loader classloader to load class
* @param service service of class implemented
* @param className class name to be loaded
* @return Class if load successfully
*/
private Class<?> loadClassByName(ClassLoader loader, Class<?> service, String className) {
try {
Class<?> subClass = loader.loadClass(className);
if (implementedInterface(subClass, service)) {
return subClass;
} else {
throw new PorterException(String.format("no implement classes found of class %s", service));
extensionType = split[0];
extensionName = split[1];
return Map.entry(extensionType, extensionName);
}
} catch (ClassNotFoundException e) {
throw new PorterException(String.format("class %s not found", className), e);
}
return null;
}

/**
* whether extension defined in resources implemented service.
*
* @param subClass extension Class
* @param interfaceClass service Class
* @return true if extension if sub class of service
*/
private boolean implementedInterface(Class<?> subClass, Class<?> interfaceClass) {
Class<?> currentClass = subClass;
private boolean implementedInterface(Class<T> extensionClass, Class<T> serviceClass) {
Class<?> currentClass = extensionClass;
boolean isImplemented = false;
while (!isImplemented) {
if (currentClass == Object.class) {
break;
}
isImplemented = Arrays.stream(currentClass.getInterfaces()).
anyMatch(oneInterface -> oneInterface == interfaceClass);
anyMatch(oneInterface -> oneInterface == serviceClass);
currentClass = currentClass.getSuperclass();
}
return isImplemented;
}

private <T> void injectExtension(T instance, String type) {
private void injectExtension(T instance, String type) throws InvocationTargetException, IllegalAccessException {
for (Method method : instance.getClass().getMethods()) {
if (!isTypeSetter(method)) {
continue;
}
try {
method.invoke(instance, type);
} catch (IllegalAccessException | InvocationTargetException e) {
throw new PorterException(String.format("instance %s inject %s failed", instance, type));
}
method.invoke(instance, type);
}
}

Expand All @@ -181,7 +164,7 @@ private boolean isTypeSetter(Method method) {
&& method.getParameterCount() == 1;
}

private ClassLoader findClassLoader(Class<?> clazz) {
private ClassLoader findClassLoader(Class<T> clazz) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
if (cl == null) {
cl = clazz.getClassLoader();
Expand All @@ -191,4 +174,4 @@ private ClassLoader findClassLoader(Class<?> clazz) {
}
return cl;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ public final class Strings {
private Strings() {
}

public static boolean notNullOrEmpty(String str) {
public static boolean notNullOrBlank(String str) {
return str != null && !str.isBlank();
}
}
19 changes: 9 additions & 10 deletions common/src/test/java/de/xab/porter/common/test/spi/SPITest.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package de.xab.porter.common.test.spi;

import de.xab.porter.api.exception.PorterException;
import de.xab.porter.common.spi.ExtensionLoader;
import de.xab.porter.common.test.spi.service.MockService;
import de.xab.porter.common.test.spi.service.UnregisteredService;
Expand All @@ -15,31 +14,31 @@
public class SPITest {
@Test
public void testExists() {
MockService impl = ExtensionLoader.getExtensionLoader().loadExtension("impl", MockService.class);
MockService impl = ExtensionLoader.getExtensionLoader(MockService.class).loadExtension("impl");
assertEquals("hello world", impl.mock());
}

@Test
public void testNotExists() {
assertThrows(PorterException.class, () ->
ExtensionLoader.getExtensionLoader().loadExtension("none", MockService.class));
assertThrows(TypeNotPresentException.class, () ->
ExtensionLoader.getExtensionLoader(MockService.class).loadExtension("none"));
}

@Test
public void testTypoImpl() {
assertThrows(PorterException.class, () ->
ExtensionLoader.getExtensionLoader().loadExtension("typo", MockService.class));
assertThrows(IllegalArgumentException.class, () ->
ExtensionLoader.getExtensionLoader(MockService.class).loadExtension("typo"));
}

@Test
public void testNoImplemented() {
assertThrows(PorterException.class, () ->
ExtensionLoader.getExtensionLoader().loadExtension("noimpl", MockService.class));
assertThrows(IllegalStateException.class, () ->
ExtensionLoader.getExtensionLoader(MockService.class).loadExtension("noimpl"));
}

@Test
public void testNoneRegistered() {
assertThrows(PorterException.class, () ->
ExtensionLoader.getExtensionLoader().loadExtension("any", UnregisteredService.class));
assertThrows(TypeNotPresentException.class, () ->
ExtensionLoader.getExtensionLoader(UnregisteredService.class).loadExtension("any"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,24 +13,24 @@ public class StringTest {
@Test
public void testNull() {
String str = null;
assertFalse(Strings.notNullOrEmpty(str));
assertFalse(Strings.notNullOrBlank(str));
}

@Test
public void testEmpty() {
String str = "";
assertFalse(Strings.notNullOrEmpty(str));
assertFalse(Strings.notNullOrBlank(str));
}

@Test
public void testNotNullOrEmpty() {
String str = "abc";
Assertions.assertTrue(Strings.notNullOrEmpty(str));
Assertions.assertTrue(Strings.notNullOrBlank(str));
}

@Test
public void testLongEmpty() {
String str = " ";
assertFalse(Strings.notNullOrEmpty(str));
assertFalse(Strings.notNullOrBlank(str));
}
}
Loading