Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hateoas Links are not deserialized in Spring Boot Native applications #5924

Open
klopfdreh opened this issue Sep 4, 2024 · 2 comments
Open
Labels
status/need-investigation Oh need to look under a hood
Milestone

Comments

@klopfdreh
Copy link
Contributor

Description:
This is a followup issue on spring-projects/spring-hateoas#2210.

Release versions:
2.11.x

Custom apps:
N/A

Steps to reproduce:

  • Build a task application that uses spring-cloud-data-flow-rest-client and native-compile it

Screenshots:
N/A

Additional context:
I am going to research a bit more when I converted all other task application of us to native-images, but it would be great if you could tell me which classes required to be included for reflect-config.json in addition to the mentioned classes/packages here: spring-projects/spring-hateoas#2210 (comment)

@github-actions github-actions bot added the status/need-triage Team needs to triage and take a first look label Sep 4, 2024
@klopfdreh
Copy link
Contributor Author

klopfdreh commented Sep 6, 2024

Here are some background information:

We are currently using the following dependency in one of our task application we want to native compile with Spring Boot Native:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-dataflow-rest-client</artifactId>
</dependency

we also added

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

because we saw that the HypermediaAutoConfiguration has several ConditionalOnClass which requires org.springframework.web

To customize the DataFlowTemplate we are using the following code:

RestTemplate prepareRestTemplate = DataFlowTemplate.prepareRestTemplate(restTemplateBuilder.build());

// Here we are going to apply a ClientHttpRequestInterceptor to get the a bearer from our OAuth Server and a RetryTemplate for retry behavior 

return new DataFlowTemplate(new URI(customUriToOurSCDFBackend), prepareRestTemplate, null);

To retrieve the RestTemplate with restTemplateBuilder.build() in the code above we are using an auto configuration providing this bean:

    @Bean
    public RestTemplateCustomizer hypermediaRestTemplateCustomizer(HypermediaRestTemplateConfigurer hypermediaRestTemplateConfigurer) {
        return restTemplate -> {
            log.info("RestTemplateCustomizer has been configured to use HypermediaRestTemplateConfigurer");
            hypermediaRestTemplateConfigurer.registerHypermediaTypes(restTemplate);
        };
    }

As of https://docs.spring.io/spring-hateoas/docs/current/reference/html/#client.rest-template the RestTemplateCustomizer is used for restTemplateBuilder.build() and The RestTemplate instance will be able to interact using hypermedia.

As we are using WebApplicationType.NONE in our task applications the HypermediaAutoConfiguration is not going to be applied because of @ConditionalOnWebApplication see https://github.com/spring-projects/spring-boot/blob/109142e691fda17cc951af726a3ca0a9bcf10a8d/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/hateoas/HypermediaAutoConfiguration.java so we use the following configuration to ensure that all reflection / resources hints are applied correctly:

/**
 * Configuration class for hateoas.
 */
@Configuration
@EnableConfigurationProperties(HateoasProperties.class)
@EnableHypermediaSupport(type = {
    EnableHypermediaSupport.HypermediaType.HAL
})
public class SerialLauncherHateaosConfiguration {

    /**
     * Gets the hal configuration.
     *
     * @return the hal configuration
     */
    @Bean
    public HalConfiguration applicationJsonHalConfiguration() {
        return new HalConfiguration().withMediaType(MediaType.APPLICATION_JSON);
    }
}

The spring-boot-maven-plugin also shows it during process-aot at build time

[INFO] --- spring-boot:3.3.2:process-aot (process-aot) @ task-module ---

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.3.2)

....

{"timestamp":"2024-09-06T06:38:40.404+0200","level":"INFO","thread":"main","logger":"org.springframework.hateoas.aot.AotUtils","message":"Registering Spring HATEOAS types in org.springframework.hateoas.mediatype.hal for reflection.","context":"default"}
{"timestamp":"2024-09-06T06:38:40.832+0200","level":"INFO","thread":"main","logger":"org.springframework.hateoas.aot.AotUtils","message":"Registering Spring HATEOAS types in org.springframework.hateoas for reflection.","context":"default"}

....

In addition to that all scdf classes are not registered for reflections, because it is a Spring Boot 2 dependency. So we are using a RuntimeHintsRegistrar to register them. We are using the same approach as described here: hub4j/github-api#1908 (comment) but for the package org/springframework/cloud/dataflow/rest. The process-aot shows all required classes including the org.springframework.cloud.dataflow.rest.resource.RootResource like


....

