Skip to content

Commit

Permalink
fix #447: Allow the CaffeineCachingProvider to be used with OSGi DS
Browse files Browse the repository at this point in the history
* annotated CaffeineCachingProvider to make it an OSGi service
* when running in an OSGi container, set the CaffeineCachingProvider's
classloader as the Thread context classloader for every API call on the
provided CacheManagerImpl - this makes sure that the default config is
read from the bundle; the default classloader will do a fallback to
the app classloader, allowing to override / define configurations
* added an OSGi DS test
* made the bnd build reproducible (no extra bundle headers)
  • Loading branch information
raducotescu authored and ben-manes committed Aug 18, 2020
1 parent ecd5e13 commit ac014b5
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 68 deletions.
12 changes: 2 additions & 10 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,9 @@ subprojects {
testRuntimeOnly testLibraries.osgiRuntime
}

configurations {
bundleCompile
}

sourceSets {
bundle
}

task bundle(type: aQute.bnd.gradle.Bundle) {
from sourceSets.bundle.output
sourceSet = sourceSets.bundle
from sourceSets.main.output
sourceSet = sourceSets.main
}

tasks.withType(JavaCompile) {
Expand Down
11 changes: 10 additions & 1 deletion gradle/dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ ext {
jsr330: '1',
nullaway: '0.7.9',
ohc: '0.6.1',
osgiComponentAnnotations: '1.4.0',
picocli: '4.5.0',
slf4j: '1.7.30',
tcache: '2.0.1',
Expand All @@ -73,6 +74,10 @@ ext {
paxExam: '4.13.3',
testng: '7.3.0',
truth: '0.24',
felix: '6.0.3',
felixScr: '2.1.20',
osgiUtilFunction: '1.1.0',
osgiUtilPromise: '1.1.0'
]
pluginVersions = [
apt: '0.21',
Expand Down Expand Up @@ -128,6 +133,7 @@ ext {
jsr330: "javax.inject:javax.inject:${versions.jsr330}",
nullaway: "com.uber.nullaway:nullaway:${versions.nullaway}",
ohc: "org.caffinitas.ohc:ohc-core-j8:${versions.ohc}",
osgiComponentAnnotations: "org.osgi:org.osgi.service.component.annotations:${versions.osgiComponentAnnotations}",
picocli: "info.picocli:picocli:${versions.picocli}",
slf4jNop: "org.slf4j:slf4j-nop:${versions.slf4j}",
tcache: "com.trivago:triava:${versions.tcache}",
Expand All @@ -151,10 +157,13 @@ ext {
junit: "junit:junit:${testVersions.junit}",
mockito: "org.mockito:mockito-core:${testVersions.mockito}",
osgiCompile: [
'org.apache.felix:org.apache.felix.framework:6.0.3',
"org.ops4j.pax.exam:pax-exam-junit4:${testVersions.paxExam}",
],
osgiRuntime: [
"org.apache.felix:org.apache.felix.framework:${testVersions.felix}",
"org.apache.felix:org.apache.felix.scr:${testVersions.felixScr}",
"org.osgi:org.osgi.util.function:${testVersions.osgiUtilFunction}",
"org.osgi:org.osgi.util.promise:${testVersions.osgiUtilPromise}",
"org.ops4j.pax.exam:pax-exam-container-native:${testVersions.paxExam}",
"org.ops4j.pax.exam:pax-exam-link-mvn:${testVersions.paxExam}",
"org.ops4j.pax.url:pax-url-aether:2.6.2",
Expand Down
15 changes: 10 additions & 5 deletions jcache/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ dependencies {
api libraries.jcache
api libraries.config
api libraries.jsr330
api libraries.osgiComponentAnnotations

testImplementation libraries.guava
testImplementation testLibraries.junit
Expand All @@ -32,16 +33,16 @@ dependencies {
jar.manifest {
attributes 'Bundle-SymbolicName': 'com.github.ben-manes.caffeine.jcache'
attributes 'Import-Package': [
'javax.cache.*',
'javax.management',
'com.typesafe.config',
'com.github.benmanes.caffeine.cache',
'com.github.benmanes.caffeine.cache.stats'].join(',')
'!org.checkerframework.checker.*',
'*'].join(',')
attributes 'Export-Package': [
'com.github.benmanes.caffeine.jcache.spi',
'com.github.benmanes.caffeine.jcache.copy',
'com.github.benmanes.caffeine.jcache.configuration'].join(',')
attributes 'Automatic-Module-Name': 'com.github.benmanes.caffeine.jcache'
attributes '-exportcontents': '${removeall;${packages;VERSIONED};${packages;CONDITIONAL}}'
attributes '-snapshot': 'SNAPSHOT'
attributes '-noextraheaders': true
}

task unzipJCacheJavaDoc(type: Copy, group: 'Build', description: 'Unzips the JCache JavaDoc') {
Expand Down Expand Up @@ -87,6 +88,10 @@ task osgiTests(type: Test, group: 'Build', description: 'Isolated OSGi tests') {
tasks.test.dependsOn(it)
systemProperty 'config.osgi.version', versions.config
systemProperty 'jcache.osgi.version', versions.jcache
systemProperty 'checkerAnnotations.version', versions.checkerFramework
systemProperty 'felixScr.version', testVersions.felixScr
systemProperty 'osgiUtil.function', testVersions.osgiUtilFunction
systemProperty 'osgiUtil.promise', testVersions.osgiUtilPromise
systemProperty 'caffeine.osgi.jar', project(':caffeine').jar.archivePath.path
systemProperty 'caffeine-jcache.osgi.jar', project(':jcache').jar.archivePath.path
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@

import org.checkerframework.checker.nullness.qual.Nullable;

import com.github.benmanes.caffeine.jcache.spi.CaffeineCachingProvider;

/**
* An implementation of JSR-107 {@link CacheManager} that manages Caffeine-based caches.
*
Expand All @@ -45,6 +47,7 @@ public final class CacheManagerImpl implements CacheManager {
private final CachingProvider cacheProvider;
private final Properties properties;
private final URI uri;
private final boolean runsAsAnOsgiBundle;

private volatile boolean closed;

Expand All @@ -55,6 +58,8 @@ public CacheManagerImpl(CachingProvider cacheProvider,
this.properties = requireNonNull(properties);
this.caches = new ConcurrentHashMap<>();
this.uri = requireNonNull(uri);
runsAsAnOsgiBundle =
cacheProvider instanceof CaffeineCachingProvider && ((CaffeineCachingProvider) cacheProvider).isOsgiComponent();
}

@Override
Expand All @@ -80,63 +85,90 @@ public Properties getProperties() {
@Override
public <K, V, C extends Configuration<K, V>> Cache<K, V> createCache(
String cacheName, C configuration) {
requireNotClosed();
requireNonNull(configuration);

CacheProxy<?, ?> cache = caches.compute(cacheName, (name, existing) -> {
if ((existing != null) && !existing.isClosed()) {
throw new CacheException("Cache " + cacheName + " already exists");
} else if (CacheFactory.isDefinedExternally(cacheName)) {
throw new CacheException("Cache " + cacheName + " is configured externally");
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
if (runsAsAnOsgiBundle) {
// override the context class loader with the CachingManager's classloader
Thread.currentThread().setContextClassLoader(getClassLoader());
}
return CacheFactory.createCache(this, cacheName, configuration);
});
enableManagement(cache.getName(), cache.getConfiguration().isManagementEnabled());
enableStatistics(cache.getName(), cache.getConfiguration().isStatisticsEnabled());

@SuppressWarnings("unchecked")
Cache<K, V> castedCache = (Cache<K, V>) cache;
return castedCache;
requireNotClosed();
requireNonNull(configuration);

CacheProxy<?, ?> cache = caches.compute(cacheName, (name, existing) -> {
if ((existing != null) && !existing.isClosed()) {
throw new CacheException("Cache " + cacheName + " already exists");
} else if (CacheFactory.isDefinedExternally(cacheName)) {
throw new CacheException("Cache " + cacheName + " is configured externally");
}
return CacheFactory.createCache(this, cacheName, configuration);
});
enableManagement(cache.getName(), cache.getConfiguration().isManagementEnabled());
enableStatistics(cache.getName(), cache.getConfiguration().isStatisticsEnabled());

@SuppressWarnings("unchecked")
Cache<K, V> castedCache = (Cache<K, V>) cache;
return castedCache;
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}

@Override
public @Nullable <K, V> Cache<K, V> getCache(
String cacheName, Class<K> keyType, Class<V> valueType) {
CacheProxy<K, V> cache = getCache(cacheName);
if (cache == null) {
return null;
}
requireNonNull(keyType);
requireNonNull(valueType);

Configuration<?, ?> config = cache.getConfiguration();
if (keyType != config.getKeyType()) {
throw new ClassCastException("Incompatible cache key types specified, expected " +
config.getKeyType() + " but " + keyType + " was specified");
} else if (valueType != config.getValueType()) {
throw new ClassCastException("Incompatible cache value types specified, expected " +
config.getValueType() + " but " + valueType + " was specified");
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
if (runsAsAnOsgiBundle) {
// override the context class loader with the CachingManager's classloader
Thread.currentThread().setContextClassLoader(getClassLoader());
}
CacheProxy<K, V> cache = getCache(cacheName);
if (cache == null) {
return null;
}
requireNonNull(keyType);
requireNonNull(valueType);

Configuration<?, ?> config = cache.getConfiguration();
if (keyType != config.getKeyType()) {
throw new ClassCastException("Incompatible cache key types specified, expected " +
config.getKeyType() + " but " + keyType + " was specified");
} else if (valueType != config.getValueType()) {
throw new ClassCastException("Incompatible cache value types specified, expected " +
config.getValueType() + " but " + valueType + " was specified");
}
return cache;
} finally {
Thread.currentThread().setContextClassLoader(old);
}
return cache;
}

@Override
public <K, V> CacheProxy<K, V> getCache(String cacheName) {
requireNonNull(cacheName);
requireNotClosed();

CacheProxy<?, ?> cache = caches.computeIfAbsent(cacheName, name -> {
CacheProxy<?, ?> created = CacheFactory.tryToCreateFromExternalSettings(this, name);
if (created != null) {
created.enableManagement(created.getConfiguration().isManagementEnabled());
created.enableStatistics(created.getConfiguration().isStatisticsEnabled());
ClassLoader old = Thread.currentThread().getContextClassLoader();
try {
if (runsAsAnOsgiBundle) {
// override the context class loader with the CachingManager's classloader
Thread.currentThread().setContextClassLoader(getClassLoader());
}
return created;
});

@SuppressWarnings("unchecked")
CacheProxy<K, V> castedCache = (CacheProxy<K, V>) cache;
return castedCache;
requireNonNull(cacheName);
requireNotClosed();

CacheProxy<?, ?> cache = caches.computeIfAbsent(cacheName, name -> {
CacheProxy<?, ?> created = CacheFactory.tryToCreateFromExternalSettings(this, name);
if (created != null) {
created.enableManagement(created.getConfiguration().isManagementEnabled());
created.enableStatistics(created.getConfiguration().isStatisticsEnabled());
}
return created;
});

@SuppressWarnings("unchecked")
CacheProxy<K, V> castedCache = (CacheProxy<K, V>) cache;
return castedCache;
} finally {
Thread.currentThread().setContextClassLoader(old);
}
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,13 @@
import javax.cache.spi.CachingProvider;

import org.checkerframework.checker.nullness.qual.Nullable;
import org.osgi.service.component.annotations.Activate;
import org.osgi.service.component.annotations.Component;

import com.github.benmanes.caffeine.jcache.CacheManagerImpl;
import com.github.benmanes.caffeine.jcache.configuration.TypesafeConfigurator;
import com.google.errorprone.annotations.concurrent.GuardedBy;
import com.typesafe.config.ConfigFactory;

/**
* A provider that produces a JCache implementation backed by Caffeine. Typically this provider is
Expand All @@ -53,13 +57,16 @@
*
* @author [email protected] (Ben Manes)
*/
@Component
public final class CaffeineCachingProvider implements CachingProvider {
private static final ClassLoader DEFAULT_CLASS_LOADER = AccessController.doPrivileged(
(PrivilegedAction<ClassLoader>) JCacheClassLoader::new);

@GuardedBy("itself")
private final Map<ClassLoader, Map<URI, CacheManager>> cacheManagers;

private boolean isOsgiComponent;

public CaffeineCachingProvider() {
this.cacheManagers = new WeakHashMap<>(1);
}
Expand Down Expand Up @@ -165,11 +172,15 @@ private ClassLoader getManagerClassLoader(ClassLoader classLoader) {
*/
private static final class JCacheClassLoader extends ClassLoader {

public JCacheClassLoader() {
super(Thread.currentThread().getContextClassLoader());
}

@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
ClassNotFoundException error = null;
if (contextClassLoader != null) {
if (contextClassLoader != null && contextClassLoader != DEFAULT_CLASS_LOADER) {
try {
return contextClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
Expand All @@ -179,29 +190,43 @@ public Class<?> loadClass(String name) throws ClassNotFoundException {

ClassLoader classClassLoader = getClass().getClassLoader();
if ((classClassLoader != null) && (classClassLoader != contextClassLoader)) {
return classClassLoader.loadClass(name);
try {
return classClassLoader.loadClass(name);
} catch (ClassNotFoundException e) {
error = e;
}
}

ClassLoader parentClassLoader = getParent();
if (parentClassLoader != null && parentClassLoader != contextClassLoader && parentClassLoader != classClassLoader) {
return parentClassLoader.loadClass(name);
}
throw (error == null) ? new ClassNotFoundException(name) : error;
}

@Override
public @Nullable URL getResource(String name) {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
if (contextClassLoader != null && contextClassLoader != DEFAULT_CLASS_LOADER) {
URL resource = contextClassLoader.getResource(name);
if (resource != null) {
return resource;
}
}

ClassLoader classClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classClassLoader = getClass().getClassLoader();
if ((classClassLoader != null) && (classClassLoader != contextClassLoader)) {
URL resource = classClassLoader.getResource(name);
if (resource != null) {
return resource;
}
}

ClassLoader parentClassLoader = getParent();
if (parentClassLoader != null && parentClassLoader != contextClassLoader && parentClassLoader != classClassLoader) {
return parentClassLoader.getResource(name);
}

return null;
}

Expand All @@ -210,16 +235,33 @@ public Enumeration<URL> getResources(String name) throws IOException {
List<URL> resources = new ArrayList<>();

ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
if (contextClassLoader != null && contextClassLoader != DEFAULT_CLASS_LOADER) {
resources.addAll(Collections.list(contextClassLoader.getResources(name)));
}

ClassLoader classClassLoader = Thread.currentThread().getContextClassLoader();
ClassLoader classClassLoader = getClass().getClassLoader();
if ((classClassLoader != null) && (classClassLoader != contextClassLoader)) {
resources.addAll(Collections.list(classClassLoader.getResources(name)));
}

ClassLoader parentClassLoader = getParent();
if (parentClassLoader != null && parentClassLoader != contextClassLoader && parentClassLoader != classClassLoader) {
resources.addAll(Collections.list(parentClassLoader.getResources(name)));
}

return Collections.enumeration(resources);
}
}

@Activate
@SuppressWarnings("unused")
private void activate() {
isOsgiComponent = true;
TypesafeConfigurator.setConfigSource(() -> ConfigFactory.load(DEFAULT_CLASS_LOADER));
}

public boolean isOsgiComponent() {
return isOsgiComponent;
}

}
Loading

0 comments on commit ac014b5

Please sign in to comment.