diff --git a/demo-native.adoc b/demo-native.adoc new file mode 100644 index 0000000..2539da7 --- /dev/null +++ b/demo-native.adoc @@ -0,0 +1,440 @@ +:experimental: +:commandkey: ⌘ +:toc: macro +:source-highlighter: highlight.js + += Spring Native for JHipster: Serverless Full-Stack Made Easy + +This demo shows you how to use the new JHipster Native blueprint to convert a full-stack Java + React app to work with GraalVM. Why start in seconds when you can start in milliseconds?! + +*Prerequisites:* + +- https://nodejs.org/[Node 14]+ +- https://sdkman.io/[Java 17 with GraalVM] +- https://docs.docker.com/compose/install/[Docker Compose] + +_If you're on Windows, you may need to install the https://docs.microsoft.com/en-us/windows/wsl/about[Windows Subsystem for Linux] for some commands to work._ + +toc::[] + +== 🔥 Announcing the JHipster Native blueprint! + +The JHipster Native blueprint integrates Spring Native into a JHipster project based on https://github.com/mraible/spring-native-examples#readme[findings from the research by Josh Long and me]. I documented our findings in September and December 2021. + +- Sep 30, 2021: https://www.linkedin.com/pulse/jhipster-works-spring-native-matt-raible/[JHipster Works with Spring Native!] +- Dec 14, 2021: https://www.linkedin.com/pulse/jhipster-works-spring-native-part-2-matt-raible/[JHipster works with Spring Native, Part 2!] + +During this experience, I was surprised to find that Spring Native https://github.com/spring-projects-experimental/spring-native/issues/465[doesn't support caching yet]. + +In the meantime, if you're looking to start/stop your infra as fast as possible, you probably don't care about caching. Caching is made for long-lived, JVM-strong, JVM-loving apps. + +Here's how to use the https://github.com/jhipster/generator-jhipster-native[JHipster Native blueprint]: + +[source,shell] +---- +npm install -g generator-jhipster-native +jhipster-native +---- + +This will generate a JHipster app and integrate GraalVM automatically. You can build and run a native image with the following commands: + +[source,shell] +---- +./mvnw package -Pnative,prod -DskipTests +npm run ci:e2e:prepare # start docker dependencies +./target/native-executable +---- + +== Go native with Spring Native and GraalVM + +To see Spring Native + JHipster in action, let's look at a https://auth0.com/blog/full-stack-java-with-react-spring-boot-and-jhipster/[previous JHipster app I created for the Auth0 blog]. + +. Clone the example: ++ +[source,shell] +---- +git clone https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native +cd jhipster-native +---- ++ +[TIP] +==== +Want results right away? Clone the `spring-native` branch with the changes below already made: +---- +git clone -b spring-native \ + https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native +---- + +Then, skip to the <> section to continue. +==== + +. Install JHipster 7.8.1 and the JHipster Native blueprint: ++ +[source,shell] +---- +npm i -g generator-jhipster@7.8.1 +npm i -g generator-jhipster-native@1.1.2 +---- + +. Remove all the existing project files and regenerate them. The `jhipster-native` command includes parameters to disable caching because it's not supported by Spring Native, yet. ++ +[source,shell] +---- +rm -rf * +jhipster-native --with-entities --cache-provider no --no-enable-hibernate-cache +# When prompted to overwrite .gitignore, type "a" to overwrite all files +---- + +. Run the following `git checkout` commands to restore the files that were modified in the original example. ++ +[source,shell] +---- +git checkout .gitignore +git checkout README.md +git checkout demo.adoc +git checkout flickr2.jdl +git checkout screenshots +git checkout src/main/webapp/app/entities/photo/photo.tsx +git checkout src/main/webapp/app/entities/photo/photo-update.tsx +git checkout src/main/java/com/auth0/flickr2/config/SecurityConfiguration.java +git checkout src/main/resources/config/application-heroku.yml +git checkout src/main/resources/config/bootstrap-heroku.yml +git checkout Procfile +git checkout system.properties +---- + +After running the `git checkout` commands, there are several changes I made in the first tutorial that'll need to be re-applied: + +. In `src/main/resources/config/application-dev.yml`, remove the `faker` profile for Liquibase. + +. In `pom.xml`, re-add Drew Noake's `metadata-extractor` library: ++ +[source,xml] +---- + + com.drewnoakes + metadata-extractor + 2.16.0 + +---- + +. Next, modify the `createPhoto()` method in `src/main/java/com/auth0/flickr2/web/rest/PhotoResource.java` to set the metadata when an image is uploaded. ++ +[source, java] +---- +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifSubIFDDirectory; +import com.drew.metadata.jpeg.JpegDirectory; + +import javax.xml.bind.DatatypeConverter; +import java.io.BufferedInputStream; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; + +import java.time.Instant; +import java.util.Date; + +public class PhotoResource { + ... + + public ResponseEntity createPhoto(@Valid @RequestBody Photo photo) throws URISyntaxException { + log.debug("REST request to save Photo : {}", photo); + if (photo.getId() != null) { ... } + + try { + photo = setMetadata(photo); + } catch (ImageProcessingException | IOException | MetadataException ipe) { + log.error(ipe.getMessage()); + } + + Photo result = photoRepository.save(photo); + ... + } + + private Photo setMetadata(Photo photo) throws ImageProcessingException, IOException, MetadataException { + String str = DatatypeConverter.printBase64Binary(photo.getImage()); + byte[] data2 = DatatypeConverter.parseBase64Binary(str); + InputStream inputStream = new ByteArrayInputStream(data2); + BufferedInputStream bis = new BufferedInputStream(inputStream); + Metadata metadata = ImageMetadataReader.readMetadata(bis); + ExifSubIFDDirectory directory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class); + + if (directory != null) { + Date date = directory.getDateDigitized(); + if (date != null) { + photo.setTaken(date.toInstant()); + } + } + + if (photo.getTaken() == null) { + log.debug("Photo EXIF date digitized not available, setting taken on date to now..."); + photo.setTaken(Instant.now()); + } + + photo.setUploaded(Instant.now()); + + JpegDirectory jpgDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class); + if (jpgDirectory != null) { + photo.setHeight(jpgDirectory.getImageHeight()); + photo.setWidth(jpgDirectory.getImageWidth()); + } + + return photo; + } + ... +} +---- + +. Install the React libraries needed: ++ +[source,shell] +---- +npm i react-photo-album react-images +---- ++ +[IMPORTANT] +==== +In the previous tutorial, I used `react-photo-gallery`. I switched to `react-photo-album` because https://github.com/neptunian/react-photo-gallery/issues/205#issuecomment-1086995379[it supports React 17]. Because of this, you'll also need to update `src/main/webapp/app/entities/photo/photo.tsx`. + +1. Change `import Gallery from 'react-photo-gallery'` to `import PhotoAlbum from 'react-photo-album'`. +2. Change `` to ``. +==== + +. In `src/test/javascript/cypress/integration/entity/photo.spec.ts`, remove the code that sets the calculated data in the `should create an instance of Photo` test: ++ +[source,typescript] +---- +cy.get(`[data-cy="height"]`).type('99459').should('have.value', '99459'); +cy.get(`[data-cy="width"]`).type('61514').should('have.value', '61514'); +cy.get(`[data-cy="taken"]`).type('2021-10-11T16:46').should('have.value', '2021-10-11T16:46'); +cy.get(`[data-cy="uploaded"]`).type('2021-10-11T15:23').should('have.value', '2021-10-11T15:23'); +---- + +Then, you'll need to add type hints for Drew Noake's EXIF processing library in `src/main/java/com/auth0/flickr2/Flickr2App.java`: + +[source,java] +---- +@org.springframework.nativex.hint.TypeHint( + types = { + ... + com.drew.metadata.exif.ExifIFD0Directory.class, + com.drew.metadata.exif.ExifSubIFDDirectory.class, + com.drew.metadata.exif.ExifThumbnailDirectory.class, + com.drew.metadata.exif.makernotes.AppleMakernoteDirectory.class, + com.drew.metadata.exif.GpsDirectory.class, +}) +@org.springframework.nativex.hint.NativeHint(options = "-H:+AddAllCharsets") +---- + +The `@NativeHint(options = "-H:+AddAllCharsets")` solves the following exception that happens when you upload a photo: + +---- +Caused by: java.nio.charset.UnsupportedCharsetException: Cp1252 + at java.nio.charset.Charset.forName(Charset.java:528) ~[native-executable:na] + at com.drew.lang.Charsets.(Charsets.java:40) ~[na:na] +---- + +Once you've made all the changes (or cloned the `spring-native` branch), you can build your hip native binary. + +=== Build a native JHipster app + +You will need a JDK with GraalVM and its `native-image` compiler. + +. Using SDKMAN, run the following command and set it as the default: ++ +[source,shell] +---- +sdk install java 22.1.0.r17-grl +---- + +. Then, use Maven to build the project. Skip tests since there's no support for Mockito at this time. ++ +[source,shell] +---- +./mvnw package -Pnative,prod -DskipTests +---- + +=== Configure your OpenID Connect identity provider + +When you generate a JHipster app with OAuth 2.0 / OIDC for authentication, it defaults to using Keycloak. It creates a `src/main/docker/keycloak.yml` file for Docker Compose, as well as a `src/main/docker/realm-config` directory with files to auto-create users and OIDC clients. + +If you want to use Keycloak for your running app, start it with the following command: + +[source,shell] +---- +docker-compose -f src/main/docker/keycloak.yml up -d +---- + +If you'd rather use Okta or Auth0, that's possible too! + +==== Use Okta as your identity provider + +. Install the Okta CLI and run `okta register` to sign up for a new account. If you already have an account, run `okta login`. + +. Run `okta apps create jhipster`. + +. Source the `.okta.env` file to override the default Spring Security settings. ++ +[source,shell] +---- +source .okta.env +---- ++ +If you're on Windows, you can modify this file to use `set` instead of `export` and rename it to `okta.bat`. Then, run it with `okta.bat` from the command line. ++ +CAUTION: Modify your existing `.gitignore` file to have `*.env` so you don't accidentally check in your secrets! + +==== Use Auth0 as your identity provider + +To switch from Keycloak to Auth0, you only need override the Spring Security OAuth properties. You don't even need to write any code! + +Create a `.auth0.env` file in the root of your project, and fill it with the code below to override the default OIDC settings: + +[source,shell] +---- +export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https:/// +export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID= +export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET= +export JHIPSTER_SECURITY_OAUTH2_AUDIENCE=https:///api/v2/ +---- + +You'll need to create a new web application in Auth0 and fill in the `<...>` placeholders before this works. + +===== Create an OpenID Connect app on Auth0 + +. Log in to your Auth0 account (or https://auth0.com/signup[sign up] if you don't have an account). You should have a unique domain like `dev-xxx.eu.auth0.com`. + +. Press the **Create Application** button in the https://manage.auth0.com/#/applications[Applications section]. Use a name like `JHipster Native!`, select `Regular Web Applications`, and click **Create**. + +. Switch to the **Settings** tab and configure your application settings: ++ +- Allowed Callback URLs: `\http://localhost:8080/login/oauth2/code/oidc` +- Allowed Logout URLs: `\http://localhost:8080/` + +. Scroll to the bottom and click **Save Changes**. + +. Copy your Auth0 domain, client ID, and client secret into the `.auth0.env` file you created earlier. Then, run `source .auth0.env`. + +. In the https://manage.auth0.com/#/roles[roles] section, create new roles named `ROLE_ADMIN` and `ROLE_USER`. + +. Create a new user account in the https://manage.auth0.com/#/users[users] section. Click on the **Role** tab to assign the roles you just created to the new account. ++ +_Make sure your new user's email is verified before attempting to log in!_ + +. Next, head to **Auth Pipeline** > **Rules** > **Create**. Select the `Empty rule` template. Provide a meaningful name like `Group claims` and replace the Script content with the following. ++ +[source,js] +---- +function(user, context, callback) { + user.preferred_username = user.email; + const roles = (context.authorization || {}).roles; + + function prepareCustomClaimKey(claim) { + return `https://www.jhipster.tech/${claim}`; + } + + const rolesClaim = prepareCustomClaimKey('roles'); + + if (context.idToken) { + context.idToken[rolesClaim] = roles; + } + + if (context.accessToken) { + context.accessToken[rolesClaim] = roles; + } + + callback(null, user, context); +} +---- + +. This code is adding the user's roles to a custom claim (prefixed with `\https://www.jhipster.tech/roles`). Click **Save changes** to continue. + +TIP: Want to have all these steps automated for you? Add a 👍 to https://github.com/auth0/auth0-cli/issues/351[issue #351] in the Auth0 CLI project. + +=== Run your native JHipster app + +After you've built your app, it will be available in `target/native-executable`. + +Start Keycloak or source your Okta/Auth0 settings. Then, run the following commands: + +[source,shell] +---- +npm run ci:e2e:prepare # start docker dependencies +./target/native-executable +---- + +== What's the performance like? + +The native binary starts in just over 500ms (577ms) on my 2019 MacBook Pro with a 2.4 GHz 8-Core Intel Core i9 processor and 64 GB of RAM. + +If I start it in JVM mode with Maven, it takes a little over four seconds. + +As far as build time goes, Spring Native says: + +---- +Finished generating 'native-executable' in 3m 15s. +---- + +If I build a Docker image with the native binary: + +---- +mvn spring-boot:build-image -Pprod +---- + +It takes a while the first time: + +---- +Total time: 07:24 min +---- + +And it's slightly faster the second time: + +---- +Total time: 06:43 min +---- + +The amount of memory used after starting: `178 MB`. + +The amount of memory used after running `npm run e2e`: `211 MB`. + +In the interest of full disclosure, here's the command I used to measure the amount of memory used: + +[source,shell] +---- +ps -o pid,rss,command | grep --color native | awk '{$2=int($2/1024)" MB";}{ print;}' +---- + +.What about the MacBook Pro M1 Max? +**** + +My MacBook Pro (16-inch, 2021) with Apple M1 Max builds _much_ faster but uses more memory. + +[cols="<,^,^",options=header] +|=== +|Metric | M1 | Intel + +|Build time | `1m 49s` | `3m 15s` +|Milliseconds to start | `433` | `577` +|MB memory used after starting | `204` | `178` +|Memory used after `npm run e2e` | `249` | `211` +|=== + +// start: started 3 times and took fastest +**** + +== Should you use GraalVM instead of the JVM? + +++++ + +++++ + +== Go Serverless with JHipster and GraalVM! + +⚡️ Find the code on GitHub: https://github.com/oktadev/auth0-full-stack-java-example/tree/spring-native[@oktadev/auth0-full-stack-java-example/tree/spring-native] + +👩‍🏫 Read the blog post: https://developer.okta.com/blog/2022/03/03/spring-native-jhipster[Introducing Spring Native for JHipster: Serverless Full-Stack Made Easy] + diff --git a/demo.adoc b/demo.adoc index 1e563d6..48c3964 100644 --- a/demo.adoc +++ b/demo.adoc @@ -1,6 +1,7 @@ :experimental: :commandkey: ⌘ :toc: macro +:source-highlighter: highlight.js = Full Stack Java with React, Spring Boot, and JHipster Demo Steps @@ -44,12 +45,12 @@ Why React? Because it's currently the https://trends.google.com/trends/explore?q . Choose the defaults except for: - - name: `flickr2` - - package: `com.auth0.flickr2` - - authentication: `OAuth 2.0 / OIDC` - - client: `React` - - bootswatch: `United > Dark` - - testing: `Cypress` +- name: `flickr2` +- package: `com.auth0.flickr2` +- authentication: `OAuth 2.0 / OIDC` +- client: `React` +- bootswatch: `United > Dark` +- testing: `Cypress` === Verify Everything Works with Cypress and Keycloak @@ -89,8 +90,8 @@ WARNING: Modify your existing .gitignore file to have *.env so you don't acciden . Switch to the *Settings* tab and configure your application settings: - - Allowed Callback URLs: `\http://localhost:8080/login/oauth2/code/oidc` - - Allowed Logout URLs: `\http://localhost:8080/` +- Allowed Callback URLs: `\http://localhost:8080/login/oauth2/code/oidc` +- Allowed Logout URLs: `\http://localhost:8080/` . Scroll to the bottom and click *Save Changes*. @@ -471,8 +472,8 @@ Use `heroku logs --tail` to watch your logs. . https://auth0.com/auth/login[Log in] to your Auth0 account, navigate to your app, and add your Heroku URLs as valid redirect URIs: - - Allowed Callback URLs: `\https://flickr-2.herokuapp.com/login/oauth2/code/oidc` - - Allowed Logout URLs: `\https://flickr-2.herokuapp.com` +- Allowed Callback URLs: `\https://flickr-2.herokuapp.com/login/oauth2/code/oidc` +- Allowed Logout URLs: `\https://flickr-2.herokuapp.com` . Test it with https://developers.google.com/web/tools/lighthouse/[Lighthouse] or https://webpagetest.org/[WebPageTest]. @@ -485,4 +486,3 @@ Wahoo! You streamlined your path to full-stack Java development with JHipster!! 🤓 Find the code on GitHub: https://github.com/oktadev/auth0-full-stack-java-example[@oktadev/auth0-full-stack-java-example] 👀 Read the blog post: https://auth0.com/blog/full-stack-java-with-react-spring-boot-and-jhipster/[Full Stack Java with React, Spring Boot, and JHipster] -