JHipster的Spring Native:轻松实现无服务器全栈教程

333 阅读10分钟

多年来,我开发了大量的Java应用程序。我在90年代末开始写Java代码,在尝试其他服务器端语言之前,我花了几年时间做Java。当我第一次尝试用Ruby on Rails、Python和Node.js构建应用程序时,我留下了深刻的印象--它们的启动速度都是超快的!

启动速度快是很酷,但我们Java社区的人经常问,随着时间的推移,它的性能如何?Java虚拟机因其性能和长期优化而闻名。

无服务器刚出来的时候,我对它嗤之以鼻。主要是因为我是一名Java开发者,我的应用并不是在几毫秒内启动的。他们还使用了大量的内存,而且看不到任何希望。

然后,GraalVM出现了。在过去的几年里,它得到了许多Java框架的支持,并使他们的应用程序在几毫秒内就能启动。

今天,我很自豪地宣布,这种能力现在也可以用于你的JHipster 7+应用程序了!

先决条件

  • 使用GraalVM+的Java 17

  • Docker桌面

为什么要关注无服务器?

来自IBM的《什么是无服务器计算?

无服务器是一种云计算执行模式,能够以更简单、更经济的方式构建和运营云原生应用。

就是这样!

对于拥有高流量和大额云费用的公司来说,无服务器是有意义的。他们每个月可以节省数百万美元。

GraalVM允许所有人使用无服务器的Java!

我玩GraalVM和支持它的Java框架大约有18个月了。在做了大量的研究后,我在博客中介绍了用Micronaut、Quarkus和Spring Boot构建原生Java应用

我在LinkedIn上的JHipster Works with Spring Native!中写到了这一点,并创建了一个JHipster问题来自动化我们的学习成果。

什么是Spring Native?

Spring Native提供了一个API来配置Spring Boot和GraalVM,使那些不容易被发现的类变得可以被识别。例如,那些使用反射的实例化的类。这是对Spring Boot的一个非常巧妙的扩展,而且很可能在Spring Boot 3中消失,因为它默认是原生的。

由于我们在SF JUG上的成功演讲,Josh和我被邀请在12月的花园州JUG上演讲。我们低调了几个月,在12月重新开始了研究。我在《JHipster与Spring Native的合作,第二部分》中写到了这一点

🔥宣布JHipster Native蓝图!

2022年1月底,我们(JHipster团队)将JHipster升级到使用Spring Boot 2.6,并发布了7.6.0版本。

2月初,我更新了Josh和我一直用于研究的Spring Native与JHipster的例子。我准备开始使用JHipster模块来自动化Spring Native的集成。当我向JHipster团队询问实现它的最佳方式时,Marcelo Shima自愿创建了最初的蓝图。

那是在一个星期五的下午(山区时间或MT)。到了第二天早上,Marcelo完成了MVP,并提供了重现我们的例子的步骤。😳

我感到很惊讶我记得我告诉(我的合作伙伴)Trish,他在几个小时内完成了(我估计的)一周的工作。

今天,我很自豪地宣布,JHipster Native蓝图已经面世!它的使用方法如下: 1.以下是如何使用它。

npm install -g generator-jhipster-native@1.0
jhipster-native

这将生成一个JHipster应用程序并自动整合GraalVM。你可以通过以下命令构建和运行一个原生镜像。

./mvnw package -Pnative,prod -DskipTests
npm run ci:e2e:prepare # start docker dependencies
./target/native-executable
Gradle目前不是一个选项,但如果我们为JHipster Native蓝图添加支持,它就可以成为一个选项。

使用Spring Native和GraalVM实现原生

为了看看Spring Native+JHipster的运行情况,让我们看看之前我为Auth0博客创建的JHipster应用。首先,克隆这个例子:

git clone https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native
cd jhipster-native

想马上得到结果吗?克隆spring-native 分支,下面的修改已经完成。

git clone -b spring-native https://github.com/oktadev/auth0-full-stack-java-example.git jhipster-native

然后,跳到配置你的OpenID Connect身份提供者部分,继续。

安装JHipster 7.7.0和JHipster Native蓝图:

npm i -g generator-jhipster@7.7
npm i -g generator-jhipster-native@1.0

然后,删除所有现有的项目文件并重新生成它们。jhipster-native 命令包括禁用缓存的参数,因为Spring Native还不支持缓存。

rm -rf *
jhipster-native --with-entities --cache-provider no --no-enable-hibernate-cache
# When prompted to overwrite .gitignore, type "a" to overwrite all files

