Hibernate ByteBuddy + GraalVM Native Image with Spring Boot (Full Workaround)

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:

  1. hibernate.properties with bytecode.provider=none — Hibernate reads the property, but AggregatedServiceLoader runs independently and still loads ByteBuddy.

  2. System.setProperty("hibernate.bytecode.provider", "none") in main() — Same issue. ServiceLoader runs before the property is evaluated.

  3. META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider override file — Hibernate’s AggregatedServiceLoader scans ALL JARs on the classpath. It finds the ByteBuddy provider in hibernate-core.jar in addition to our override, then tries to instantiate both.

  4. Excluding net.bytebuddy from Gradle dependencieshibernate-core.jar bundles BytecodeProviderImpl internally. The class still references ByteBuddy classes, causing ClassNotFoundException on JVM or NoSuchMethodException in native image.

  5. GraalVM substitution class (@TargetClass / @Substitute) — Method signature mismatch between our substitution and Hibernate 6.5.3’s internal BytecodeProviderInitiator.

  6. hibernate-graalvm dependency — Did not resolve the ServiceLoader loading behavior.

  7. Hibernate enhance Gradle plugin alone — Handles build-time bytecode enhancement but does not prevent ServiceLoader from loading ByteBuddy at runtime.

  8. -H:ExcludeResources=META-INF/services/org.hibernate.bytecode.spi.BytecodeProvider — Did not prevent Hibernate’s classpath scanning from finding the provider.

  9. Spring Boot AOT substitution — Spring Framework has a built-in substitution in SpringHibernateJpaPersistenceProvider that sets bytecode.provider=none when NativeDetector.inNativeImage() returns true. This did not work in our setup, likely because our custom native-image.gradle configuration bypassed parts of Spring’s AOT processing pipeline.

  10. Upgrading Spring Boot to 3.5.9 / Hibernate to 6.6.11 — The same AggregatedServiceLoader behavior 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.

Related Issues

  • HHH-19530 — Move ByteBuddy to separate artifact

  • HHH-17643 — Original discussion on disabling ByteBuddy

  • This workaround is necessary until Hibernate provides a clean way to exclude the ByteBuddy BytecodeProvider from ServiceLoader scanning.

I’m glad you found a way to make it work in your environment, but just like I wrote in other topics about this matter already, you can use GraalVM mechanisms to substitute the BytecodeProvider. This works fine for Quarkus, so maybe your setup just does something weird. If Spring AOT is supposed to make this work already and you admit that your setup somehow bypasses that, then it’s just a problem in your build and you should probably spend time on fixing that instead of replacing files in the hibernate-core.jar.

I described here how I could workaround the same problem. But I didn’t patch hiberate-core.jar, but built the uber-jar, provided no-op bytecode provider in my app (which then overrides the hibernate-core.jar ByteBuddy implementation). Alternatively you can filter out the ByteBuddy service loader in the META-INF of the uber-jar with maven plugin. And then I used the uber-jar to build GraalVM Native Image instead of the classpath. I also used this approach when also using Spring Boot (4) with JPA (and Hibernate) on AWS Lambda and have some examples in my repo for it. The same works also without AWS Lambda.

But I’m glad that you also found the solution.

But I still think that the Hibernate’s AggregatedServiceLoader doesn’t work correctly or at least there is no clear way to explicitly configure the no-op implementation without doing dirty workarounds or magic that frameworks like Quarkus “somehow” do.