Skip to content

mraible/spring-native-examples

Repository files navigation

Spring Native with JHipster

For Josh’s examples, see his Spring Native 0.11.x repo.

This repository contains five apps we (@joshlong and @mraible) used to figure out how to make Spring Native work with JHipster.

  • spring-native-webflux - no client, no db, just WebFlux

  • spring-native-mvc - no client, no db, just Spring MVC

  • angular-webflux - Angular client, no db

  • postgres-webflux - Angular, WebFlux, R2DBC + PostgreSQL

  • postgres-mvc - Angular, Spring MVC, JPA + PostgreSQL

If you already have a JHipster app with OIDC, you can make the changes below to make it work with Spring Native.

⚡️ You can also use the JHipster Native blueprint to generate an app with all of these changes already included!

  1. Modify pom.xml to add Spring Native support:

    <repository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </repository>
    
    <pluginRepository>
        <id>spring-releases</id>
        <name>Spring Releases</name>
        <url>https://repo.spring.io/release</url>
        <snapshots>
            <enabled>false</enabled>
        </snapshots>
    </pluginRepository>
    ...
    
    <repackage.classifier/>
    <spring-native.version>0.11.3</spring-native.version>
    ...
    
    <!-- If reactive, comment out boringssl. This allows Spring Native to build the image and gets around this error:
        Error: Classes that should be initialized at run time got initialized during image building:
        io.netty.buffer.UnpooledUnsafeDirectByteBuf the class was requested to be initialized at run time
        (subtype of io.netty.buffer.AbstractReferenceCountedByteBuf).
        To see why io.netty.buffer.UnpooledUnsafeDirectByteB got initialized use
        trace-class-initialization=io.netty.buffer.UnpooledUnsafeDirectByteBuf
    -->
    <!--dependency>
        <groupId>io.netty</groupId>
        <artifactId>netty-tcnative-boringssl-static</artifactId>
    </dependency-->
    
    <dependency>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-native</artifactId>
        <version>${spring-native.version}</version>
    </dependency>
    
    <plugin>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-maven-plugin</artifactId>
        <configuration>
            <classifier>${repackage.classifier}</classifier>
            <image>
                <builder>paketobuildpacks/builder:tiny</builder>
                <env>
                    <BP_NATIVE_IMAGE>true</BP_NATIVE_IMAGE>
                </env>
            </image>
        </configuration>
    </plugin>
    <plugin>
        <groupId>org.springframework.experimental</groupId>
        <artifactId>spring-aot-maven-plugin</artifactId>
        <version>${spring-native.version}</version>
        <executions>
            <execution>
                <id>test-generate</id>
                <goals>
                    <goal>test-generate</goal>
                </goals>
            </execution>
            <execution>
                <id>generate</id>
                <goals>
                    <goal>generate</goal>
                </goals>
            </execution>
        </executions>
    </plugin>
    
    <profile>
        <id>native</id>
        <properties>
            <repackage.classifier>exec</repackage.classifier>
            <native-buildtools.version>0.9.10</native-buildtools.version>
        </properties>
        <dependencies>
            <dependency>
                <groupId>org.junit.platform</groupId>
                <artifactId>junit-platform-launcher</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
        <build>
            <plugins>
                <plugin>
                    <groupId>org.graalvm.buildtools</groupId>
                    <artifactId>native-maven-plugin</artifactId>
                    <version>${native-buildtools.version}</version>
                    <extensions>true</extensions>
                    <executions>
                        <execution>
                            <id>test-native</id>
                            <phase>test</phase>
                            <goals>
                                <goal>test</goal>
                            </goals>
                        </execution>
                        <execution>
                            <id>build-native</id>
                            <phase>package</phase>
                            <goals>
                                <goal>build</goal>
                            </goals>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
    </profile>
  2. Delete src/main/resources/logback-spring.xml and tone down logging. Remove src/test/resources/logback.xml too.

    logging:
      level:
        root: ERROR
        io.netty: ERROR
        liquibase: ERROR
        org.hibernate: ERROR
        org.springframework: ERROR
        com.zaxxer.hikari: ERROR
        org.apache.catalina: ERROR
        org.apache.tomcat: ERROR
        tech.jhipster.config: ERROR
        jdk.event.security: ERROR
        java.net: ERROR
        sun.net.www: ERROR
  3. There’s an issue when using Spring WebFlux if you don’t use -DskipTests when running ./mvnw package -Pnative:

    [ERROR] Failed to execute goal org.springframework.experimental:spring-aot-maven-plugin:0.11.2:test-generate (test-generate) on project jhipster:
    Build failed during Spring AOT test code generation: Unable to execute mojo:
    Unable to parse configuration of mojo org.apache.maven.plugins:maven-compiler-plugin:3.9.0:testCompile for parameter compilePath:
    Cannot find 'compilePath' in class org.apache.maven.plugin.compiler.TestCompilerMojo -> [Help 1]
    [ERROR]

    The error seems to be better when using Spring MVC:

    Caused by: java.lang.IllegalStateException: @MockBean is not supported yet by Spring AOT
    and has been detected on type org.springframework.web.client.RestTemplate
  4. If using Spring MVC, swap Undertow dependencies for Tomcat (in pom.xml) and modify WebConfigurer to comment out setLocationForStaticAssets(server).

  5. Update main App.java to add hints for Micrometer:

    import org.springframework.nativex.hint.TypeHint;
    
    @TypeHint(
        types = {
            org.HdrHistogram.Histogram.class,
            org.HdrHistogram.ConcurrentHistogram.class
        })
  6. Add springdocs native dependency:

    <dependency>
        <groupId>org.springdoc</groupId>
        <artifactId>springdoc-openapi-native</artifactId>
        <version>1.6.6</version>
    </dependency>
  7. Liquibase is not supported yet, but you can make it work by adding files from this pull request to your src/main/resources/META-INF/native-image/liquibase directory.

  8. Add type hints for Liquibase and related classes.

    @TypeHint(
        types = {
            ...
            liquibase.configuration.LiquibaseConfiguration.class,
            com.zaxxer.hikari.HikariDataSource.class,
            liquibase.change.core.LoadDataColumnConfig.class,
            tech.jhipster.domain.util.FixedPostgreSQL10Dialect.class,
            org.hibernate.type.TextType.class,
        })
  9. If you’re using JPA, add a type hint for java.util.HashSet.class and turn off loading of SQL in application.yml so Liquibase works:

    spring:
      ...
      sql:
        init:
          mode: never

    Without this change, the following error happens:

    org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'dataSourceScriptDatabaseInitializer':
    Circular depends-on relationship between 'dataSourceScriptDatabaseInitializer' and 'liquibase'
  10. If you’re using Spring WebFlux with R2DBC, you’ll need to add @Component to your Impl classes and add SimpleR2dbcRepository to your type hints and com.zaxxer.hikari.util.ConcurrentBag$IConcurrentBagEntry[] as a typeName for Liquibase.

    @TypeHint(types = {
        ...
        org.springframework.data.r2dbc.repository.support.SimpleR2dbcRepository.class
    }, typeNames = "com.zaxxer.hikari.util.ConcurrentBag$IConcurrentBagEntry[]")
  11. If Spring MVC, add names to any @RequestParam and @PathVariable annotations.

    @RequestParam(name = "eagerload", required = false, defaultValue = "false") boolean eagerload
    @PathVariable("id") Long id
  12. For logout to work, update LogoutResource to remove (expression = "idToken"), inject the OidcUser instead, and get the token from there:

    public ResponseEntity<?> logout(HttpServletRequest request, @AuthenticationPrincipal OidcUser oidcUser) {
        ...
        OidcIdToken idToken = oidcUser.getIdToken();
  13. Caching is not supported yet. The only workaround I’ve found so far is to re-generate your app with "cacheProvider": "no" in your .yo-rc.json.

  14. Build with ./mvnw package -Pnative,prod -DskipTests

Known Issues

  • -DskipTests is needed for both Spring MVC and WebFlux. This seems to be caused by Mockito.

  • Several of JHipster’s Administration features don’t work: metrics, logs, and configuration.

  • Metrics: UnsupportedFeatureError: ThreadMXBean methods

  • Logs: /management/loggers returns HTML instead of JSON

  • Configuration error:

    org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class org.springframework.boot.actuate.context.properties.ConfigurationPropertiesReportEndpoint$ApplicationConfigurationProperties] with preset Content-Type 'null'
  • H2 doesn’t work if you build with the dev profile:

    java.lang.IllegalStateException: Failed to process lifecycle methods on bean definition with name 'h2TCPServer'

    I tried adding the following to the @TypeHint in the main class, but it doesn’t work.

    typeNames = {"org.h2.tools.Server", "org.h2.server.web.WebServlet"}

    This class is not on the classpath by default. Maybe that has something to do with it?