{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.AppStatusResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.CurrentTaskExecutionsResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.StepExecutionResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.StreamStatusResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.JobExecutionResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.RootResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.978+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.SchemaVersionTargetResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.DeploymentStateResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.JobExecutionResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.DetailedAppRegistrationResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.StepExecutionResource for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.StreamStatusResource$Page for reflections and serialization.","context":"default"}
{"timestamp":"2024-09-06T06:38:42.979+0200","level":"INFO","thread":"main","logger":"task-module.aot.SerialLauncherRuntimeHints","message":"Registered class org.springframework.cloud.dataflow.rest.resource.about.AboutResource for reflections and serialization.","context":"default"}

....

Note: To enable all reflection access ways we are using MemberCategory.values() for the registration

The org.springframework.cloud.dataflow.rest.resource.RootResource itself only extends RepresentationModel<RootResource> so it is basic Hateoas logic, but the exception Caused by: java.util.NoSuchElementException: No value present occurs like mentioned here spring-projects/spring-hateoas#2210 (comment) when the following code is executed

this.aboutOperations = new AboutTemplate(restTemplate, resourceSupport.getLink(AboutTemplate.ABOUT_REL).get());

To me this looks like the ABOUT_REL link can't be retrieved from the resourceSupport (which is the RootResource) and .get() returns no value meaning that the links are not deserialized correctly.

I think that this will also occur when you port the DataFlowTemplate to Spring Boot 3 without refactoring the code / providing specific native image hints.

It has only to be a small thing to be adjusted to make this work 😃 and it would be great if you could help even if this might not be supported out of the box.

@klopfdreh
Copy link
Contributor Author

klopfdreh commented Sep 9, 2024

Hey @cppwfs / @onobc / @corneil
I found the issue. It was because of WebApplicationType.NONE. I solved it the following way:

  1. The task application needs to be Spring Boot 3.3.x
  2. Create a RuntimeHintsRegistrar to register all classes for reflection and serialization in the package org/springframework/cloud/dataflow/rest
/**
 * Generates hints of the used classes for the native build.
 */
@Configuration
@ImportRuntimeHints(SerialLauncherRuntimeHints.class)
@Slf4j
public class SpringCloudDataFlowRestRuntimeHints implements RuntimeHintsRegistrar {

    @Override
    public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
        Arrays.stream(System.getProperty("java.class.path").split(File.pathSeparator)).forEach(classpathEntry -> {
            // If the classpathEntry is no jar skip it
            if (!classpathEntry.endsWith(".jar")) {
                return;
            }

            try (JarInputStream is = new JarInputStream(Files.newInputStream(Path.of(classpathEntry)))) {
                JarEntry entry = is.getNextJarEntry();
                while (entry != null) {
                    String entryName = entry.getName();
                    if (entryName.endsWith(".class")
                        && !entryName.contains("package-info")
                        && entryName.startsWith("org/springframework/cloud/dataflow/rest")
                    ) {
                        String scdfResourceClassName = entryName.replace("/", ".");
                        String scdfResourceClassNameWithoutClass = scdfResourceClassName.substring(0, scdfResourceClassName.length() - 6);
                        log.info("Registered class {} for reflections and serialization.", scdfResourceClassNameWithoutClass);
                        hints.reflection().registerType(TypeReference.of(scdfResourceClassNameWithoutClass), MemberCategory.values());
                        hints.serialization().registerType(TypeReference.of(scdfResourceClassNameWithoutClass));
                    }
                    entry = is.getNextJarEntry();
                }
            } catch (IOException e) {
                log.warn("Error while reading jars", e);
            }
        });
    }
}
  1. I added a configuration to the project like the following:
/**
 * Configuration class for hateoas. See <a href="https://stackoverflow.com/questions/56352544/how-to-ensure-application-haljson-is-the-first-supported-media-type">hal+json issue</a>
 */
@Configuration
@EnableConfigurationProperties(HateoasProperties.class)
@Import(RepositoryRestMvcConfiguration.class)
public class SerialLauncherHateaosConfiguration {

    /**
     * Gets the hal configuration.
     *
     * @return the hal configuration
     */
    @Bean
    public HalConfiguration applicationJsonHalConfiguration() {
        return new HalConfiguration().withMediaType(MediaType.APPLICATION_JSON);
    }
}
  1. I added the following dependencies to pom.xml
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dataflow-rest-client</artifactId>
            <!-- Excluded because we don't need JPA / Hibernate / Skipper for Rest-Calls -->
            <exclusions>
                <exclusion>
                    <artifactId>spring-data-jpa</artifactId>
                    <groupId>org.springframework.data</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>hibernate-core</artifactId>
                    <groupId>org.hibernate</groupId>
                </exclusion>
                <exclusion>
                    <artifactId>spring-cloud-skipper</artifactId>
                    <groupId>org.springframework.cloud</groupId>
                </exclusion>
                <!-- TODO SCDF 3.x Remove this exculsion if scdf is updated -->
                <exclusion>
                    <artifactId>httpclient</artifactId>
                    <groupId>org.apache.httpcomponents</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.data</groupId>
            <artifactId>spring-data-rest-webmvc</artifactId>
        </dependency>
  1. See ticket Hints for spring-cloud-dataflow-rest-client Spring Boot 3 port #5862 for hints about the rest client.

SCDF Rest Client of version 2.11.x is working this way with Spring Boot 3 native images

I think beside the registration of the package this is still an Spring Hateoas issue, because it does not take WebApplicationType.NONE into account.

@klopfdreh klopfdreh changed the title Links are not deserialized in Spring Boot Native applications Hateoas Links are not deserialized in Spring Boot Native applications Sep 9, 2024
@cppwfs cppwfs added for/team-attention For team attention status/need-investigation Oh need to look under a hood and removed status/need-triage Team needs to triage and take a first look for/team-attention For team attention labels Sep 30, 2024
@cppwfs cppwfs added this to the 3.0.x milestone Sep 30, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status/need-investigation Oh need to look under a hood
Projects
None yet
Development

No branches or pull requests

2 participants