# Java云原生开发初体验报告

# 背景

前段时间在考虑做一款小工具，功能非常简单，调用多个HTTP接口，分析处理返回的数据，生成Excel文件即可。 为了尽量的让这个工具的实用性更高，我首先想到Java的云原生开发方案，直接构建为可执行文件，不需要使用的人再去安装jre运行环境，或者是带着庞大的jre文件发出。再者，我也想试试Java的云原生方案到底好不好用。

# 技术选型

因为一直在使用Spring开发业务，所以我这次直接使用了Spring Native

# 开发过程

> 注：均基于 macOS

## 开发环境安装

Spring官网的文章写的非常好，直接参考官文就可以了。[Spring官方文档](https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/) 我选择安装了sdk man，然后

```shell
sdk install java 22.1.r11-nik
```

剩下的就是直接用spring native创建一个项目就可以了，这个非常简单。

## 编译构建

1. 如果你用了阿里云的maven仓库，请记得切换成apache的原库
    
2. macOS默认mvn命令指定的jdk版本可能不是你新安装的GraalVM（像我本地就有七八个不同的JDK版本），会出现一些编译错误，那么你需要创建`~/.mavenrc`文件
    

```plaintext
// 路径记得自己改成正确的
export JAVA_HOME=/Users/baofeidyz/.sdkman/candidates/java/current
```

## 报错集锦

### 报错1

```plaintext
gu install native-image
Downloading: Component catalog from download.bell-sw.com
Error: Unknown component: native-image
```

