Hardwood 到达 1.0,是 Java 生态里处理 Parquet 文件的一次值得注意的尝试:它由 Gunnar Morling 启动,目标是用多线程读取和“零强制外部依赖”降低接入成本。和成熟但依赖较重的 Apache Parquet Java 实现相比,Hardwood 目前更像一个专注的读取器:先把读路径做轻、做快,写入能力留给后续版本。
为什么“零强制依赖”很有吸引力
在 JVM 服务里接 Parquet,经常不是单独跑一个工具,而是嵌进已有的数据同步、批处理、搜索索引构建或离线分析链路。此时依赖树会直接影响发布包大小、冲突概率和安全扫描噪音。
Hardwood 的卖点不是“替代所有 Parquet 工具”,而是把一个常见场景切窄:如果你的服务只需要读 Parquet,并且希望少引依赖,它提供了一个更轻的候选项。
这类库尤其适合几种地方:
- JVM 后端需要读取少量到中等规模的 Parquet 文件做导入。
- 批处理任务已经有自己的线程池、日志、指标体系,不希望 Parquet 读取层带入一串传递依赖。
- 应用运行在受限环境里,例如函数计算、镜像体积敏感的容器、插件化系统。
需要注意的是,“零强制依赖”不等于没有工程成本。你仍然要验证格式兼容性、性能曲线、异常处理和生产可观测性。
多线程读取:收益来自数据形态,而不是口号
摘要里明确提到 Hardwood 采用多线程方式。对 Parquet 这类列式格式来说,多线程读取可能带来明显收益,但收益会被文件结构和硬件条件限制。
更容易受益的场景包括:
- 文件较大,读取和解码可以被拆分。
- 机器有足够 CPU 核心和磁盘吞吐。
- 下游处理不会马上变成瓶颈,例如每行都同步写远端数据库。
不容易受益的场景也很常见:
- 文件很小,线程调度成本盖过了解码收益。
- 存储很慢,瓶颈在网络或磁盘读取。
- 下游处理是单线程、锁竞争严重或有全局排序。
所以更实际的采用方式是:先把 Hardwood 放在清晰的读取边界后面,再用你自己的数据和部署环境跑基准。不要只看单机样例吞吐。
可以这样实践:把 Hardwood 隔离在读取适配器后面
由于 Hardwood 当前聚焦读取,写入支持还在后续版本预期中,建议不要让业务代码直接散落调用读取库。下面是一个可复制运行的最小 Java 项目骨架:它先定义业务侧接口,真实 Hardwood 调用放在一个适配器里。示例中的 HardwoodParquetReader 使用占位实现,接入时只需要替换 TODO 区域为 Hardwood v1 的实际 API。
保存为 ParquetImportDemo.java 后可直接运行:
import java.nio.file.Path;
import java.util.List;
import java.util.Map;
public class ParquetImportDemo {
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: java ParquetImportDemo <file.parquet>");
System.exit(2);
}
ParquetRows reader = new HardwoodParquetReader();
long rows = 0;
for (Map<String, Object> row : reader.read(Path.of(args[0]))) {
// Replace this with validation, indexing, batching, or transformation.
System.out.println(row);
rows++;
}
System.out.printf("Imported %,d rows%n", rows);
}
}
interface ParquetRows {
Iterable<Map<String, Object>> read(Path file) throws Exception;
}
final class HardwoodParquetReader implements ParquetRows {
@Override
public Iterable<Map<String, Object>> read(Path file) throws Exception {
// Assumption: keep all Hardwood-specific code here.
// Replace this block with Hardwood v1's actual reader API after adding the dependency.
// For example, map each Parquet record into Map<String, Object> or your domain object.
System.err.println("TODO: wire Hardwood reader for " + file.toAbsolutePath());
return List.of();
}
}
运行:
javac ParquetImportDemo.java
java ParquetImportDemo ./sample.parquet
如果用 Gradle,可以先把依赖集中在一个模块里。下面的坐标需要替换为 Hardwood v1 在你仓库中使用的实际 Maven 坐标:
plugins {
java
application
}
repositories {
mavenCentral()
}
dependencies {
// Replace with the published Hardwood v1 coordinates used by your organization.
implementation("<group-id>:<artifact-id>:1.0.0")
}
application {
mainClass.set("ParquetImportDemo")
}
然后用依赖树确认它是否符合你对“轻依赖”的期待:
./gradlew dependencies --configuration runtimeClasspath
这条命令在引入新库时很实用:它能让你看到实际进入运行时 classpath 的内容,而不是只相信宣传语。
采用前的检查清单
Hardwood 1.0 当前最明确的边界是“只读”。如果你的链路需要读后再写 Parquet,短期内仍要保留其他写入方案,或把写入接口设计成可替换实现。
建议上线前检查这些点:
- 用生产样本文件测试,不只用玩具数据。
- 覆盖你实际遇到的列类型、空值、嵌套结构和压缩方式。
- 对比 Apache Parquet Java 的读取结果,确认语义一致。
- 分别压测小文件、大文件、本地盘、对象存储路径。
- 观察线程数、CPU、GC、磁盘吞吐和下游处理耗时。
- 把读取库封装在适配器后面,避免将来切换或升级时改动业务代码。
Hardwood 的价值在于给 Java Parquet 读取提供了一个更轻的选项。它不是一个“现在就覆盖全部 Parquet 生命周期”的承诺,而是一个明确的工程取舍:先把读取做好,把依赖压低,把写入留给后续版本。对只读链路来说,这已经足够值得单独评估。