resteasy / resteasy-spring-boot

Apache License 2.0
124 stars 51 forks source link

Spring native compilation works but execution fails #260

Closed itineric closed 1 year ago

itineric commented 1 year ago

I was trying to run the sample app compiled with spring native, compilation works fine but I am getting an error on app startup: ` The bean 'com.sample.app.configuration.JaxrsApplication' could not be registered. A bean with that name has already been defined and overriding is disabled.

` AOT seems fine on the app, I cannot understand how it finds the same bean many times. Any clue ?

liweinan commented 1 year ago

Hi @itineric The project has not verified with native compilation yet, any contribution on this field is welcome.

itineric commented 1 year ago

I got something that works based on your servlet/resteasy-servlet-spring-boot-sample-app.

Hope it helps you to add your lib to the graalvm supported ones: https://www.graalvm.org/native-image/libraries-and-frameworks/

Bean double registration

First you need to declare a BeanRegistrationExcludeFilter because of this code in your library: https://github.com/resteasy/resteasy-spring-boot/blob/main/servlet/resteasy-servlet-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot/ResteasyBeanProcessorTomcat.java#L57

Why ? the bean registered there is added once during Spring AOT process, then again during the runtime. The error is then The bean 'com.sample.app.configuration.JaxrsApplication' could not be registered. A bean with that name has already been defined and overriding is disabled.

The implementation of the BeanRegistrationExcludeFilter looks like this:

class ServletRegistrationBeanExcludeFilter implements BeanRegistrationExcludeFilter
{
  @Override
  public boolean isExcludedFromAotProcessing(final RegisteredBean registeredBean)
  {
    return registeredBean.getBeanClass().equals(ServletRegistrationBean.class);
  }
}

Maybe in more complex applications filtering needs to be more specific (not excluding every ServletRegistrationBean from AOT process). But for applications built like the sample, it works.

Another solution may be to keep AOT for that bean registration (thats the point of AOT after all) but then the registration code in https://github.com/resteasy/resteasy-spring-boot/blob/main/servlet/resteasy-servlet-spring-boot-starter/src/main/java/org/jboss/resteasy/springboot/ResteasyBeanProcessorTomcat.java must be excluded from native deliverable (since already done during AOT).

In order for the BeanRegistrationExcludeFilter to be triggered it must be declared in a file named META-INF/spring/aot.factories using the following key: org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter=com.sample.app.configuration.ServletRegistrationBeanExcludeFilter

Reflection hints

When thats done, you then get reflection errors. Such errors can to be addressed using hints or reflect-config.json files. I have done it using hints (which spring native will use to generation reflect-config.json).

The class with comments:

import static org.springframework.aot.hint.ExecutableMode.INVOKE;

import java.io.OutputStream;

import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportRuntimeHints;

@Configuration
@ImportRuntimeHints(NativeImageHints.class)
public class NativeImageHints implements RuntimeHintsRegistrar
{
  @Override
  public void registerHints(final RuntimeHints hints, final ClassLoader classLoader)
  {
    try
    {
      hints.reflection()
        // resteasy hints (yous should add them to your lib for now since resteasy is not (yet) a supported graalvm lib)
        .registerConstructor(org.jboss.resteasy.resteasy_jaxrs.i18n.LogMessages_$logger.class.getConstructor(org.jboss.logging.Logger.class),
                             INVOKE)
        .registerConstructor(org.jboss.resteasy.plugins.validation.i18n.LogMessages_$logger.class.getConstructor(org.jboss.logging.Logger.class),
                             INVOKE)
        .registerField(org.jboss.resteasy.resteasy_jaxrs.i18n.Messages_$bundle.class.getField("INSTANCE"))
        .registerField(org.jboss.resteasy.plugins.validation.i18n.Messages_$bundle.class.getField("INSTANCE"))
        .registerConstructor(classLoader.loadClass("org.jboss.resteasy.core.ContextServletOutputStream").getDeclaredConstructor(classLoader.loadClass("org.jboss.resteasy.core.ContextParameterInjector"),
                                                                                                                                OutputStream.class),
                             ExecutableMode.INVOKE)
        // this lib hint
        .registerType(classLoader.loadClass("org.jboss.resteasy.springboot.ResteasyApplicationBuilder"),
                      MemberCategory.INTROSPECT_PUBLIC_METHODS,
                      MemberCategory.INVOKE_PUBLIC_METHODS)

        // app hints (thats only here to make the sample work, must not be included in your lib)
        .registerType(JaxrsApplication.class,
                      MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS)
        .registerType(org.jboss.resteasy.springboot.common.sample.resources.Echo.class,
                      MemberCategory.INVOKE_PUBLIC_METHODS)
        .registerType(org.jboss.resteasy.springboot.common.sample.resources.Foo.class,
                      MemberCategory.INVOKE_PUBLIC_METHODS)
        .registerType(org.jboss.resteasy.springboot.common.sample.resources.EchoMessage.class,
                      MemberCategory.INTROSPECT_PUBLIC_METHODS,
                      MemberCategory.INVOKE_PUBLIC_METHODS);

    }
    catch (final ClassNotFoundException | NoSuchMethodException | NoSuchFieldException | SecurityException exception)
    {
      throw new RuntimeHintsException(exception);
    }
  }