在高版本里面，这个`native-image`好像不需要手动安装了（也有可能是我之前安装过）[Install fails in 19.2.0 with gu · Issue #1665 · oracle/graal · GitHub](https://github.com/oracle/graal/issues/1665) 然后我直接运行了一下`native-image`发现已经有了

### 报错2 反射错误

```shell
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `com.baofeidyz.XXXDTO` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
at [Source: UNKNOWN; byte offset: #UNKNOWN] (through reference chain: java.util.ArrayList[0])
at com.fasterxml.jackson.databind.DeserializationContext.reportBadDefinition(DeserializationContext.java:1904) 
at com.fasterxml.jackson.databind.DatabindContext.reportBadDefinition(DatabindContext.java:400)
at com.fasterxml.jackson.databind.DeserializationContext.handleMissingInstantiator(DeserializationContext.java:1349)
at com.fasterxml.jackson.databind.deser.BeanDeserializerBase.deserializeFromObjectUsingNonDefault(BeanDeserializerBase.java:1415)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserializeFromObject(BeanDeserializer.java:351)
at com.fasterxml.jackson.databind.deser.BeanDeserializer.deserialize(BeanDeserializer.java:184) 
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer._deserializeFromArray(CollectionDeserializer.java:355) 
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:244) 
at com.fasterxml.jackson.databind.deser.std.CollectionDeserializer.deserialize(CollectionDeserializer.java:28) 
at com.fasterxml.jackson.databind.deser.DefaultDeserializationContext.readRootValue(DefaultDeserializationContext.java:323)
at com.fasterxml.jackson.databind.ObjectMapper._readValue(ObjectMapper.java:4650) 
at com.fasterxml.jackson.databind.ObjectMapper.readValue(ObjectMapper.java:2856) 
有省略部分堆栈
```

这个报错只有在`GraalVM`下运行时才会出现，我最开始以为是因为我用了`lombok`然后导致的编译问题，同时我也有注意到，`Spring Native`本身是直接带了`lombok`的，我觉得不太可能吧。 但是我还是尝试去掉了`lombok`，当然并没有什么用。

然后我又开始在网上寻找一些蛛丝马迹，看看有没有其他人也遇到了类似的问题。但是Java Native问题的搜索真的是非常难找，因为这类问题有可能因为其他的原因也会出现，并且由于Spring Native使用人非常的少，你很难在Google的结果中找到真正的答案。我也尝试使用`site:github.com`再去Google搜索，仍没有找到答案。 我甚至都想去提issue了 😂 [GitHub - graalvm/native-build-tools: Native-image plugins for various build tools](https://github.com/graalvm/native-build-tools)

我后来咨询了一位做过Spring Native开发的朋友，他告诉我，可能是因为反射导致的，需要增加配置。 我顺着这位朋友的思路，去看了一下反射配置的问题，正好在[这篇文章](https://docs.spring.io/spring-native/docs/current/reference/htmlsingle/#_annotated_hints)中给出了一个示例

```java
@TypeHint(types = Data.class, typeNames = "com.example.webclient.Data$SuperHero") 
@SpringBootApplication
public class SampleApplication { // ... }
```

我顺着这个例子，修改了一下代码

```java
@TypeHint(types = XXXDTO.class, typeNames = "com.baofeidyz.XXXDTO")  
@SpringBootApplication  
public class MySpringNativeApplication {
// ...
}
```

确实解决了这个`jackson`反序列化导致的错误，但我又遇到了新的错误。

### 报错3 缺少字符集

```plaintext
java.nio.charset.UnsupportedCharsetException: CP1252
        at java.nio.charset.Charset.forName(Charset.java:529)
        at org.apache.poi.util.LocaleUtil.<clinit>(LocaleUtil.java:56)
        at org.apache.poi.ss.usermodel.DataFormatter.<init>(DataFormatter.java:242)
        at org.apache.poi.ss.usermodel.DataFormatter.<init>(DataFormatter.java:233) 
        at org.apache.poi.ss.formula.functions.TextFunction.<clinit>(TextFunction.java:33) 
        at org.apache.poi.ss.formula.atp.AnalysisToolPak.createFunctionsMap(AnalysisToolPak.java:82)
        at org.apache.poi.ss.formula.atp.AnalysisToolPak.<init>(AnalysisToolPak.java:47)
        at org.apache.poi.ss.formula.atp.AnalysisToolPak.<clinit>(AnalysisToolPak.java:33) 
        at org.apache.poi.ss.formula.udf.AggregatingUDFFinder.<clinit>(AggregatingUDFFinder.java:35)
        at java.lang.Class.ensureInitialized(DynamicHub.java:518) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:155) ~[na:na]
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:226) ~[na:na]
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:214) ~[na:na]
        // 有省略堆栈信息
```

这个看起来像是poi在操作的时候，遇到了一个`chartset`问题，spring有一个专门的[仓库](https://github.com/spring-projects-experimental/spring-native)我在这个仓库里面找到了这个[issue](https://github.com/spring-projects-experimental/spring-native/issues/966)，我看到一个人也遇到了这个`charset`的问题，但是他是`mysql`的时候遇到的，他提到加了一个参数用于解决这个问题。 紧接着，我又找到了几个issue：

* [I tried to build a native image program with POI, but it failed · Issue #3191 · oracle/graal · GitHub](https://github.com/oracle/graal/issues/3191)
    
* [issue with okhttp with graalvm-ce-19.0.0 · Issue #1294 · oracle/graal · GitHub](https://github.com/oracle/graal/issues/1294)
    
* [UnsupportedEncodingException when running native-image generated binary · Issue #1370 · oracle/graal · GitHub](https://github.com/oracle/graal/issues/1370)
    

了解到说，需要在`native-image`编译的时候，增加一个`AddAllCharsets`参数 但是我是使用的spring native的maven插件，我的构建命令是`mvn -Pnative`，我并没有直接使用`native-image`，我不知道怎么增加这个参数才是对的。 此外，我还看了两份文档：

* 关于这个如何查看堆信息之类的：[GraalVM Dashboard](https://www.graalvm.org/dashboard/?ojr=help%3Btopic%3Dgetting-started.md)
    
* 关于`native-image`这个命令如何使用的：[https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2\_A4.pdf](https://www.graalvm.org/uploads/quick-references/native-image-quick-reference-v2_A4.pdf) 然后我又去查了一下Spring Native自己维护的一个hints项目：[spring-native/spring-native-configuration/src/main/java at main · spring-projects-experimental/spring-native · GitHub](https://github.com/spring-projects-experimental/spring-native/tree/main/spring-native-configuration/src/main/java) 然后又去研究了一下Spring Native的脚本：[spring-native/compileWithMaven.sh at main · spring-projects-experimental/spring-native · GitHub](https://github.com/spring-projects-experimental/spring-native/blob/main/scripts/compileWithMaven.sh)
    

终于，我知道如何在Spring Native中增加这个`AddAllCharsets`参数了 😄

```java
@TypeHint(types = XXXDTO.class, typeNames = "com.baofeidyz.XXXDTO")  
// 👇 下面这行代码就解决了我的问题
@NativeHint(options = "-H:+AddAllCharsets")  
@SpringBootApplication  
public class MySpringNativeApplication {  
// ...
}
```

最佳实践就是加一个`@NativeHint(options = "-H:+AddAllCharsets")` 当然不可能这么顺利的，很快我又遇到了新的报错

### 错误4 POI的resource文件未加载

```java
org.apache.xmlbeans.SchemaTypeLoaderException: XML-BEANS compiled schema: Could not locate compiled schema resource org/apache/poi/schemas/ooxml/system/ooxml/index.xsb (org.apache.poi.schemas.ooxml.system.ooxml.index) - code 0
        at org.apache.xmlbeans.impl.schema.XsbReader.<init>(XsbReader.java:63) 
        at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl.initFromHeader(SchemaTypeSystemImpl.java:235) 
        at org.apache.xmlbeans.impl.schema.SchemaTypeSystemImpl.<init>(SchemaTypeSystemImpl.java:201) 
        at org.apache.poi.schemas.ooxml.system.ooxml.TypeSystemHolder.<init>(TypeSystemHolder.java:9) 
        at org.apache.poi.schemas.ooxml.system.ooxml.TypeSystemHolder.<clinit>(TypeSystemHolder.java:6) 
        at org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbook.<clinit>(CTWorkbook.java:22) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.onWorkbookCreate(XSSFWorkbook.java:475)
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:232) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:226) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:214)
        // 有省略
```

我猜测应该是因为资源没有引入导致的，所以我理解应该还是应该用spring native提供的hint去实现。我参考这个[代码块](https://github.com/GoodforGod/graalvm-hint#resourcehint) 增加了一个`@ResourceHint`

```java
@TypeHint(types = XXXDTO.class, typeNames = "com.baofeidyz.XXXDTO")  
@ResourceHint(patterns = {"(^/|[a-zA-Z])*/.+(/$)?.xsb"})  
@NativeHint(options = "-H:+AddAllCharsets")  
@SpringBootApplication  
public class MySpringNativeApplication {
// ...
}
```

### 报错5

```plaintext
java.lang.ClassCastException: org.apache.xmlbeans.impl.values.XmlComplexContentImpl cannot be cast to org.openxmlformats.schemas.spreadsheetml.x2006.main.CTWorkbook
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.onWorkbookCreate(XSSFWorkbook.java:475)
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:232) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:226) 
        at org.apache.poi.xssf.usermodel.XSSFWorkbook.<init>(XSSFWorkbook.java:214) 
        // 有省略
```

然后我在GraalVM的issue清单中找到了这个issue：[Getting following error while generating excel file using native image. · Issue #1929 · oracle/graal · GitHub](https://github.com/oracle/graal/issues/1929) 基本上错误信息就是一致的。 我试了一下，不行。

我感觉POI和GraalVM配合坑有点多，我不打算一个一个找了，我尝试去找一些大而全的文章，于是，我找到了这篇文章：[Graalvm使用采坑 - 行万里路才能回到内心深处，读万卷书才能看得清皓月繁星](http://lokie.wang/article/66)

可惜还是失败了，我感觉算了吧，POI和GraalVM的路还很长哦...

# 一些思考

1. 对于这种POI组件的问题，处理起来确实很难。我查了GraalVM的ISSUE清单，5.0之前的POI确实是有一些问题，但与此同时GraalVM可能需要大量的构建参数去配置，然后Spring Native又在其中转了一次。也就是说，我需要知道这个问题不是POI导致的，然后去看native-image对应的解决方案（配置参数）是什么，然后再转成Spring Native的hints。如果我不能确定这个问题是不是POI导致的，我连提ISSUE给谁都不清楚，如果自己尝试去排查编译问题，这就会进入一个更深的坑。
    
2. 我建议如果[Spring Native example](https://github.com/spring-projects-experimental/spring-native/tree/main/samples)下找不到对应的例子之前，不要去趟这个浑水，整个开发过程非常阻塞，问题很难说真正的解决完。
    
3. Java云原生的路确实很难走，不管是GraalVM去适配组件，还是组件去适配GraalVM，整个适配时间会拉得很长很长。
    
4. GraalVM编译时间很长，我的项目并不大，4核8线程，16G内存，但是每次都需要大概3分钟左右，并且编译的时候，CPU使用率一直都很高。
    

# 后续

我后来用GO重写了整个程序，这是我第一次用GO，从0开始学，大概花了20个小时就写完了，并且我还用了协程并发，处理了一些线程安全的问题。相比之下，Spring Native至少花了我四倍的时间，并且没有做完。 GO编译速度非常快，我交叉编译linux或windows也才10s，CPU使用率也不高，相比之下，GO做这类小工具的云原生开发真的是非常好用。

所以我在怀疑，Java云原生是否真的有意义？希望十年后，能被打脸。

以上就是我Java云原生的初步体验报告，如果对你有一点点帮助，希望可以给我的文章点个赞和收藏。
