spring-projects / spring-ai

An Application Framework for AI Engineering
https://docs.spring.io/spring-ai/reference/index.html
Apache License 2.0
3.22k stars 818 forks source link

native compilation under java 22 fails for an example that works on java 21 #494

Open joshlong opened 7 months ago

joshlong commented 7 months ago

it's not critical or anything but we should probably understand it..

code:

package com.example.service;

import org.springframework.ai.chat.ChatClient;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.function.RouterFunction;
import org.springframework.web.servlet.function.ServerResponse;

import java.util.Map;

import static org.springframework.web.servlet.function.RouterFunctions.route;
import static org.springframework.web.servlet.function.ServerResponse.ok;

@SpringBootApplication
public class ServiceApplication {

    public static void main(String[] args) {
        SpringApplication.run(ServiceApplication.class, args);
    }

    @Bean
    RouterFunction<ServerResponse> storyRoutes(ChatClient ai ) {
        var prompt = """
                tell me a joke
                """;
        return route()
            .GET("/story", r -> ok().body(Map.of("story", ai.call(prompt))))
            .build();
    }
}

build:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.4'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'org.graalvm.buildtools.native' version '0.9.28'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '21'
}

repositories {
    mavenCentral()
    maven { url 'https://repo.spring.io/milestone' }
}

ext {
    set('springAiVersion', "0.8.1")
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.ai:spring-ai-vertex-ai-gemini-spring-boot-starter'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

dependencyManagement {
    imports {
        mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}"
    }
}

tasks.named('test') {
    useJUnitPlatform()
}

error


➜  service git:(main) ./gradlew nativeCompile  | pbcopy 
Mar 21, 2024 6:20:20 PM com.google.api.gax.nativeimage.NativeImageUtils registerClassForReflection
WARNING: Failed to find io.grpc.netty.shaded.io.netty.channel.ProtocolNegotiators on the classpath for reflection.
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
Error: Unsupported features in 2 methods
Detailed message:
Error: An object of type 'org.slf4j.helpers.NOP_FallbackServiceProvider' was found in the image heap. This type, however, is marked for initialization at image run time for the following reason: classes are initialized at run time by default.
This is not allowed for correctness reasons: All objects that are stored in the image heap must be initialized at build time.

You now have two options to resolve this:

1) If it is intended that objects of type 'org.slf4j.helpers.NOP_FallbackServiceProvider' are persisted in the image heap, add 

    '--initialize-at-build-time=org.slf4j.helpers.NOP_FallbackServiceProvider'

to the native-image arguments. Note that initializing new types can store additional objects to the heap. It is advised to check the static fields of 'org.slf4j.helpers.NOP_FallbackServiceProvider' to see if they are safe for build-time initialization,  and that they do not contain any sensitive data that should not become part of the image.

2) If these objects should not be stored in the image heap, you can use 

    '--trace-object-instantiation=org.slf4j.helpers.NOP_FallbackServiceProvider'

to find classes that instantiate these objects. Once you found such a class, you can mark it explicitly for run time initialization with 

    '--initialize-at-run-time=<culprit>'

to prevent the instantiation of the object.

If you are seeing this message after upgrading to a new GraalVM release, this means that some objects ended up in the image heap without their type being marked with --initialize-at-build-time.
To fix this, include '--initialize-at-build-time=org.slf4j.helpers.NOP_FallbackServiceProvider' in your configuration. If the classes do not originate from your code, it is advised to update all library or framework dependencies to the latest version before addressing this error.

The following detailed trace displays from which field in the code the object was reached.
Trace: Object was reached by
  reading static field org.slf4j.LoggerFactory.NOP_FALLBACK_SERVICE_PROVIDER
    at <unknown-location>
  registered as read because: null
Error: An object of type 'org.slf4j.helpers.SubstituteServiceProvider' was found in the image heap. This type, however, is marked for initialization at image run time for the following reason: classes are initialized at run time by default.
This is not allowed for correctness reasons: All objects that are stored in the image heap must be initialized at build time.

You now have two options to resolve this:

1) If it is intended that objects of type 'org.slf4j.helpers.SubstituteServiceProvider' are persisted in the image heap, add 

    '--initialize-at-build-time=org.slf4j.helpers.SubstituteServiceProvider'

to the native-image arguments. Note that initializing new types can store additional objects to the heap. It is advised to check the static fields of 'org.slf4j.helpers.SubstituteServiceProvider' to see if they are safe for build-time initialization,  and that they do not contain any sensitive data that should not become part of the image.

2) If these objects should not be stored in the image heap, you can use 

    '--trace-object-instantiation=org.slf4j.helpers.SubstituteServiceProvider'

to find classes that instantiate these objects. Once you found such a class, you can mark it explicitly for run time initialization with 

    '--initialize-at-run-time=<culprit>'

to prevent the instantiation of the object.

