Environment
-
Spring Boot 3.3.11 (also tested with 3.5.9)
-
Hibernate ORM 6.5.3.Final (also confirmed on 6.6.11.Final)
-
GraalVM JDK 17.0.12 / 17.0.18
-
Gradle 7.6
-
Spring Cloud Stream with Kafka + Avro
-
Hibernate Spatial with PostGIS
-
PostgreSQL 17.5
-
Docker (multi-stage build, Linux ARM64)
The Problem
When compiling a Spring Boot microservice with Hibernate/JPA to a GraalVM native image, the application fails at runtime with:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'entityManagerFactory':
org.hibernate.bytecode.spi.BytecodeProvider: org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl
Unable to get public no-arg constructor
Root cause: Hibernate’s AggregatedServiceLoader always loads the ByteBuddy BytecodeProviderImpl via classpath scanning, regardless of any configuration. ByteBuddy then tries to generate classes at runtime using ClassLoader.defineClass(), which GraalVM native image does not support (closed-world assumption).
What Did NOT Work
We spent two full days trying every approach documented online. None of these resolved the issue:
-
hibernate.propertieswithbytecode.provider=none— Hibernate reads the property, butAggregatedServiceLoaderruns independently and still loads ByteBuddy. -
System.setProperty("hibernate.bytecode.provider", "none")inmain()— Same issue. ServiceLoader runs before the property is evaluated. -
META-INF/services/org.hibernate.bytecode.spi.BytecodeProvideroverride file — Hibernate’sAggregatedServiceLoaderscans ALL JARs on the classpath. It finds the ByteBuddy provider inhibernate-core.jarin addition to our override, then tries to instantiate both. -
Excluding
net.bytebuddyfrom Gradle dependencies —hibernate-core.jarbundlesBytecodeProviderImplinternally. The class still references ByteBuddy classes, causingClassNotFoundExceptionon JVM orNoSuchMethodExceptionin native image. -
GraalVM substitution class (
@TargetClass/@Substitute) — Method signature mismatch between our substitution and Hibernate 6.5.3’s internalBytecodeProviderInitiator. -
hibernate-graalvmdependency — Did not resolve the ServiceLoader loading behavior. -
Hibernate enhance Gradle plugin alone — Handles build-time bytecode enhancement but does not prevent ServiceLoader from loading ByteBuddy at runtime.
-
-H:ExcludeResources=META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider— Did not prevent Hibernate’s classpath scanning from finding the provider. -
Spring Boot AOT substitution — Spring Framework has a built-in substitution in
SpringHibernateJpaPersistenceProviderthat setsbytecode.provider=nonewhenNativeDetector.inNativeImage()returns true. This did not work in our setup, likely because our customnative-image.gradleconfiguration bypassed parts of Spring’s AOT processing pipeline. -
Upgrading Spring Boot to 3.5.9 / Hibernate to 6.6.11 — The same
AggregatedServiceLoaderbehavior exists in all Hibernate 6.x and 7.x versions. The upgrade alone did not fix the issue.
What Actually Worked
The solution is to physically remove the ByteBuddy ServiceLoader registration from hibernate-core.jar before native image compilation. This is done inside the Docker build.
This approach was inspired by @Vadym_Kazulkin’s post in this thread, where he successfully patched hibernate-core.jar and deployed to AWS Lambda.
Step-by-Step Solution
Step 1: Update the Dockerfile
Add zip to the build dependencies and split the Gradle build into two phases: first compile (which downloads dependencies), then patch hibernate-core.jar, then native compile.
FROM --platform=linux/aarch64 ubuntu:22.04 AS graalvm-builder
RUN apt-get update && \
apt-get install -y --no-install-recommends \
curl wget python3 unzip zip \
build-essential libz-dev ca-certificates git \
&& rm -rf /var/lib/apt/lists/*
RUN curl -L -o /tmp/graalvm.tar.gz \
https://download.oracle.com/graalvm/17/archive/graalvm-jdk-17.0.12_linux-aarch64_bin.tar.gz \
&& mkdir -p /opt/graalvm \
&& tar -xzf /tmp/graalvm.tar.gz -C /opt/graalvm --strip-components=1 \
&& rm /tmp/graalvm.tar.gz
ENV GRAALVM_HOME=/opt/graalvm
ENV JAVA_HOME=$GRAALVM_HOME
ENV PATH=$GRAALVM_HOME/bin:$PATH
ARG SERVICE_MODULE
ARG SERVICE_NAME
WORKDIR /nup-dev
COPY ./ ./
# Phase 1: Compile to download dependencies
# Phase 2: Patch hibernate-core.jar to remove ByteBuddy ServiceLoader
# Phase 3: Native compile
RUN cd modules/${SERVICE_MODULE} && \
./gradlew clean compileJava --no-daemon && \
find /root/.gradle/caches -name "hibernate-core-*.jar" -exec \
zip -d {} "META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider" \; 2>/dev/null; \
./gradlew nativeCompile --no-daemon
Step 2: Add ServiceLoader Override File
Create src/main/resources/META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider containing:
org.hibernate.bytecode.internal.none.BytecodeProviderImpl
Step 3: Add System Property in Application Main
public static void main(String[] args) {
System.setProperty("hibernate.bytecode.provider", "none");
SpringApplication.run(Application.class, args);
}
Step 4: Add Reflect Config Entry
Add this to src/main/resources/META-INF/native-image/reflect-config.json:
{
"name": "org.hibernate.bytecode.internal.none.BytecodeProviderImpl",
"allDeclaredConstructors": true,
"allDeclaredMethods": true
}
Step 5: Run the GraalVM Tracing Agent (per service)
Before building the native image, run the tracing agent on each service to generate complete reflection/proxy/resource metadata:
java -agentlib:native-image-agent=config-output-dir=src/main/resources/META-INF/native-image \
-jar build/libs/your-service.jar
Let it run for a minute, then Ctrl+C. This generates the reflect-config.json, proxy-config.json, etc.
Step 6: Build and Test
docker build \
--build-arg SERVICE_MODULE=your-module \
--build-arg SERVICE_NAME=your-binary-name \
-f Dockerfile.native \
-t your-image:native .
Why This Works
The zip -d command physically removes the META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider file from the cached hibernate-core.jar inside the Docker build container. With that file gone, Hibernate’s AggregatedServiceLoader can no longer find the ByteBuddy provider during classpath scanning. It falls back to the none provider registered by our override file.
The System.setProperty and ServiceLoader override file provide belt-and-suspenders redundancy. The reflect config entry ensures GraalVM includes the none provider class in the native image.
Confirmed Working
We have successfully compiled and deployed 13 Spring Boot microservices as GraalVM native images using this approach, including services with:
-
Hibernate Spatial + PostGIS
-
Spring Cloud Stream + Kafka + Avro
-
Spring Security OAuth2
-
HikariCP connection pooling
All services start in under 1 second as native images versus 3+ seconds on JVM.