ServiceLoaders are now cached.

The first ServiceLoaders found when looking up a class will be cached. This cache can be reset by calling `Services.reload()`, to help
facilitate testing or instances where a classpaths are dynamically changed at runtime.

Fixes: #752
This commit is contained in:
Brian Demers 2023-09-15 12:59:50 -04:00 committed by Brian Demers
parent a920163be4
commit 7805e08bff
2 changed files with 52 additions and 22 deletions

View File

@ -21,6 +21,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import static io.jsonwebtoken.lang.Collections.arrayToList;
@ -30,6 +32,8 @@ import static io.jsonwebtoken.lang.Collections.arrayToList;
*/
public final class Services {
private static ConcurrentMap<Class<?>, ServiceLoader<?>> SERVICE_CACHE = new ConcurrentHashMap<>();
private static final List<ClassLoaderAccessor> CLASS_LOADER_ACCESSORS = arrayToList(new ClassLoaderAccessor[] {
new ClassLoaderAccessor() {
@Override
@ -64,25 +68,19 @@ public final class Services {
public static <T> List<T> loadAll(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) {
List<T> implementations = loadAll(spi, classLoaderAccessor.getClassLoader());
if (!implementations.isEmpty()) {
return Collections.unmodifiableList(implementations);
ServiceLoader<T> serviceLoader = serviceLoader(spi);
if (serviceLoader != null) {
List<T> implementations = new ArrayList<>();
for (T implementation : serviceLoader) {
implementations.add(implementation);
}
return implementations;
}
throw new UnavailableImplementationException(spi);
}
private static <T> List<T> loadAll(Class<T> spi, ClassLoader classLoader) {
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi, classLoader);
List<T> implementations = new ArrayList<>();
for (T implementation : serviceLoader) {
implementations.add(implementation);
}
return implementations;
}
/**
* Loads the first available implementation the given SPI class from the classpath. Uses the {@link ServiceLoader}
* to find implementations. When multiple implementations are available it will return the first one that it
@ -96,23 +94,49 @@ public final class Services {
public static <T> T loadFirst(Class<T> spi) {
Assert.notNull(spi, "Parameter 'spi' must not be null.");
for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) {
T result = loadFirst(spi, classLoaderAccessor.getClassLoader());
if (result != null) {
return result;
}
ServiceLoader<T> serviceLoader = serviceLoader(spi);
if (serviceLoader != null) {
return serviceLoader.iterator().next();
}
throw new UnavailableImplementationException(spi);
}
private static <T> T loadFirst(Class<T> spi, ClassLoader classLoader) {
ServiceLoader<T> serviceLoader = ServiceLoader.load(spi, classLoader);
if (serviceLoader.iterator().hasNext()) {
return serviceLoader.iterator().next();
/**
* Returns a ServiceLoader for <code>spi</code> class, checking multiple classloaders. The ServiceLoader
* will be cached if it contains at least one implementation of the <code>spi</code> class.<BR>
*
* <b>NOTE:</b> Only the first Serviceloader will be cached.
* @param spi The interface or abstract class representing the service loader.
* @return A service loader, or null if no implementations are found
* @param <T> The type of the SPI.
*/
private static <T> ServiceLoader<T> serviceLoader(Class<T> spi) {
// TODO: JDK8, replace this get/putIfAbsent logic with ConcurrentMap.computeIfAbsent
ServiceLoader<T> serviceLoader = (ServiceLoader<T>) SERVICE_CACHE.get(spi);
if (serviceLoader != null) {
return serviceLoader;
}
for (ClassLoaderAccessor classLoaderAccessor : CLASS_LOADER_ACCESSORS) {
serviceLoader = ServiceLoader.load(spi, classLoaderAccessor.getClassLoader());
if (serviceLoader.iterator().hasNext()) {
SERVICE_CACHE.putIfAbsent(spi, serviceLoader);
return serviceLoader;
}
}
return null;
}
/**
* Clears internal cache of ServiceLoaders. This is useful when testing, or for applications that dynamically
* change classloaders.
*/
public static void reload() {
SERVICE_CACHE.clear();
}
private interface ClassLoaderAccessor {
ClassLoader getClassLoader();
}

View File

@ -17,6 +17,7 @@ package io.jsonwebtoken.impl.lang
import io.jsonwebtoken.StubService
import io.jsonwebtoken.impl.DefaultStubService
import org.junit.After
import org.junit.Test
import org.junit.runner.RunWith
import org.powermock.api.easymock.PowerMock
@ -73,6 +74,11 @@ class ServicesTest {
assertEquals(ClassLoader.getSystemClassLoader(), accessorList.get(2).getClassLoader())
}
@After
void resetCache() {
Services.reload();
}
static class NoServicesClassLoader extends ClassLoader {
private NoServicesClassLoader(ClassLoader parent) {
super(parent)