If you are seeing this message after upgrading to a new GraalVM release, this means that some objects ended up in the image heap without their type being marked with --initialize-at-build-time.
To fix this, include '--initialize-at-build-time=org.slf4j.helpers.SubstituteServiceProvider' in your configuration. If the classes do not originate from your code, it is advised to update all library or framework dependencies to the latest version before addressing this error.

The following detailed trace displays from which field in the code the object was reached.
Trace: Object was reached by
  reading static field org.slf4j.LoggerFactory.SUBST_PROVIDER
    at <unknown-location>
  registered as read because: null

FAILURE: Build failed with an exception.

* What went wrong:
Execution failed for task ':nativeCompile'.
> Process 'command '/Users/jlong/.sdkman/candidates/java/22-graalce/bin/native-image'' finished with non-zero exit value 1

* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.
> Get more help at https://help.gradle.org.

BUILD FAILED in 30s
joshlong commented 7 months ago

it breaks similarly with a Maven pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.4</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>22</java.version>
        <spring-ai.version>0.8.1</spring-ai.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.ai</groupId>
            <artifactId>spring-ai-vertex-ai-gemini-spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.ai</groupId>
                <artifactId>spring-ai-bom</artifactId>
                <version>${spring-ai.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
    <repositories>
        <repository>
            <id>spring-milestones</id>
            <name>Spring Milestones</name>
            <url>https://repo.spring.io/milestone</url>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
        </repository>
    </repositories>

</project>
fniephaus commented 7 months ago

When you add --verbose to the native-image build args, you can see the expanded list of arguments and their origin. Here's an excerpt for your reproducer:

...
'-H:ReflectionConfigurationResources@jar:file:///home/fniephaus/.gradle/caches/modules-2/files-2.1/io.netty/netty-codec/4.1.107.Final/a1d32debf2ed07c5852ab5b2904c43adb76c39e/netty-codec-4.1.107.Final.jar!/META-INF/native-image/io.netty/netty-codec/generated/handlers/reflect-config.json+api=META-INF/native-image/io.netty/netty-codec/generated/handlers/reflect-config.json' \
'-H:EnableURLProtocols@jar:file:///home/fniephaus/.gradle/caches/modules-2/files-2.1/com.google.api/gax/2.45.0/78ce52ef9330a8093c98579b70c93c5d9ee83e1b/gax-2.45.0.jar!/META-INF/native-image/com.google.api/gax/native-image.properties+api=https,http' \
'-H:ClassInitialization@jar:file:///home/fniephaus/.gradle/caches/modules-2/files-2.1/com.google.api/gax/2.45.0/78ce52ef9330a8093c98579b70c93c5d9ee83e1b/gax-2.45.0.jar!/META-INF/native-image/com.google.api/gax/native-image.properties+api=org.conscrypt:build_time,org.slf4j.LoggerFactory:build_time,org.junit.platform.engine.TestTag:build_time' \
'-H:ClassInitialization@jar:file:///home/fniephaus/.gradle/caches/modules-2/files-2.1/com.google.api/gax/2.45.0/78ce52ef9330a8093c98579b70c93c5d9ee83e1b/gax-2.45.0.jar!/META-INF/native-image/com.google.api/gax/native-image.properties+api=com.google.api.client.googleapis.services.AbstractGoogleClientRequest$ApiClientVersion:run_time' \
'-H:Features@jar:file:///home/fniephaus/.gradle/caches/modules-2/files-2.1/com.google.api/gax/2.45.0/78ce52ef9330a8093c98579b70c93c5d9ee83e1b/gax-2.45.0.jar!/META-INF/native-image/com.google.api/gax/native-image.properties+api=com.google.api.gax.nativeimage.OpenCensusFeature,com.google.api.gax.nativeimage.GoogleJsonClientFeature' \
...

I've searched for slf4j through the list and found org.slf4j.LoggerFactory. As we can see, it is registered for build-time initialization by com.google.api/gax. The relevant and problematic native-image.properties file is here. It seems the file was updated for GraalVM 22.2.0 two years ago and it's no longer working with --strict-image-heap because the build-time initialize is incomplete: org.slf4j.LoggerFactory allocates objects from the org.slf4j.helpers package and those become part of the image heap. I don't think that gax should even include build-time initialization for slf4j.

In the meantime, this is how you can work around the build issue for now:

graalvmNative {
    binaries.all {
        buildArgs.add("--initialize-at-build-time=org.slf4j.helpers")
    }
}
simasch commented 5 months ago

I have the same issue in one of my projects and I thought maybe someone might be interested in the Maven plugin configuration:

<plugin>
    <groupId>org.graalvm.buildtools</groupId>
    <artifactId>native-maven-plugin</artifactId>
    <extensions>true</extensions>
    <configuration>
        <buildArgs>
            <arg>--initialize-at-build-time=org.slf4j.helpers</arg>
        </buildArgs>
    </configuration>
</plugin>