Hardwood 到达 1.0,是 Java 生态里处理 Apache Parquet 文件的一个值得关注的信号。这个由 Gunnar Morling 发起的项目,目标很直接:用多线程读取和零强制外部依赖,给开发者一个比 Apache Parquet Java 实现更轻、更容易嵌入的选择。当前版本只支持读取,写入能力还在后续版本计划中。
为什么“零强制依赖”很有吸引力
Parquet 常出现在数据湖、批处理、分析平台和离线导入链路里。传统 Java 项目接入 Parquet 时,经常不只是引入一个 JAR,而是带上一串传递依赖。对平台型服务、命令行工具、嵌入式组件来说,这会带来几个现实问题:
- 依赖冲突:例如不同模块拉入不同版本的压缩、Hadoop 或 Arrow 相关库。
- 启动和打包复杂度:fat jar、native image、serverless 场景都更敏感。
- 安全扫描噪声:传递依赖越多,CVE 和许可证审计越难控。
Hardwood 的“zero mandatory external dependencies”并不等于你永远不需要任何配套库,而是它把基础读取能力做得更独立。这个设计对只想读取 Parquet、抽取字段、做轻量转换的 Java 服务尤其友好。
多线程读取适合什么场景
Parquet 是列式格式,天然适合按列读取、跳过不需要的数据块。Hardwood 强调多线程处理,意味着它的目标不是只做一个最小解析器,而是想把现代 CPU 的并行能力用起来。
比较适合优先评估的场景包括:
- 后端服务读取上传的 Parquet 文件,转换成内部行模型。
- 数据质量检查工具扫描目录中的 Parquet 文件。
- 批处理任务从对象存储下载文件后做本地解析。
- 不想引入完整 Hadoop/Parquet Java 依赖树的小型 CLI。
边界也要明确:当前 Hardwood 1.0 只支持读取。如果你的链路需要生成 Parquet 文件、重写 schema、追加分区文件,暂时还不能只靠 Hardwood 完成。
可以这样做一次低风险试用
下面这个例子不是 Hardwood 官方 API 示例,因为摘要没有给出具体类名和 Maven 坐标。它是一个可复制的评估骨架:你只需要把依赖坐标和读取调用替换成 Hardwood 当前发布版本中的真实 API,就能在自己的项目里验证依赖体积、读取耗时和线程配置。
创建一个临时 Gradle 项目:
mkdir hardwood-parquet-spike
cd hardwood-parquet-spike
cat > settings.gradle <<'EOF'
pluginManagement {
repositories {
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories { mavenCentral() }
}
rootProject.name = 'hardwood-parquet-spike'
EOF
cat > build.gradle <<'EOF'
plugins {
id 'java'
id 'application'
}
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
mainClass = 'demo.ParquetReadSpike'
}
dependencies {
// 替换为 Hardwood 1.x 的实际 Maven 坐标。
// implementation '...:hardwood:1.0.0'
}
EOF
mkdir -p src/main/java/demo
cat > src/main/java/demo/ParquetReadSpike.java <<'EOF'
package demo;
import java.nio.file.Path;
public class ParquetReadSpike {
public static void main(String[] args) throws Exception {
if (args.length != 1) {
System.err.println("Usage: ./gradlew run --args '<file.parquet>'");
System.exit(2);
}
Path parquetFile = Path.of(args[0]);
long started = System.nanoTime();
// 假设性适配层:把这里替换成 Hardwood 当前版本的真实读取 API。
// 建议保留这一层,不要让业务代码直接散落具体库调用。
long rows = new HardwoodParquetReader().countRows(parquetFile);
long elapsedMs = (System.nanoTime() - started) / 1_000_000;
System.out.printf("file=%s rows=%d elapsedMs=%d%n", parquetFile, rows, elapsedMs);
}
static final class HardwoodParquetReader {
long countRows(Path file) {
throw new UnsupportedOperationException(
"Replace this adapter with Hardwood's real read API"
);
}
}
}
EOF
你可以先不写业务逻辑,只检查依赖树是否符合预期:
./gradlew dependencies --configuration runtimeClasspath
接入真实 API 后,再用同一个 Parquet 文件对比旧实现和 Hardwood:
./gradlew run --args '/path/to/sample.parquet'
评估时建议记录三类数据:读取耗时、峰值内存、运行时依赖数量。Hardwood 的卖点之一是依赖简单,所以不要只看吞吐量,也要看打包产物和依赖冲突是否真的减少。
接入时保留一层适配器
即使 Hardwood 到了 1.0,也建议用接口隔离它和业务代码。原因很简单:写入支持还没到,未来 API 可能围绕 writer、schema 或并发配置继续扩展。把读取逻辑包在一个小接口里,后续切换或双跑更容易。
可以这样设计:
package demo;
import java.nio.file.Path;
import java.util.stream.Stream;
public interface ParquetRowsReader<T> {
Stream<T> read(Path file) throws Exception;
}
业务代码依赖 ParquetRowsReader<T>,实现类里再调用 Hardwood。这样你可以在测试环境里同时保留 Apache Parquet Java 实现和 Hardwood 实现,用同一批样本文件比较结果一致性。
采用建议:从读取链路开始,不要急着全量替换
Hardwood 1.0 最适合从“只读”的小入口开始试用,比如导入、校验、预览、统计这类任务。它的价值点很清楚:多线程读取、依赖更轻、嵌入更简单。但它当前不是完整 Parquet 工具箱,尤其还不覆盖写入。
落地前可以按这个清单走:
- 确认当前链路只需要读取 Parquet。
- 选 3 到 5 个真实样本文件,覆盖不同 schema 和压缩方式。
- 对比旧实现与 Hardwood 的行数、字段值、异常行为。
- 检查 runtime classpath,确认依赖减少是否真实发生。
- 把 Hardwood 调用封装在适配器后面,避免业务层直接绑定库 API。
如果你的系统被 Parquet Java 的依赖树拖得很重,Hardwood 值得认真试一次;如果你需要成熟的读写全套能力,现在更合理的策略是把它放进只读路径中渐进验证。