用纯 Java 读写 Esri FileGDB:java-openfilegdb 使用指南与依赖解析项目地址:https://github.com/lizhiziwang/java_openfilegdb.git一个不依赖 GDAL、不碰 JNI 的纯 Java 库,直接按字节解析.gdb。本文讲清它怎么用、依赖什么、以及几个容易踩的资源管理坑。0. 太长不看它是 GDALOpenFileGDB驱动的纯 Java 重新实现:不链接 GDAL、不用 JNI/JNA、不依赖 Esri SDK,格式细节全用java.nio.ByteBuffer逐字节解析。对外 API 对齐 OGR:Dataset → Layer → Feature → Field / Geometry。SQL 引擎用Apache Calcite 1.38.0,executeSql返回的就是一个可读的GdbLayer(对标 GDALExecuteSQL()返回OGRLayer*)。读是流式的:SELECT / WHERE / LIMIT默认不占额外内存,2 万行全取 GC 后零增长。1. 背景:为什么需要它Esri File Geodatabase(.gdb目录)是 ArcGIS 的私有矢量格式。过去在 Java 里读它基本两条路:通过 JNI 调 GDAL 的 OpenFileGDB 驱动 —— 要带 native 库,部署痛苦;用 Esri 官方 FileGDB API(C SDK)—— 同样要 native,且有许可限制。java-openfilegdb是 GDALogr/ogrsf_frmts/openfilegdb/C 驱动的纯 Java 重新实现。它适合:不能引入 native 依赖的运行环境(云函数、部分容器 / Android 受限场景、纯 JVM 微服务);想把.gdb读取能力直接编进 Java 应用,又不想维护 GDAL 交叉编译的工程。能力边界(刻意与 GDAL 对齐的限制):只读矢量:feature class 与 table;不读栅格目录。写只到版本 3:不写 Z/M、不写空间索引(.spx)、不写域 / 子类型 / 关系类 / 要素数据集。SQL只读:SELECT / WITH / VALUES / TABLE / EXPLAIN;不做ST_*空间函数、不做DROP TABLE写命令。2. 依赖关系2.1 外部依赖(Maven)依赖版本作用域说明org.slf4j:slf4j-api2.0.13compile日志门面,实现由你提供(logback / log4j2 / slf4j-simple 任选)org.apache.calcite:calcite-core1.38.0compileSQL 引擎(executeSql用)org.junit.jupiter:junit-jupiter5.10.2test单元测试org.slf4j:slf4j-simple2.0.13test测试用日志实现关键点:跑 SQL 必须带上calcite-core及其传递依赖(Calcite 会拉入calcite-linq4j、avatica-core、jackson-*、janino等)。在 Hadoop / Spark / Flink 这类已带旧版 Calcite 的环境里,注意版本冲突,必要时用 shade 重定位。Calcite 在运行时会用Janino(一个纯 Java 实现的编译器)动态生成 Java 字节码执行表达式。Janino 不涉及 JNI,因此不破坏本库的纯 Java硬约束。运行环境要求Java 8(源码 / 字节码 target 8)。Maven 坐标(当前是1.0.0-SNAPSHOT,先本地mvn clean install):dependencygroupIdcom.gdal/groupIdartifactIdjava-openfilegdb/artifactIdversion1.0.0-SNAPSHOT/version/dependency!-- 运行时还需显式提供: --dependencygroupIdorg.slf4j/groupIdartifactIdslf4j-api/artifactIdversion2.0.13/version/dependencydependencygroupIdorg.apache.calcite/groupIdartifactIdcalcite-core/artifactIdversion1.38.0/version/dependencySLF4J 只给门面,别忘了再选一个日志实现;Calcite 是重依赖,生产打包请确认它进了最终运行 classpath。2.2 内部模块依赖图包结构(包名前缀com.gdal.openfilegdb):dataset ──► layer ──► table ──► feature / index / srs / io / util / exception sql ──► dataset / layer / feature (把 OpenFileGdbLayer 暴露给 Calcite) demo ──► 以上全部(示例)依赖方向图(A → B表示 A 依赖 B):demodatasetlayersqltablefeatureindexsrsioutil几条架构铁律(也是阅读源码时的导航):table层不认识layer/dataset—— 它只管单个.gdbtable的字节;往上组装交给layer/dataset。io/util/exception是叶子层,不反向依赖任何上层(exception被各处抛出,util提供通用工具)。所有公共资源类(OpenFileGdbDataset/GdbTable/GdbTableIndex/GdbSystemCatalog/GdbSpatialIndex/GdbSqlLayer/GdbSqlResult)都实现AutoCloseable,文档示例一律 try-with-resources。IO 一律走FileChannel定位读写(不共享文件游标):读可并发;写必须单线程串行。2.3 与 GDAL 的关系它不是对 GDAL 的 JNI 封装,而是行为对齐的重新实现。遇到格式细节分歧时,以工程里本地保留的reference/cpp/(上游 GDAL C 源码,只读、不入库)为准;跨语言实现的正确性也已和真实ogrinfo/ogr2ogr对照验证过(见 §7)。3. 快速开始importcom.gdal.openfilegdb.dataset.OpenFileGdbDataset;// 打开已有 .gdbtry(OpenFileGdbDatasetdsOpenFileGdbDataset.open(java.nio.file.Paths.get(D:/data/2024年国土行政区划.gdb))){System.out.println(图层: ds.getLayerNames());}// 新建一个 .gdb(写)try(OpenFileGdbDatasetdsOpenFileGdbDataset.create(java.nio.file.Paths.get(out/write-demo.gdb))){// ... 建图层、写要素(见 §4.3)ds.flush();// 落盘}4. 核心用法4.1 打开与遍历图层(OGR 风格)API 命名直接照抄 OGR:Dataset → Layer → Feature → Field / Geometry。try(OpenFileGdbDatasetdsOpenFileGdbDataset.open(path)){intnds.getLayerCount();// 用户图层数(系统表不算)for(inti0;in;i){OpenFileGdbLayerlayerds.getLayer(i);// 等价 ds.getLayer(name)System.out.println(图层: layer.getName());System.out.println(几何: layer.getGeometryType());System.out.println(要素数: layer.getFeatureCount());System.out.println(范围: java.util.Arrays.toString(layer.getExtent()));// 字段定义for(GdbFieldf:layer.getFields()){System.out.printf( %s %s 长度%d 可空%s%n,f.getName(),f.getTypeName(),f.getWidth(),f.isNullable());}// 逐条遍历:单游标,支持 for-each(对应 OGR GetNextFeature / ResetReading)for(GdbFeaturefeat:layer){longoidfeat.getFID();// OBJECTIDStringnamefeat.getFieldAsString(NAME);GdbGeometrygeomfeat.getGeometryRef();// 几何(惰性解码)if(geom!null)System.out.println(oid name geom.toWkt());}}}取值捷径:getFieldAsString / getFieldAsInteger / getFieldAsInteger64 / getFieldAsDouble / getFieldAsBoolean,以及isFieldSet(name)。空间参考:layer.getSpatialReference().getWkid()。4.2 空间过滤(精确几何相交)两种方式,语义不同:// (A) 流式:设置过滤矩形后逐条拉 —— 内存 O(1)layer.setSpatialFilterRect(xmin,ymin,xmax,ymax);GdbFeaturef;while((flayer.getNextFeature())!null){/* ... */}layer.clearSpatialFilter();// (B) 急切:一次返回命中列表(全驻留)ListGdbFeaturehitslayer.search(xmin,ymin,xmax,ymax);要点:过滤判据是Intersects(精确几何相交),不是包围盒相交 —— 凹多边形、带洞面都按真实几何判断,因此和ogrinfo -spat的结果逐条一致。有.spx空间索引时先粗筛候选行号;索引缺失或损坏自动退化为全表扫描,行为不变。getFeature(oid)绕过空间过滤(与 OGR 一致),只看 FID。4.3 写数据ListGdbFieldfieldsnewArrayList();fields.add(newGdbField(NAME,FieldType.STRING).setLength(64));fields.add(newGdbField(POP,FieldType.INT32));try(OpenFileGdbDatasetdsOpenFileGdbDataset.create(outDir.resolve(cities.gdb))){OpenFileGdbLayerlayerds.createLayer(城市,SpatialReference.fromEpsg(4326),GeometryType.POINT,fields);GdbFeaturefnewGdbFeature();f.setAttribute(NAME,北京).setAttribute(POP,21893000);f.setGeometry(GdbGeometry.point(116.4074,39.9042));// 点几何longoidlayer.createFeature(f);// OBJECTID 自动分配ds.flush();// 必须 flush 才真正落盘}建图层时只给业务字段;OBJECTID与Shape由库自动补在前面。非空间表用ds.createTable(name, fields)。只读打开的库上调用写方法会抛GdbWriteException。写出的库可被 GDAL 直接识别:ogrinfo -al -so cities.gdb。4.4 SQL 查询(重点)executeSql对标 GDALExecuteSQL():返回一个GdbLayer,可以像普通图层一样getLayerDefn()/getNextFeature()/getFID()/getGeometryRef()。try(OpenFileGdbDatasetdsOpenFileGdbDataset.open(path)){// 方式一:当图层读(对标 GDAL ExecuteSQL 返回 OGRLayer)try(GdbLayerrsds.executeSql(SELECT NAME, POP FROM \城市\ WHERE POP 20000000 ORDER BY POP DESC)){System.out.println(层名rs.getName());// 固定 SELECT,与 GDAL 一致while((GdbFeaturefrs.getNextFeature())!null){System.out.println(f.getFID() f.getFieldAsString(NAME));}}// 自动 releaseResultSet// 方式二:拿原始 Object[] 游标(聚合查询更顺手)try(GdbSqlResultrowsds.executeSqlRows(SELECT COUNT(*) AS n FROM \城市\)){for(Object[]row:rows)System.out.println(row[0]);}}关键语义:行 → 要素映射:几何列 →feature.getGeometry()(不进属性表);OBJECTID列 →feature.getFID()(也不进属性表);其余列按名进属性表,getField(列名)取。无 OBJECTID 时 FID 退化为 1…n。流式 vs 物化:SELECT / WHERE / LIMIT与聚合全程流式(实测 2 万行全取 GC 后无增长);ORDER BY / DISTINCT / GROUP BY按 SQL 语义物化(与 GDAL 文档同款取舍)。必须释放资源:GdbSqlLayer持有 Calcite JDBC 连接,一定要try-with-resources 或ds.releaseResultSet(rs),否则连接泄漏。带空间过滤的 SQL:ds.executeSql(sql, bbox)/ds.executeSqlRows(sql, bbox),等价于 GDALExecuteSQL(pszSQL, poSpatialFilter)—— 会把矩形下推到源图层走.spx索引。性能贴士:SELECT *会把几何列也解码出来(虽然没用到)。只想读属性就SELECT 列名,Calcite 会把投影下推,几何 blob整个不读不解码,更快更省。不支持:写语句(DROP TABLE/DELLAYER:)、ST_*空间函数、fid伪列(用OBJECTID)。5. 资源管理与其他坑优先 try-with-resources:OpenFileGdbDataset、GdbSqlLayer、GdbSqlResult都是AutoCloseable。OpenFileGdbLayer的资源随 Dataset 释放;而 SQL 结果层(GdbSqlLayer/GdbSqlResult)握有自己的 JDBC 连接,必须单独关闭。SQL 结果必须释放:executeSql返回的图层不是普通图层,漏掉releaseResultSet会泄漏连接。单游标语义:getNextFeature()/for-each是单游标(对齐 OGR)。嵌套遍历同一图层前要先resetReading()。字符集:SQL 字面量按 UTF-8 解析(内部已处理,无需你设系统属性)。日期空值 / 秒级吸附等格式怪癖库已按 GDAL 规则处理,直接用LocalDateTime读日期时间字段即可。6. 性能与内存流式读取:每行 O(1) 常驻(只活着当前 feature getLayerDefn()内部预取缓存的一行)。几何惰性解码:getGeometry()第一次被调用时才解 blob;SELECT投影只引用属性列时连几何 blob 都不碰。唯一 O(n) 常驻结构是.gdbtablx偏移表(8 字节 / 条);空间过滤的候选行号数组同样可忽略。对照实测:21,217 行SELECT OBJECTID, Shape全取,GC 后内存零增长。7. 与 GDAL 行为对照(已验证)场景本库GDAL (ogrinfo / ogr2ogr)村行政区划 空间过滤命中数60946094(逐 OBJECTID 一致)乡行政区划 空间过滤2649 / 6042649 / 604SELECT ... WHERE dcmj 1e6命中57125712COUNT(*)/SUM/MIN/MAX/DISTINCT一致一致8. 小结java-openfilegdb让你在纯 JVM里读写 Esri FileGDB,API 对齐 OGR、SQL 返回可当图层读的结果集,读路径流式且内存稳定。引入时记住三件事:带上 slf4j 实现 calcite-core(Calcite 是重依赖,注意环境冲突);SQL 结果用完释放(try-with-resources 或releaseResultSet);大表用投影列(SELECT 列名)避免无谓的几何解码。源码包:com.gdal.openfilegdb构建:mvn clean package(83 测试,0 失败)示例:demo.GdbReadDemo/GdbWriteDemo/GdbSqlDemo
阅读完成 · 觉得有帮助?