如果你在IntelliJ IDEA中打开项目,你可以使用提交工具窗口(macOS上的⌘+0或Linux/Windows上的Ctrl+0)来查看改变的文件。

接下来,运行下面的git checkout 命令来恢复原始例子中被修改的文件:

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

如果你不想使用命令行,你可以在每个文件上点击右键,选择回滚

如果你运行了git checkout 命令,有几个我在第一个教程中的修改需要重新应用:

  1. src/main/resources/config/application-dev.yml ,删除Liquibase的faker 配置文件。

  2. pom.xml 中,重新添加 Drew Noake 的metadata-extractor 库。

    <dependency>
        <groupId>com.drewnoakes</groupId>
        <artifactId>metadata-extractor</artifactId>
        <version>2.16.0</version>
    </dependency>
    
  3. 接下来,修改src/main/java/com/auth0/flickr2/web/rest/PhotoResource.java 中的createPhoto() 方法,在上传图片时设置元数据。

    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<Photo> createPhoto(@Valid @RequestBody Photo photo) throws Exception {
            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;
        }
        ...
    }
    
  4. 安装所需的React库。

    npm i react-photo-gallery@8 --force
    npm i react-images
    
  5. src/test/javascript/cypress/integration/entity/photo.spec.ts ,删除should create an instance of Photo 测试中设置计算数据的代码。

    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');
    

然后,你需要在src/main/java/com/auth0/flickr2/Flickr2App.java 中为Drew Noake的EXIF处理库添加类型提示。

@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")

@NativeHint(options = "-H:+AddAllCharsets") ,解决当你上传照片时发生的以下异常。

Caused by: java.nio.charset.UnsupportedCharsetException: Cp1252
    at java.nio.charset.Charset.forName(Charset.java:528) ~[native-executable:na]
    at com.drew.lang.Charsets.<clinit>(Charsets.java:40) ~[na:na]

一旦你做了所有的修改(或者克隆了spring-native 分支),你就可以构建你的Hip原生二进制文件了。

构建一个本地的JHipster应用程序

你将需要一个带有GraalVM和其native-image 编译器的JDK。使用SDKMAN,运行以下命令并将其设置为默认:

sdk install java 21.3.0.r17-grl

将本地扩展添加到JDK中:

gu install native-image

然后,使用Maven来构建项目。跳过测试,因为目前还不支持Mockito。

./mvnw package -Pnative,prod -DskipTests

这个过程需要几分钟的时间来完成。

配置你的OpenID Connect身份提供者

当你用OAuth 2.0 / OIDC生成一个JHipster应用进行认证时,它默认使用Keycloak。它为Docker Compose创建了一个src/main/docker/keycloak.yml 文件,以及一个src/main/docker/realm-config 目录,其中有自动创建用户和OIDC客户端的文件。

如果你想为你正在运行的应用程序使用Keycloak,用以下命令启动它:

docker-compose -f src/main/docker/keycloak.yml up -d

如果你想使用Okta或Auth0,那也是可以的!

使用Okta作为你的身份提供者

在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,注册一个新账户。如果您已经有一个账户,运行okta login 。然后,运行okta apps create jhipster 。选择默认的应用程序名称,或根据您的需要进行更改。 接受为您提供的默认重定向URI值。

Okta CLI是做什么的?

Okta CLI简化了对JHipster应用程序的配置,并为您做了几件事:

  1. 创建一个具有正确重定向URI的OIDC应用程序。
    • 登录:http://localhost:8080/login/oauth2/code/oidchttp://localhost:8761/login/oauth2/code/oidc
    • 注销:http://localhost:8080http://localhost:8761
  2. 创建JHipster期望的ROLE_ADMINROLE_USER
  3. 将你的当前用户添加到ROLE_ADMINROLE_USER 组中。
  4. 在你的默认授权服务器中创建一个groups ,并将用户的组添加到其中。

注意http://localhost:8761* 重定向URI是为JHipster注册处准备的,在用JHipster创建微服务时经常使用。Okta CLI默认会添加这些。

完成后,你会看到如下的输出:

Okta application configuration has been written to: /path/to/app/.okta.env

运行cat .okta.env (或Windows上的type .okta.env ),查看你的应用程序的发行者和凭证。它将看起来像这样(除了占位符的值将被填充)。

export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI="/oauth2/default"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID="{clientId}"
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET="{clientSecret}"

注意:您也可以使用Okta管理控制台来创建您的应用程序。参见在Okta上创建一个JHipster应用程序以获得更多信息。

