Spring Native: Going Native with JHipster and Spring Boot

How we adjusted a JHipster-generated Spring Boot/Hibernate JPA application for native image build with Spring Native?

For quite some time now, (to be more precise since its announcement in March 2021), we have been closely following the progress of the Spring Native project. Ever since, the idea of Spring Boot application built as native image, with lighting-speed boot time, measured in milliseconds instead of dozens of seconds, and smaller memory footprint captured our imagination.

Then in November 2021 when Matt Raible and Josh Long posted that they had managed to build a native image of JHipster application using Spring Native I knew I had to find time and try it for myself.

In this blog post I will share our experience in adjusting a JHipster generated Spring Boot/Hibernate JPA application for native image build with Spring Native.

For the purposes of this exercise we will use Spring Native engine version 0.11.1 and a JHipster (v7.5.0) generated demo application named “Geodata”. Geodata is a simple application with two entities: Country and Currency, consisting of Spring Boot backend Rest API and Angular frontend application with JWT as authentication method.

Code repositories of both applications are available as public repositories at GitHub:

Known Issues

Before proceeding any further it would be opportune to first point out the features of the JHipster application that we were not able to make work in the native image build of Geodata application.

1. Cache

In this run, we haven’t managed to figure out how to make ehcache work with Spring Native build. There is an ongoing thread on spring-experimental project forum on this topic. It seems that, at the moment, neither ehcache nor caffein are supported.

2. EntityGraph

For some reasons EntityGraph annotation on User entity is not working.

3. Logs endpoint (/management/loggers)

This endpoint returns HTML instead of JSON?

4. Configuration endpoint (/management/configprops) 

Calls to this endpoint result in the following error:

org.springframework.http.converter.HttpMessageNotWritableException: No converter for [class$ApplicationConfigurationProperties] with preset Content-Type ‘null’)

5. Mockito is not supported

