多年来,我开发了大量的Java应用程序。我在90年代末开始写Java代码,在尝试其他服务器端语言之前,我花了几年时间做Java。当我第一次尝试用Ruby on Rails、Python和Node.js构建应用程序时,我留下了深刻的印象--它们的启动速度都是超快的!
启动速度快是很酷,但我们Java社区的人经常问,随着时间的推移,它的性能如何?Java虚拟机因其性能和长期优化而闻名。
无服务器刚出来的时候,我对它嗤之以鼻。主要是因为我是一名Java开发者,我的应用并不是在几毫秒内启动的。他们还使用了大量的内存,而且看不到任何希望。
然后,GraalVM出现了。在过去的几年里,它得到了许多Java框架的支持,并使他们的应用程序在几毫秒内就能启动。
今天,我很自豪地宣布,这种能力现在也可以用于你的JHipster 7+应用程序了!
先决条件:
-
使用GraalVM+的Java 17
-
Docker桌面
为什么要关注无服务器?
无服务器是一种云计算执行模式,能够以更简单、更经济的方式构建和运营云原生应用。
就是这样!
对于拥有高流量和大额云费用的公司来说,无服务器是有意义的。他们每个月可以节省数百万美元。
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
想马上得到结果吗?克隆 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 命令,有几个我在第一个教程中的修改需要重新应用:
-
在
src/main/resources/config/application-dev.yml,删除Liquibase的faker配置文件。 -
在
pom.xml中,重新添加 Drew Noake 的metadata-extractor库。<dependency> <groupId>com.drewnoakes</groupId> <artifactId>metadata-extractor</artifactId> <version>2.16.0</version> </dependency> -
接下来,修改
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; } ... } -
安装所需的React库。
npm i react-photo-gallery@8 --force npm i react-images -
在
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应用程序的配置,并为您做了几件事:
- 创建一个具有正确重定向URI的OIDC应用程序。
- 登录:
http://localhost:8080/login/oauth2/code/oidc和http://localhost:8761/login/oauth2/code/oidc - 注销:
http://localhost:8080和http://localhost:8761
- 登录:
- 创建JHipster期望的
ROLE_ADMIN和ROLE_USER组 - 将你的当前用户添加到
ROLE_ADMIN和ROLE_USER组中。 - 在你的默认授权服务器中创建一个
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_ADMIN 和ROLE_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月记录了我们的发现:
-
2021年9月30日:JHipster与Spring Native一起工作!
-
2021年12月14日:JHipster与Spring Native合作,第二部分!
意想不到的是,我们要解决的最难的问题之一是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的下一个版本中出现。