  private static class RuntimeHintsException extends RuntimeException
  {
    private static final long serialVersionUID = 1L;

    RuntimeHintsException(final Throwable cause)
    {
      super(cause);
    }
  }
}

The build

To build all that natively, thats the profile I added to the pom.xml

<build>
 ...
 ...
 ...
  <pluginManagement>
    <plugins>
      <plugin>
        <groupId>org.graalvm.buildtools</groupId>
        <artifactId>native-maven-plugin</artifactId>
        <version>0.9.25</version>
        <extensions>true</extensions>
      </plugin>
    </plugins>
  </pluginManagement>
</build>

<profiles>
  <profile>
    <id>native</id>
    <build>
      <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
              <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                  <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
              </image>
            </configuration>
            <executions>
              <execution>
                <id>process-aot</id>
                <goals>
                  <goal>process-aot</goal>
                </goals>
              </execution>
            </executions>
          </plugin>
          <plugin>
            <groupId>org.graalvm.buildtools</groupId>
            <artifactId>native-maven-plugin</artifactId>
            <configuration>
              <classesDirectory>${project.build.outputDirectory}</classesDirectory>
              <metadataRepository>
                <enabled>true</enabled>
              </metadataRepository>
              <requiredVersion>22.3</requiredVersion>
            </configuration>
            <executions>
              <execution>
                <id>add-reachability-metadata</id>
              <goals>
                <goal>add-reachability-metadata</goal>
              </goals>
            </execution>
            <execution>
              <id>build-native</id>
              <goals>
                <goal>compile-no-fork</goal>
              </goals>
              <phase>package</phase>
              <configuration>
                <fallback>false</fallback>
                <buildArgs>
                  <arg>--install-exit-handlers</arg>
                </buildArgs>
              </configuration>
            </execution>
          </executions>
        </plugin>
      </plugins>
    </build>
  </profile>
</profiles>

And the command line: mvn -f servlet/resteasy-servlet-spring-boot-sample-app -Pnative package that will produce some ./servlet/resteasy-servlet-spring-boot-sample-app/target/resteasy-servlet-spring-boot-sample-app executable

Resources: Setup graalvm: https://www.graalvm.org/jdk17/docs/getting-started/ The graalvm manual to understand what I am talking about: https://www.graalvm.org/latest/reference-manual/native-image/metadata/

liweinan commented 1 year ago

@itineric Thanks for sharing the information! I've talked with @jamezp on this topic, who is the leader of the RESTEasy project, and the conclusion is that currently the RESTEasy project hasn't supported native build formally.(In Quarkus it uses resteasy-reactive which officially supports native build). So the native build of RESTEasy projects are not formally supported yet (except the resteasy-reactive for Quarkus). But any contribution in this field is welcome :D

liweinan commented 1 year ago

Close as answered.