从零放弃学习 Spring - 在 Native Image 中使用 Bouncy Castle
基于 Bouncy Castle 的 RSA 实现加解密
我的开发环境是 Oracle GraalVM 17.0.8+9.1,Linux x64。
完整代码参考 native-bc。
加密代码就不贴了,参考我的仓库代码,只需要执行:
1 | make deps keys run |
Fallback Image
在上面环境基础上,执行命令构建 Native Image:
1 | make native |
这一步虽然构建出了可执行文件并且也可以正常运行,但请注意控制台输出:
1 | Warning: Image 'rsa' is a fallback image that requires a JDK for execution (use --no-fallback to suppress fallback image generation and to print more detailed information why a fallback image was necessary). |
不难看出,Native Image 为了做到最大兼容性,默认情况下构建出来的可执行文件可能仍然依赖 JDK,如果想要生成不依赖 JDK 的可执行文件,还需要增加一个 --no-fallback
参数。
Image heap writing found a class not seen during static analysis…
为了构建不依赖 JDK 的可执行文件,尝试执行 make native-no-fallback
构建,然后出现异常:
1 | > com.oracle.svm.core.util.VMError$HostedError: com.oracle.svm.core.util.UserError$UserException: Image heap writing found a class not seen during static analysis. Did a static field or an object referenced from a static field change during native image generation? For example, a lazily initialized cache could have been initialized during image generation, in which case you need to force eager initialization of the cache before static analysis or reset the cache using a field value recomputation. |
这个异常应该是一个 Bug,我目前没找到正确的解决方法,包括添加元数据配置,有一个可以不出现异常的办法是把 GraalVM 回滚到 22.X 版本。顺便一提,GraalVM 目前命名已经变成 GraalVM Oracle JDK 了,所以版本直接跟着官方的版本走,早期则是年份作为大版本,比如 2022 发布的版本为 GraalVM 22.X。
实际上,经过我测试发现还有一种可以在当前版本绕过这个异常的办法,就是使用 Spring Boot 框架构建 Native Image(我只测试过 Spring Boot 框架,没有测试过其他框架)。其实我也不理解为什么 Spring Boot 构建可以避免上述的异常。
no such algorithm: RSA for provider BC…
参考项目代码 native-spring-bc。
把 RSA 加解密代码移入 Spring Boot 项目,使用 mvn -Pnative native:compile
顺利构建出了可执行文件,但是运行时又出现了一个新异常:
1 | Exception in thread "main" org.bouncycastle.openssl.PEMException: unable to convert key pair: no such algorithm: RSA for provider BC |
这是因为 Native Image 的 Security Provider 都是在构建期间确定的,不能在运行期间注册。这里分享两种可以在构建期间注册 Security Provider 的方式。
使用 org.graalvm.nativeimage.hosted.Feature 在构建时执行代码
这个方法参考自 insinfo/java_native。
首先需要添加一个依赖:
1 | <dependency> |
然后创建 BouncyCastleFeature
类实现 org.graalvm.nativeimage.hosted.Feature
:
1 | package com.example.nativebc; |
这个 Feature 会在构建时做这几件事:
- 把
org.bouncycastle
下所有子包都标记为构建时初始化 - 单独标记两个需要运行时初始化的类
- 注册 BouncyCastleProvider
还没完,创建构建配置 META-INF/native-image/native-image.properties
,启用自定义的 Feature:
1 | Args = --features=com.example.nativebc.BouncyCastleFeature |
然后执行命令:
1 | mvn -Pnative native:compile |
会观察到构建时输出:
1 | ... |
这说明自定义的 Feature 生效了,顺利的话你可以在 target/
目录下找到可以正常执行的 nativebc
。
完整代码参考 native-spring-bc。
通过 java.security 注册 Security Provider
上面是通过 Feature 机制,在构建期间完成 Security Provider 的注册,需要引入官方的 SDK 和指定 Feature。官方文档 [JCA Security Services in Native Image] 中提到了另外一种注册 Security Provider 的办法,就是修改 java.security
文件。Native Image 构建时会从 java.security 读取预定义的 Provider 并按顺序加载。
查看 GraalVM 的 java.security 文件:
1 | $ rg 'security.provider.\d+' $GRAALVM_HOME/conf/security/java.security |
java.security 文件内容开头的注释也向开发者说明了添加 Provider 的办法,也就是直接把 BC 的 Provider 追加到这个列表后面就可以了:
1 | security.provider.13=org.bouncycastle.jce.provider.BouncyCastleProvider |
NOTE: 虽然 Native Image 不支持运行时注册 Security Provider,但是对于已经注册的 Provider,允许 removeProvider 以后根据自己想要的顺序重新 addProvider/insertProviderAt
在 java.security 中添加 Provider 相对于前文 Feature 代码中的 Security.addProvider
,所以还要通过配置 META-INF/native-image/native-image.properties
完成 Feature 代码中另外两个功能:
1 | Args = --initialize-at-build-time=org.bouncycastle \ |
配置完成后就是构建和运行测试,没有意外的情况下不会有意外。
补充一下,如果不想修改 GraalVM JDK 中的配置文件,可以通过添加构建参数 -Djava.security.properties=path/to/security.properties
指定一个自定义的 security.properties,在自定义文件中注册 Security Provider。
JCE cannot authenticate the provider BC…
就算用了 Spring Boot 也不是非常顺利,虽然到目前 Native Image 可以成功构建和运行,但现在直接运行 Jar 文件反而会出现异常:
1 | java.lang.SecurityException: JCE cannot authenticate the provider BC |
这是因为 Spring Boot 的 Jar in Jar 实现和 JCE 存在一些冲突(我猜的),有一个解决办法就是直接从本地文件加载 BC 对应的 jar 文件,而不是再使用 Spring 的 Jar in Jar,或者等待官方去修复这个问题。
最后
终于靠着瞎编把这个系列写完了。这一个月边学边写博客,全都是为了分享一下之前研究 GraalVM Native Image 构建 Bouncy Castle 的收获,主打一个为了碟醋包饺子。这篇本来是合在上一篇博客里,但是内容比较独立,还是决定拆出来单独发了。
吐槽一下 GraalVM,这个项目也是有点迷惑操作的。我 6 月刚开始研究 Native Image 时,一开始看到官网上的 GraalVM 是 23.0 版本,结果有一天官网突然改版,所有文档链接全部失效,从搜索引擎进文档花式 404 页面,我只能靠着修改链接上的格式找到了部分文档(后来知道在 Github 仓库上找文档)。对比不同版本文档也发现了有些不错的 feature 在调整后消失或者改名了,但是这种改动需要自己理解,如果你搜到了老文档,但是在新文档里没有或者对不上都是一件非常平常的事情。官网改后不久,GraalVM 也变成了 Oracle GraalVM JDK,下载安装的方式也变了,随着这种变动,ghcr 上的 GraalVM 镜像就对不上了,官方也没有及时更新,我在内部 CI/CD 中集成时,还要自己制作 builder 镜像(或者我应该直接使用 spring-boot:build-image 构建的思路?)。一个背靠大公司,备受社区期待的产品,经过了 5 年的打磨以后还是这个状态,我是不理解的。当然我不是一个专业的 Java 开发者,我也就是在自己的角度发表一些偏见。