JHipster works with Spring Native, Part 2!
A couple of months ago (September 2021), I traveled to San Francisco to speak at the SF JUG with Josh Long. I arrived early and we spent a couple of days making JHipster work with Spring Native.
Shortly after, the Garden State JUG reached out to us and asked if we'd like to speak in December. We happily agreed, not realizing that new versions of Spring Boot (v2.6) and Spring Native (v0.11) would be released beforehand.
If you'd like to learn all about Spring Native and how we integrated the latest version with JHipster, you can watch our presentation below. Keep reading if you'd like to hear what we encountered.
As good developers do, Josh and I waited until the last minute to try and get things working with the latest releases. Part of this was because JHipster is still upgrading to Spring Boot 2.6 and I wanted to wait to see if that finished first. As a workaround, I started trying to make things work with the latest GitHub code on Saturday afternoon and had good early success.
To recap, we had five apps that we wanted to make work:
I started at the top and regenerated the apps in the spring-native-examples repository. In each app, I removed all the files and ran the jhipster --with-entities command.
rm -rf *
jhipster --with-entities
I integrated Spring Native as one does, adding repositories, a dependency, a couple of plugins, and a new profile. If you use start.spring.io and choose "native", all of these settings will be added to your build file. It's very useful for copy and pasting.
I was able to get spring-native-webflux working by deleting spring-logback.xml and adding some hints to the main class.
import org.springframework.nativex.hint.TypeHint;
@TypeHint(
types = {
org.HdrHistogram.Histogram.class,
org.HdrHistogram.ConcurrentHistogram.class
})
With spring-native-mvc, I encountered a strange "Code should not be empty" error. This was caused by having Mockito in the classpath, so I excluded the dependency and deleted all the tests that imported it. I'm not sure why I had to do this for Spring MVC and not WebFlux.
When Josh and I were hacking in September, we never attempted to get Springfox and Swagger working. Mostly because the Springfox project said they wouldn't add support for Spring Native. JHipster recently migrated to Spring Doc as a replacement and we've experienced very good support. When I received an error about springdocs, I added their native dependency to fix it.
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-native</artifactId>
<version>1.6.0</version>
</dependency>
After getting both bare-bones JHipster with WebFlux and Spring MVC to work, I moved on to angular-webflux.
The main issue I encountered was a 404 when I tried to sign in. I spent several hours trying to fix this and even logged an issue with Spring Native. When Josh and I started hacking on Monday, he invited Steve Riesenberg to join us. After a couple of hours, Steve spotted the issue. It was caused by JHipster's SpaWebFilter having a @Component annotation. We were also manually configuring it in JHipster's SecurityConfiguration:
.addFilterAt(new SpaWebFilter(), SecurityWebFiltersOrder.AUTHENTICATION)
Removing the @Component annotation makes it work the same in JVM and native mode. This is a JHipster bug that will be fixed in the next release.
After getting that fixed, a 404 happened when we were redirected back to the app. Adding a TypeHint for JwtDecoder fixed the problem.
import org.springframework.nativex.hint.TypeHint;
@TypeHint(
types = {
...
org.springframework.security.oauth2.jwt.JwtDecoder.class
})
This will be fixed in the next release of Spring Native.
Once Spring Security was functioning properly, the app worked and we moved on to postgres-webflux. The postgres-webflux app worked in JVM mode, but not after it's compiled to a native binary. At startup, it fails with an error about "no suitable constructor" for SimpleR2dbcRepository. I'm still not sure what's causing this.
Next, I moved on to getting postgres-mvc running. Last time, Josh and I figured out how to make Liquibase work with a native app, and those instructions haven't changed.
@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
})
Support for JPA now requires a JDKProxyHint, whereas it didn't need it before. Josh also didn't need it in his examples, so this might be specific to JHipster.
@JdkProxyHint(
types = {
org.springframework.data.jpa.repository.support.CrudMethodMetadata.class,
org.springframework.aop.SpringProxy.class,
org.springframework.aop.framework.Advised.class,
org.springframework.core.DecoratingProxy.class
})
I also found I had to add an EntityListener to the top of the User entity. Its superclass has the following, but it doesn't get invoked when running natively.
@EntityListeners(AuditingEntityListener.class)
For Spring MVC, I discovered that some methods didn't work when they were missing a name in their @RequestParam or @PathVariable annotations. Adding the name to the annotation fixed it for the most part. For example:
// previous
@RequestParam(required = false, defaultValue = "false") boolean eagerload
@PathVariable Long id
// works with spring native
@RequestParam(name = "eagerload", required = false, defaultValue = "false") boolean eagerload
@PathVariable("id") Long id
Thanks to this blog post for helping me figure this out.
I also experienced an issue with JHipster's LogoutResource and how it uses SpEL in a method parameter to extract the ID token from the @AuthenticationPrincipal.
@PostMapping("/api/logout")
public ResponseEntity<?> logout(HttpServletRequest request, @AuthenticationPrincipal(expression = "idToken") OidcIdToken idToken) { }
I was able to fix the issue by removing (expression = "idToken") and just getting the OidcUser, then the ID token from that.
public ResponseEntity<?> logout(HttpServletRequest request, @AuthenticationPrincipal OidcUser oidcUser) {
The final JPA issue I was unable to solve was updating an entity with many-to-many relationships. You can read more about it in this comment.
We documented all the things we did in a GitHub project called spring-native-samples. It has instructions for integrating Spring Native 0.11 into JHipster 7.4.2-SNAPSHOT in its README and sample apps to see things running. I'll do my best to keep this updated as new releases happen. In the meantime, you can compile and run any of the apps.
./mvnw package -Pprod,native -DskipTests
# start Docker containers for Keycloak and PostgreSQL
./target/<name-of-binary>
Thanks to everyone who helped us get this far!
Update: JHipster now has a Spring Native blueprint! Please see Introducing Spring Native for JHipster for more information.