您需要将Okta CLI创建的.okta.env 文件作为来源,以覆盖默认的Spring Security设置。

source .okta.env
如果你在Windows上,你可以修改这个文件,使用set ,而不是export ,并将其重命名为okta.bat 。然后,从命令行中用okta.bat
修改你现有的.gitignore 文件,使其具有*.env ,这样你就不会意外地检入你的秘密了

如果你已经为Okta配置了你的应用,只是想看看它的运行情况,请跳到运行你的本地JHipster应用

使用Auth0作为你的身份提供者

要从Keycloak切换到Auth0,覆盖Spring Security的OAuth属性。你甚至不需要写任何代码!

要看看它是如何工作的,在你的项目根部创建一个.auth0.env 文件,然后用下面的代码填充它,以覆盖默认的OIDC设置。

export SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OIDC_ISSUER_URI=https://<your-auth0-domain>/
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_ID=<your-client-id>
export SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OIDC_CLIENT_SECRET=<your-client-secret>
export JHIPSTER_SECURITY_OAUTH2_AUDIENCE=https://<your-auth0-domain>/api/v2/

你需要在Auth0中创建一个新的网络应用,并在这之前填写<…​> 占位符。

在Auth0上创建一个OpenID Connect应用

登录到你的Auth0账户(如果你没有账户,也可以注册)。你应该有一个独特的域名,如dev-xxx.eu.auth0.com

按下应用程序部分创建应用程序按钮。使用一个像JHipster Native! ,选择Regular Web Applications ,然后点击创建

切换到设置标签,配置你的应用程序设置:

  • 允许的回调URLs。http://localhost:8080/login/oauth2/code/oidc

  • 允许的注销URLs。http://localhost:8080/

滚动到底部,点击保存更改

将你的Auth0域名、客户ID和客户秘密复制到你之前创建的.auth0.env 文件中。然后,运行source .auth0.env

角色部分,创建名为ROLE_ADMINROLE_USER 的新角色。

用户部分创建一个新的用户账户。单击 "角色"选项卡,将你刚刚创建的角色分配给新的帐户。

在尝试登录之前,请确保你的新用户的电子邮件是经过验证的!

接下来,前往Auth Pipeline>Rules>Create。选择Empty rule 模板。提供一个有意义的名字,如Group claims ,并将脚本内容替换为以下内容。

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);
}

这段代码是将用户的角色添加到一个自定义的索赔(前缀为https://www.jhipster.tech/roles )。单击 "保存更改"以继续。

想让所有这些步骤为你自动化吗?在Auth0 CLI项目中的问题#351中添加👍。

运行你的本地JHipster应用程序

在你建立了你的应用程序后,它将在target/native-executable 。启动Keycloak或源码你的Okta/Auth0设置。然后,运行以下命令。

npm run ci:e2e:prepare # start docker dependencies
./target/native-executable

它应该在一秒钟内启动!

JHipster Native蓝图是做什么的?

JHipster Native插件根据Josh Long和我的研究结果,将Spring Native整合到JHipster项目中。我在2021年9月和12月记录了我们的发现:

意想不到的是,我们要解决的最难的问题之一是JPA和关系。在JVM模式下,一切运作正常。当在本地模式下运行时,出现了一个异常。解决方案花了好几天才想出来,但解决起来却很简单。我只需要为java.util.HashSet.class 添加一个类型提示。 🤯

在这次经历中,我惊讶地发现Spring Native还不支持缓存。我相信这个支持很快就会被社区加入。同时,如果你想尽可能快地启动/停止你的infra,你可能不关心缓存。缓存是为寿命长、JVM强、喜欢JVM的应用而生的。

性能是怎样的?

在我的2019年MacBook Pro上,原生二进制的启动时间刚刚超过500毫秒(581毫秒),它配备了2.4GHz的8核英特尔酷睿i9处理器和64GB的内存。

如果我用Maven在JVM模式下启动它,只需要不到5秒。 就构建时间而言,Spring Native说:

Finished generating 'native-executable' in 3m 10s.

如果我用本地二进制文件构建一个Docker镜像。

mvn spring-boot:build-image -Pprod

第一次要花点时间:

Total time:  06:30 min

而第二次则稍快:

Total time:  06:18 min

启动后使用的内存量:180 MB

运行后使用的内存量npm run e2e:196 MB

为了完全公开,这是我用来测量所使用的内存量的命令:

ps -o pid,rss,command | grep --color native | awk '{$2=int($2/1024)" MB";}{ print;}'

那M1 Max呢?这还不是一个选项,但可能会在GraalVM的下一个版本中出现