Skip to content

Commit

Permalink
chore: cache SPI extension class (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
arjenzhou authored Jul 25, 2021
1 parent 199c15c commit f9ab562
Show file tree
Hide file tree
Showing 11 changed files with 150 additions and 143 deletions.
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

0 comments on commit f9ab562

Please sign in to comment.