graalvm-reachability-metadata icon indicating copy to clipboard operation
graalvm-reachability-metadata copied to clipboard

ClassNotFoundException with Caffeine and Spring Boot 3.2

Open sleicht opened this issue 1 year ago • 3 comments

Hello! I use spring boot (v. 3.2.1) with com.github.ben-manes.caffeine (v. 3.1.8). When I make native-image with default configuration of CaffeineCacheManager, I get this error:

Caused by: java.lang.IllegalStateException: SSSW
        at com.github.benmanes.caffeine.cache.LocalCacheFactory.newFactory(LocalCacheFactory.java:114) ~[microservice:3.1.8]
        at [email protected]/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[microservice:na]
        at com.github.benmanes.caffeine.cache.LocalCacheFactory.loadFactory(LocalCacheFactory.java:97) ~[microservice:3.1.8]
        at com.github.benmanes.caffeine.cache.LocalCacheFactory.newBoundedLocalCache(LocalCacheFactory.java:46) ~[microservice:3.1.8]
        at com.github.benmanes.caffeine.cache.BoundedLocalCache$BoundedLocalManualCache.<init>(BoundedLocalCache.java:3953) ~[microservice:3.1.8]
        at com.github.benmanes.caffeine.cache.BoundedLocalCache$BoundedLocalLoadingCache.<init>(BoundedLocalCache.java:4451) ~[na:na]
        at com.github.benmanes.caffeine.cache.Caffeine.build(Caffeine.java:1073) ~[microservice:3.1.8]
        at com.COMPANY.libraries.auth.service.KeycloakService.<init>(KeycloakService.java:72) ~[microservice:na]
        at com.COMPANY.libraries.auth.service.KeycloakService__BeanDefinitions.lambda$getKeycloakServiceInstanceSupplier$0(KeycloakService__BeanDefinitions.java:19) ~[na:na]
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:68) ~[microservice:6.1.2]
        at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:54) ~[microservice:6.1.2]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$get$2(BeanInstanceSupplier.java:206) ~[na:na]
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:58) ~[microservice:6.1.2]
        at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:46) ~[microservice:6.1.2]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.invokeBeanSupplier(BeanInstanceSupplier.java:214) ~[na:na]
        at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:206) ~[na:na]
        at org.springframework.beans.factory.support.DefaultListableBeanFactory.obtainInstanceFromSupplier(DefaultListableBeanFactory.java:949) ~[microservice:6.1.2]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1216) ~[microservice:6.1.2]
        ... 18 common frames omitted
Caused by: java.lang.ClassNotFoundException: com.github.benmanes.caffeine.cache.SSSW
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:122) ~[na:na]
        at org.graalvm.nativeimage.builder/com.oracle.svm.core.hub.ClassForNameSupport.forName(ClassForNameSupport.java:86) ~[na:na]
        at [email protected]/java.lang.Class.forName(DynamicHub.java:1346) ~[microservice:na]
        at [email protected]/java.lang.Class.forName(DynamicHub.java:1335) ~[microservice:na]
        at [email protected]/java.lang.invoke.MethodHandles$Lookup.findClass(MethodHandles.java:2869) ~[microservice:na]
        at com.github.benmanes.caffeine.cache.LocalCacheFactory.newFactory(LocalCacheFactory.java:104) ~[microservice:3.1.8]
        ... 35 common frames omitted

I found similar problem here

If I understand this repo correct, it misses the metadata for Caffeine for version 3.1.8 and it misses class com.github.benmanes.caffeine.cache.SSSW

The application starts if I add following hint manually:

hints.reflection().registerType(
    TypeReference.of("com.github.benmanes.caffeine.cache.SSSW"),
    MemberCategory.PUBLIC_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS

This additional CaffeineTest leads to the same exception:

    @Test
    void testRecordStats() {
        LoadingCache<String, String> cache = Caffeine.newBuilder()
            .expireAfterWrite(Duration.ofMinutes(20))
            .recordStats()
            .build(key -> key.equals("Hello") ? "World" : "Universe");
        assertThat(cache.get("Hello")).isEqualTo("World");
        assertThat(cache.getAll(List.of("Hi", "Aloha"))).isEqualTo(Map.of("Hi", "Universe", "Aloha", "Universe"));
    }

sleicht avatar Jan 17 '24 13:01 sleicht

Therefore missing in reflect-config.json:


  {
    "condition": {
      "typeReachable": "com.github.benmanes.caffeine.cache.BoundedLocalCache$BoundedLocalLoadingCache"
    },
    "name": "com.github.benmanes.caffeine.cache.SSSW",
    "methods": [
      {
        "name": "<init>",
        "parameterTypes": [
          "com.github.benmanes.caffeine.cache.Caffeine",
          "com.github.benmanes.caffeine.cache.AsyncCacheLoader",
          "boolean"
        ]
      }
    ]
  }

sleicht avatar Jan 17 '24 14:01 sleicht

SSW is not the only class, I received the same error on com.github.benmanes.caffeine.cache.SSMSA. I bet there are a ton of classes to include here as Caffeine is building the class name dynamically based on cache parameters.

bpfoster avatar Feb 06 '24 19:02 bpfoster

@bpfoster yeah, pre-AOT this was a neat trick because unloaded classes are basically free (just a little extra disk space). Thus, we could code generate the cache and the entry classes to minimize the memory footprint, e.g. only include the expiration timestamp if used. Using reflection avoids bloating the constant pool with a static mapping and that factory is cached making the cost a one-time class load followed by a map lookup and direct call to the constructor. All extremely cheap, hidden, classic JVM dynamism except now AOT peeks into the implementation and exposes this magic. There's not much we can do since it is breaking encapsulation and including all classes would bloat your native binary. I'm curious to see how Project Leyden will address this, as it might allow AOT for the main parts while not throwing out the JIT for dynamic parts.

You can try this example if you want to use the agent to discover the classes (it will output the reflect-config.json which you can copy into your project).

ben-manes avatar Feb 07 '24 02:02 ben-manes