Mockito is not supported (there is a blocked issue with more info concerning this topic:, so all mockito tests in the project can not be run.

Building native image of Geodata Rest API

To be able to follow this blog post hands on, you need to have the following tools installed on your workstation:

  • Git client
  • Java SDK 11
  • Docker client
  • IDE of your choice (IntelliJ, VS Code, Eclipse …)

Start by cloning geodata-rest-sb repository:

git clone
Code language: PHP (php)

Now cd to the geodata-rest-sb folder and build the classic docker app image:

cd geodata-rest-sb/
./gradlew bootJar -Pprod jibDockerBuild -PimageVersion=latest

This will build “ag04/geodata-rest-sb:latest” docker image in your local docker repository.

domagoj@dm-tuxXP14G14:~$ docker images
REPOSITORY                                 TAG             IMAGE ID       CREATED         SIZE
ag04/geodata-rest-sb                       latest          b14bb3a6a517   17 seconds ago  339MB

Application can now be run with the use of the prepared docker-compose yaml file:

docker-compose -f src/main/docker/app.yml up

This will download all necessary images. Once this is don,e start up all the services: Geodata-rest API, nginx server with Geodata Angular frontend and PostgreSQL database (available on localhost port 5433). Startup time of the geodata rest API varies and depends on the configuration of your workstation. On mine, it usually takes around 15 seconds.

Note down this value we will compare it later with the startup time of the native image rest api build. If you point your browser to: “http://localhost:9000/” you should be greeted with the GeodataApp welcome screen. You can sign in using default jhipster username and credentials of user/user and admin/admin.

To shutdown geodata services execute the following command:

docker-compose -f src/main/docker/app.yml down

Finally, import the Geodata Rest API to your favorite IDE (Idea, IntelliJ,…) and we can start adjusting the app for native build. In the following sections we will provide detailed instructions on steps we have undertaken to achieve the build of the working native image of Geodata Rest API application.

Configuring gradle build configuration files

To build native image of the Spring Boot app we need Spring AOT Engine. Since geodata-rest-sb is an application built with gradle, therefore the first thing we need to do is to register Spring AOT gradle plugin. Thus, we need to change setting.gradle to look as shown below:

pluginManagement {
    repositories {
       //maven { url '' }
       maven { url '' }
   plugins {
       id 'org.springframework.boot' version "${springBootVersion}"
       id '' version "${jibPluginVersion}"
       id 'com.gorylenko.gradle-git-properties' version "${gitPropertiesPluginVersion}"
       id 'org.liquibase.gradle' version "${liquibasePluginVersion}"
       id 'org.sonarqube' version "${sonarqubePluginVersion}"
       id "io.spring.nohttp" version "${noHttpCheckstyleVersion}"
       id 'com.github.andygoossens.gradle-modernizer-plugin' version "${modernizerPluginVersion}"
       id 'org.springframework.experimental.aot' version "${springAotPlugin}"
} = "geodata-rest-sb"Code language: JavaScript (javascript)

What we have done is the following:

1) commented standard spring milestone repository and instead added spring release repository, and

2) registered spring aot plugin.

The next step is to modify the file. Geodata Rest API is an application generated with jhipster version 7.5.0 and is thus built on Spring Boot version 2.5.8.

However, the latest version of spring AOT engine requires Spring Boot 2.6.2. Thus we need to adjust some dependency versions, namely increase versions of Spring Boot to 2.6.2 and Hibernate to 5.6.3.Final. Change the “Dependency versions” section of the file to match the following snipper:

# Dependency versions
# The spring-boot version should match the one managed by
# The hibernate version should match the one managed by
# -->
#hibernateVersion=5.4.33Code language: PHP (php)

Additionally, at the end of the “gradle plugin versions” section add the version for spring AOT gradle plugin:

# gradle plugin version
springAotPlugin=0.11.1Code language: PHP (php)

With these changes in place, we can now switch to modifying build.gradle. The first thing we need to do is to modify buildscript section and replace milestone repository with the release one. Second, in the buildscript.dependencies section we need to register a hibernate-gradle-plugin. This is needed for bytecode build time enhancement (to allow for features such as Lazy loading). Finally, add the spring aot gradle plugin to the list of registered plugins. The next code snippet displays all the changes done:

buildscript {
   repositories {
       maven { url '' }
   dependencies {
       //jhipster-needle-gradle-buildscript-dependency - JHipster will add additional gradle build script plugins here
       classpath "org.hibernate:hibernate-gradle-plugin:5.6.3.Final"
plugins {
   id "java"
   //jhipster-needle-gradle-plugins - JHipster will add additional gradle plugins here
   id "org.springframework.experimental.aot"
apply plugin: 'org.hibernate.orm'Code language: JavaScript (javascript)

For hibernate enhancement plugin to be active, add the following block of configuration just above defaultTask declaration.

hibernate {
   enhance {
       enableLazyInitialization= true
       enableDirtyTracking = true
       enableAssociationManagement = true
       enableExtendedEnhancement = false
defaultTasks "bootRun"Code language: JavaScript (javascript)

The next change we need to make is to comment springBoot section and specify some native build parameters, as shown here:

springBoot {
    mainClassName = "com.ag04.geodata.GeodataRestSbApp"
bootBuildImage {
    imageName = "ag04/geodata-rest-sb-native"
    builder = "paketobuildpacks/builder:tiny"
    environment = [
        "BP_NATIVE_IMAGE" : "true"
}Code language: JavaScript (javascript)

In this configuration block, we specify Paketo Buildpacks as target builder of our native image and a custom image name of “ag04/geodata-rest-sb-native” as our build target.

Also, in the repositories section, we need to change spring milestone repository with the release one, as is shown below:

repositories {
   // Local maven repository is required for libraries built locally with maven like development jhipster-bom.
   // mavenLocal()
   //jhipster-needle-gradle-repositories - JHipster will add additional repositories
   maven { url '' }
}Code language: JavaScript (javascript)

At the moment spring devtools are not supported in native builds so we need to disable them. Change the configuration section of the build.gradle to look as follows:

configurations {
   //implementation.exclude module: "spring-boot-starter-tomcat"
   all {
       exclude group:"org.springframework.boot", module: "spring-boot-devtools"
       resolutionStrategy {
           // Inherited version from Spring Boot can't be used because of regressions:
           // To be removed as soon as spring-boot use the same version
           force 'org.liquibase:liquibase-core:4.6.1'
}Code language: JavaScript (javascript)

In this section we have also commented out the “spring-boot-starter-tomcat” exclusion, since we will need this dependency. 

With this done we have finished changing projects’ configuration files, and we can proceed to the next step, which is configuring correct project dependencies.

Configure project dependencies

Since we are using Gradle, we do not need to add the spring-native dependency manually,  the Spring AOT plugin will add it automatically for us. However, we still need to make a considerable number of changes in the dependencies section of the build configuration.

  1. Set explicit version for all spring dependencies

In order to avoid collision with the jhipster specified dependencies, we need to manually specify versions for ALL of the spring libraries we are using in the project. As is, for example, shown here in the following configuration block:

implementation "org.springframework.boot:spring-boot-loader-tools:${springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-mail:${springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-logging:${springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-actuator:${springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-data-jpa:${springBootVersion}"Code language: JavaScript (javascript)
  1. Explicitly add dependency for spring-boot-starter-validation
implementation "org.springframework.boot:spring-boot-starter-validation:${springBootVersion}"Code language: JavaScript (javascript)

If we do not add this, the version of the library specified by jhipster BOM would be used. Not the one we specified in

  1. Explicitly set hibernate version

Similar as in the case of the spring dependencies we need to manually set versions for the following hibernate dependencies:

implementation "org.hibernate:hibernate-core:${hibernateVersion}"
implementation "org.hibernate:hibernate-entitymanager:${hibernateVersion}"
liquibaseRuntime "org.liquibase.ext:liquibase-hibernate5:${liquibaseHibernate5Version}"Code language: JavaScript (javascript)
  1. Replace undertow with tomcat

Locate the following block of configuration in the build.gradle:

implementation ("org.springframework.boot:spring-boot-starter-web") {
   exclude module: "spring-boot-starter-tomcat"
implementation "org.springframework.boot:spring-boot-starter-undertow"Code language: JavaScript (javascript)

and change it to look as follows:

implementation ("org.springframework.boot:spring-boot-starter-web") {
   exclude module: "spring-boot-starter-tomcat"
implementation "org.springframework.boot:spring-boot-starter-undertow"
implementation "org.springframework.boot:spring-boot-starter-web:${springBootVersion}"
implementation "org.springframework.boot:spring-boot-starter-tomcat:${springBootVersion}"Code language: JavaScript (javascript)
  1. Replace springdoc-openapi-webmvc-core with springdoc-openapi-native
//implementation ("org.springdoc:springdoc-openapi-webmvc-core")
implementation ("org.springdoc:springdoc-openapi-native:1.6.0")Code language: JavaScript (javascript)
  1. Disable spring-cloud-starter-bootstrap
//implementation ""
Code language: JSON / JSON with Comments (json)
  1. Find the following block of configuration:
if (!project.hasProperty("gae")) {
    runtimeOnly "io.jsonwebtoken:jjwt-impl"
    runtimeOnly "io.jsonwebtoken:jjwt-jackson"
} else {
   implementation "io.jsonwebtoken:jjwt-impl"
   implementation "io.jsonwebtoken:jjwt-jackson"
}Code language: JavaScript (javascript)

And modify it to look as follows:

implementation "io.jsonwebtoken:jjwt-impl"
runtimeOnly "io.jsonwebtoken:jjwt-jackson"
Code language: CSS (css)
  1. Remove spring-boot-devtools dependency
//developmentOnly "org.springframework.boot:spring-boot-devtools:${springBootVersion}"Code language: JSON / JSON with Comments (json)
  1. Remove cache dependencies

Since we haven’t been able to make cache to work (bootstrap code generated by AOT is incompatible with the method signatures of the, go ahead and simply comment the following libraries:

//implementation "org.springframework.boot:spring-boot-starter-cache"
//implementation "javax.cache:cache-api"
//implementation "org.ehcache:ehcache"
//implementation "org.hibernate:hibernate-jcache"Code language: JSON / JSON with Comments (json)

Since no cache libraries are available on the application classpath, we also need to disable hibernate second level cache. This is done by setting the following property to false inside application.yml file:

hibernate.cache.use_second_level_cache: falseCode language: CSS (css)

Fix logging

Based on this discussion from spring native forum, it seems that support for Logback logging library will be available with 0.11.3 version of Spring AOT engine. For now, just delete logback-spring.xml file from the src/main/resources folder. Also, I would suggest to reduce the level of output to logs by setting logging properties in application-prod.yml on these lines:

   tech.jhipster: INFO
   com.ag04.geodata: DEBUG
   io.netty: ERROR
   org.springframework: INFOCode language: CSS (css)

Next step is to apply several code changes that are necessary for native image of the application to work.

Java code adjustments

The first thing that we need to do is to add necessary hints to main Spring boot class:

GraalVM relies on these hints for building native image, this is the source of information for the engine concerning how to add classes that are not explicitly used in the code to target build. 

Without these hints, for example, Liquibase, Jwt authentication, async processing or thymeleaf engine would not work.

So, go ahead and ad this block of annotations to the main spring boot class:

   types = {
   types = {,
@TypeHint(types = { java.util.Locale.class }, methods = { @MethodHint(name = "getLanguage") })
@EnableConfigurationProperties({ LiquibaseProperties.class, ApplicationProperties.class })
public class GeodataRestSbApp {
}Code language: JavaScript (javascript)

Task Executor configuration

The next step is to fix Task Executor configuration. Without this fix, the application behaves as if the Spring didn’t invoke the afterPropertiesSet method on the ExceptionHandlingAsyncTaskExecutor – which it does if you enable trace and debug logs in the relevant classes and check the output. The problem is solved by manually invoking the method when creating the bean in the AsyncConfiguration class.

To do so apply the changes from the following snippet to the AsyncConfiguration:

@Bean(name = "taskExecutor")
public Executor getAsyncExecutor() {
   //return new ExceptionHandlingAsyncTaskExecutor(executor);
   ExceptionHandlingAsyncTaskExecutor taskExecutor = new ExceptionHandlingAsyncTaskExecutor(executor);
   try {
       return taskExecutor;
   } catch (Exception e) {
       throw new RuntimeException(e);
}Code language: PHP (php)

As noted previously, @EntityGraph annotation is not working in the current settings, so we need to comment this annotation on following methods in the

//@EntityGraph(attributePaths = "authorities")
@Cacheable(cacheNames = USERS_BY_LOGIN_CACHE)
Optional<User> findOneWithAuthoritiesByLogin(String login);
//@EntityGraph(attributePaths = "authorities")
@Cacheable(cacheNames = USERS_BY_EMAIL_CACHE)
Optional<User> findOneWithAuthoritiesByEmailIgnoreCase(String email);Code language: JavaScript (javascript)

Also, we need to adjust the authorities property of the User entity ( to include FetchType.EAGER instruction to JPA (or the DomainUserDetailsService will fail attempting to fetch user during the login), as is shown here:

@ManyToMany(fetch = FetchType.EAGER)
name = "jhi_user_authority",
      joinColumns = { @JoinColumn(name = "user_id", referencedColumnName = "id") },
      inverseJoinColumns = { @JoinColumn(name = "authority_name", referencedColumnName = "name") }
   @Cache(usage = CacheConcurrencyStrategy.NONSTRICT_READ_WRITE)
   @BatchSize(size = 20)
   private Set<Authority> authorities = new HashSet<>();Code language: CSS (css)

Fixing support for mail sending required some hacking and workarounds. For mysterious reasons, when running inside native build, spring autoconfiguration for MailSender does not kick in and MailSender is not configured. After some pondering over this issue, we decided to manually configure the Java mail sender. We did it in the same way Spring itself configures JavaMailSender.

Therefore, inside the package com.ag04.geodata.config we added a file

package com.ag04.geodata.config;
import org.springframework.boot.autoconfigure.mail.MailProperties;
import org.springframework.context.annotation.Configuration;
public class MailPropertiesConfiguration extends MailProperties {
}Code language: CSS (css)

Now, in the same package also create file with the following content:

public class MailConfiguration {
   JavaMailSenderImpl mailSender(MailProperties properties) {
       JavaMailSenderImpl sender = new JavaMailSenderImpl();
       applyProperties(properties, sender);
       return sender;
   private void applyProperties(MailProperties properties, JavaMailSenderImpl sender) {
       if (properties.getPort() != null) {
       if (properties.getDefaultEncoding() != null) {
       if (!properties.getProperties().isEmpty()) {
   private Properties asProperties(Map<String, String> source) {
       Properties properties = new Properties();
       return properties;
}Code language: JavaScript (javascript)

The above source is basically taken from the spring package. With these last changes in place we are almost done with the adjusting goedata-rest-sb application for native build. There is one more piece of configuration we need to add.

We also need to clean up a, namely to remove all references to the CacheManager class from it (since we do not have a working cache provider at the moment). What needs to be done is to comment private property CacheManager, remove CacheManager from the signature of the constructor (as shown below):

  //private final CacheManager cacheManager;
   public UserService(
       UserRepository userRepository,
       AuthorityRepository authorityRepository,
       PasswordEncoder passwordEncoder
   ) {
       this.userRepository = userRepository;
       this.authorityRepository = authorityRepository;
       this.passwordEncoder = passwordEncoder;
   }Code language: JavaScript (javascript)

and also comment the entire body of the clearUsercaches() method:

private void clearUserCaches(User user) {
       /*  Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_LOGIN_CACHE)).evict(user.getLogin());
       if (user.getEmail() != null) {  Objects.requireNonNull(cacheManager.getCache(UserRepository.USERS_BY_EMAIL_CACHE)).evict(user.getEmail());
}Code language: JavaScript (javascript)

Finally to avoid compilation errors delete the entire class file: from the project.

Configuring static resources

The last piece of the puzzle concerns the access to static resources in the native image. Official GraalVM documentation states the following:

By default, the native image builder will not integrate any of the resources on the classpath during the generation into the final image.

As a result, any attempt to access static files located inside jar files available on the classpath will fail! In the case of our application, this means that in the native image, liquibase will not be working since it needs access to the XSD files. Or that JavaMailSender will not be able to be initialized because it will fail to access several files from jakarta-mail.jar.

To overcome this we need to instruct GraalVM to include these files in our build. We can do that by defining which static files to be included (or excluded) in the file named: resource-config.json, located in the folder src/main/resources/META-INF/native-image/. GraalVM will automatically detect any such files and apply them. Further customization is also available with the spring-native hints as documented here.

Therefore go ahead and create this file with the following content:

   "resources": {
       "includes": [
           {"pattern" : "\\Qorg/springframework/mail/javamail/mime.types\\E"},
           {"pattern" : ".*/"},
           {"pattern" : ".*/javamail.default.providers"},
           {"pattern" : ".*/"}
}Code language: JSON / JSON with Comments (json)

This solves all the Java Mail Sender initialization problems, what remains to be tackled is the problem of including all liquibase XSD files. Fortunately, there is a Pull Request that provides solutions to this problem. Take the two files from this PR (resource-config.json and reflect-config.json) and save them to “src/main/resources/META-INF/native-image/liquibase/” folder.

Finally, it is time to build native image of the Geodata Rest API.

Building and running native image

To build native image issue the following command inside root project folder:

./gradlew clean bootBuildImage

This will start a build, be patient since building native images takes considerably longer time than building a normal one.

Once the build is done, you can check if image is available in you local docker registry:

docker images

And in the list of the local container images you should now also see “ag04/geodata-rest-sb-native” image:

REPOSITORY                                 TAG             IMAGE ID       CREATED         SIZE
ag04/geodata-rest-sb-native                latest          aeb33a851c36   42 years ago    201MB

As can be seen, this image is noticeably smaller than the standard one (210Mb vs 313Mb).

In order to use this image open docker-compose file appl.yml we used previously at the beginning of this exercise, and modify geodata-rest-sb service image “name” property to refer to the new native image, as shown here:

   image: ag04/geodata-rest-sb-native:latest
     - _JAVA_OPTIONS=-Xmx512m -Xms256m

Save the changes and run the application by executing:

docker-compose -f src/main/docker/app.yml up

If all went well you should see the output similar to the one bellow:

geodata-rest-sb_1           | 2022-02-02 19:02:44.576  INFO 1 --- [           main] o.s.b.a.e.web.EndpointLinksResolver      : Exposing 9 endpoint(s) beneath base path '/management'
geodata-rest-sb_1           | 2022-02-02 19:02:44.578  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
geodata-rest-sb_1           | 2022-02-02 19:02:44.578  INFO 1 --- [           main] com.ag04.geodata.GeodataRestSbApp        : Started GeodataRestSbApp in 0.417 seconds (JVM running for 0.418)
geodata-rest-sb_1           | 2022-02-02 19:02:44.579  INFO 1 --- [           main] com.ag04.geodata.GeodataRestSbApp        : 
geodata-rest-sb_1           | ----------------------------------------------------------
geodata-rest-sb_1           | 	Application 'geodataRestSb' is running! Access URLs:
geodata-rest-sb_1           | 	Local: 		http://localhost:8080/
geodata-rest-sb_1           | 	External:
geodata-rest-sb_1           | 	Profile(s): 	[prod, api-docs, awsses]
geodata-rest-sb_1           | ---------------------------------------------------------Code language: JavaScript (javascript)

In my case, the Geodata Rest API built as native image started up in just 418 ms compared to 15 seconds in the case of the image built in a standard way! Which is really impressive.


Having an understanding of what the native image builder does – shaves off excess classes, methods, resources etc. that it deems unreachable – the task at hand seemed simple: learn and understand ways to pass hints to the builder, then employ those ways to instruct it to leave used code and resources in the final image.

However, this turned out to be all but simple. There are several ways to pass hints, and several layers of abstraction on top of the annotations in org.springframework.nativex.hint package. The docs are so-so, and there was a lot of trial and error going on. Taking into account image build times, this effort was very time consuming.

Being a detective in the library source

Furthermore, many exceptions didn’t simply state “this class/method/resource is missing”. Instead, the framework/library got initialized or used the wrong way because internal exceptions sometimes got caught and recovered from, or branching conditions got evaluated differently than in a non-native context. Then, 5 miles down the road something blew up giving you no obvious thing to fix – i.e. hint to employ. Being a detective in the library source and backtracking from the exception you got was a must have skill.

The final jalapeno is that certain code didn’t get invoked until you triggered a certain feature. E.g. sending a mail, doing work in a separate WorkExecutor thread etc. This implies that when you convert your app to a native variant, you’re left with a full feature sweep to do. Sometimes – many times – not a simple feat at all.

Given all that, we hold the opinion that this is still not ready for the mass market. And that’s when taking into account most of the features used here are basic and we didn’t integrate with anything external other than the DB or email server. What we would like though – and what surely is an area many library authors are working on – is to see better native support. Namely

  • Clearer error messages
  • Friendlier ways to choose and pass hints to the native image builder 

Finally – all the negatives aside – the positives are extremely convincing. Startup time is next to nothing, memory consumption is decreased, as well as the image size – and those alone are big enough reasons to seriously assess this approach.

Closing remarks

By the time we finished this article, new versions of both JHipster (v7.6.0) and Spring AOT plugin (0.11.2) came out. All with dozens of improvements: upgrade of JHipster to Spring Boot 2.6.3. (which is the target working version for Spring AOT 0.11.2 plugin) support for spring devtools, and many more.

However, as great as this sounds, it seems that in the world of native spring images nothing is simple, and in our case this proved to be more of a step back than a step forward. Upgrading to  the 0.11.2 version of the Spring AOT plugin introduced a problem of circular dependencies between liquibase bean and dataSourceScriptDatabaseInitializer bean.

A problem that does not exist outside of native build, and we were unable to either solve it or to find a suitable workaround. Moreover, Spring AOT plugin 0.11.1 we used in this example does not work properly with Spring Boot 2.6.3.This problem is deeply rooted in the Spring AOT plugin, and as it stands now we decided to leave things as they are and tackle this issue in some future exercise/blog post.

This latest experience strengthened our opinion that Spring Native is without any doubt very promising technology, and that one should keep close track of it, though its application at the moment should be limited to special cases.


Fully completed working native build version of the Geodata Rest API is available on “native” branch of the project github repository.

Official spring documentation

GraalVM reference manual

Matt Raible Spring Native build examples


Exceptional ideas need experienced partners.