javaspring-bootehcachegraalvm

Error when trying to run Java 17 - Spring Boot 3 project with ehcache using GraalVM native image


I'm trying to run an app with Java 17 and Spring Boot 3 using GraalVM native image support. It's a very simple app with just a GET endpoint. The app uses ehcache and Spring Boot cache to cache the result of one method. More specifically, I'm using the following Maven dependencies for cache:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <version>3.10.8</version>
</dependency>

and here is my ehcache config which seems to be the problem:

import java.time.Duration;
import javax.cache.CacheManager;
import javax.cache.Caching;
import org.ehcache.config.builders.CacheConfigurationBuilder;
import org.ehcache.config.builders.ExpiryPolicyBuilder;
import org.ehcache.config.builders.ResourcePoolsBuilder;
import org.ehcache.config.units.MemoryUnit;
import org.ehcache.jsr107.Eh107Configuration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@EnableCaching
@Configuration
public class EhcacheConfiguration {

  @Bean
  public CacheManager EhcacheManager() {
    var cachingProvider = Caching.getCachingProvider();
    var cacheManager = cachingProvider.getCacheManager();

    cacheManager.createCache("myTestOne",
        Eh107Configuration.fromEhcacheCacheConfiguration(
            CacheConfigurationBuilder
                .newCacheConfigurationBuilder(Integer.class,
                    MyObject.class,
                    ResourcePoolsBuilder.newResourcePoolsBuilder()
                        .offheap(100, MemoryUnit.MB)
                        .build())
                .withExpiry(ExpiryPolicyBuilder.timeToIdleExpiration(Duration.ofHours(23)))
                .build()
        )
    );

    return cacheManager;
  }

}

also here is the part where I declare the spring native maven plugin:

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <executions>
        <execution>
            <id>build-native</id>
            <goals>
                <goal>compile-no-fork</goal>
            </goals>
            <phase>package</phase>
        </execution>
    </executions>
</plugin>

and I'm trying to build and run the app using docker-compose, here is the Dockerfile:

FROM ghcr.io/graalvm/graalvm-ce:ol7-java17-22.3.0 as build

RUN gu install native-image

RUN yum install -y zip unzip libstdc++-static freetype-devel

WORKDIR /app

COPY mvnw .
COPY .mvn .mvn

COPY pom.xml .

RUN chmod +x ./mvnw

COPY src src

RUN ./mvnw clean package -DskipTests -Pnative

ENTRYPOINT ["sh", "-c" ,"./target/test-app-one"]

and here is the docker-compose.yml file:

services:
  app-server:
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    restart: always

I'm trying to start the docker-compose.yml by the following command:

docker-compose up --build

the build will finish successfully, but when it will try to start the app (on the last step of the Dockerfile), it will produce the following error. Here is the full log:

cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.554Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.554Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.554Z  INFO 1 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.5]
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.559Z  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.559Z  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 39 ms
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.568Z  WARN 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh a
ttempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'EhcacheManager': Instantiation of supplied bean failed
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.569Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
cache-test-app-one-app-server-1  | 2023-01-22T14:21:04.570Z ERROR 1 --- [           main] o.s.boot.SpringApplication               : Application run failed
cache-test-app-one-app-server-1  | 
cache-test-app-one-app-server-1  | org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'EhcacheManager': Instantiation of supplied bean failed
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1236) ~[test-app-one
:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainFromSupplier(AbstractAutowireCapableBeanFactory.java:1210) ~[test-app-one:6.0.4] 
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1157) ~[test-app-one:6.0.4] 
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:561) ~[test-app-one:6.0.4]        
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:915) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291) ~[test-app-one:3.0.2]
cache-test-app-one-app-server-1  |      at com.test.testappone.TestAppOneApplication.main(TestAppOneApplication.java:10) ~[test-app-one:na]
cache-test-app-one-app-server-1  | Caused by: java.lang.IllegalArgumentException: The serializer: org.ehcache.impl.serialization.PlainJavaSerializer does not have a constructor that takes in a ClassLoader.  
cache-test-app-one-app-server-1  |      at org.ehcache.impl.config.serializer.DefaultSerializationProviderConfiguration.addSerializerFor(DefaultSerializationProviderConfiguration.java:99) ~[test-app-one:3.10
.8]
cache-test-app-one-app-server-1  |      at org.ehcache.impl.config.serializer.DefaultSerializationProviderConfiguration.addSerializerFor(DefaultSerializationProviderConfiguration.java:75) ~[test-app-one:3.10
.8]
cache-test-app-one-app-server-1  |      at org.ehcache.jsr107.EhcacheCachingProvider.createCacheManager(EhcacheCachingProvider.java:148) ~[test-app-one:3.10.8]
cache-test-app-one-app-server-1  |      at org.ehcache.jsr107.EhcacheCachingProvider.getCacheManager(EhcacheCachingProvider.java:134) ~[test-app-one:3.10.8]
cache-test-app-one-app-server-1  |      at org.ehcache.jsr107.EhcacheCachingProvider.getCacheManager(EhcacheCachingProvider.java:85) ~[test-app-one:3.10.8]
cache-test-app-one-app-server-1  |      at org.ehcache.jsr107.EhcacheCachingProvider.getCacheManager(EhcacheCachingProvider.java:194) ~[test-app-one:3.10.8]
cache-test-app-one-app-server-1  |      at org.ehcache.jsr107.EhcacheCachingProvider.getCacheManager(EhcacheCachingProvider.java:202) ~[test-app-one:3.10.8]
cache-test-app-one-app-server-1  |      at com.test.testappone.EhcacheConfiguration.EhcacheManager(EhcacheConfiguration.java:22) ~[test-app-one:na]
cache-test-app-one-app-server-1  |      at com.test.testappone.EhcacheConfiguration$$SpringCGLIB$$0.CGLIB$EhcacheManager$0(<generated>) ~[test-app-one:na]
cache-test-app-one-app-server-1  |      at com.test.testappone.EhcacheConfiguration$$SpringCGLIB$$2.invoke(<generated>) ~[test-app-one:na]
cache-test-app-one-app-server-1  |      at org.springframework.cglib.proxy.MethodProxy.invokeSuper(MethodProxy.java:258) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.context.annotation.ConfigurationClassEnhancer$BeanMethodInterceptor.intercept(ConfigurationClassEnhancer.java:331) ~[na:na]
cache-test-app-one-app-server-1  |      at com.test.testappone.EhcacheConfiguration$$SpringCGLIB$$0.EhcacheManager(<generated>) ~[test-app-one:na]
cache-test-app-one-app-server-1  |      at com.test.testappone.EhcacheConfiguration__BeanDefinitions.lambda$getEhcacheManagerInstanceSupplier$0(EhcacheConfiguration__BeanDefinitions.java:30) ~[na:na]        
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:63) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingFunction.apply(ThrowingFunction.java:51) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$withGenerator$0(BeanInstanceSupplier.java:173) ~[na:na]
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:68) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingBiFunction.apply(ThrowingBiFunction.java:54) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.aot.BeanInstanceSupplier.lambda$get$2(BeanInstanceSupplier.java:208) ~[na:na]
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:59) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.util.function.ThrowingSupplier.get(ThrowingSupplier.java:47) ~[test-app-one:6.0.4]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.aot.BeanInstanceSupplier.invokeBeanSupplier(BeanInstanceSupplier.java:220) ~[na:na]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.aot.BeanInstanceSupplier.get(BeanInstanceSupplier.java:208) ~[na:na]
cache-test-app-one-app-server-1  |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.obtainInstanceFromSupplier(AbstractAutowireCapableBeanFactory.java:1225) ~[test-app-one
:6.0.4]
cache-test-app-one-app-server-1  |      ... 18 common frames omitted

if I try to remove the caching maven dependencies and the caching functionality, the app will run and start just fine. It will also run fine as it is now if I don't run it as native image.

Here you can find the full code on my GitHub repository: https://github.com/ndil101/cache-test-app-one


Solution

  • Thanks to @grekier for suggesting to try the GraalVM native image tracing agent. Firstly I changed that part of code:

    <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
        <executions>
            <execution>
                <id>build-native</id>
                <goals>
                    <goal>compile-no-fork</goal>
                </goals>
                <phase>package</phase>
            </execution>
        </executions>
    </plugin>
    

    into that:

    <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
    </plugin>
    

    Then I run locally (on my host, not inside the docker container) the folllowing command:

    ./mvnw clean package -DskipTests -Pnative
    

    Then I run the following command:

    java -Dspring.aot.enabled=true -agentlib:native-image-agent=config-output-dir=/myPathOne -jar target/test-app-one-1.0.0.jar
    

    and while the app was running I tested it by calling the simple one endpoint that it has. After that I closed the test-app-one app via CTRL+C. After that, I checked on my directory: /myPathOne (that I declaed) and there were some .json files. I took all those .json files and copied them on my project, on the following path: src/main/resources/META-INF/native-image After that, I reverted my code, into:

    <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
        <executions>
            <execution>
                <id>build-native</id>
                <goals>
                    <goal>compile-no-fork</goal>
                </goals>
                <phase>package</phase>
            </execution>
        </executions>
    </plugin>
    

    and then I run the docker-compose.yml file, by:

    docker-compose up --build
    

    and then the app started correctly without producing any error. The endpoint also works ok.

    I updated my GitHub repository with the new .json files that make the app work.