在 GIS 领域做 Java 开发,面对 JTS、GeoTools、GDAL、ESRI Geometry API 等多套底层库,如何将它们整合成一个好用的工具层,是本文想分享的核心内容。
一、问题的起点
如果你在 Java 生态里做过空间数据处理,大概率经历过这样的场景:读一个 Shapefile 要用 GeoTools 的 DataStore,做几何运算要切到 JTS 的 Geometry 对象,空间分析想跑得快一点又得换 ESRI Geometry API,而处理 FileGDB 还得单独装上 GDAL,调它的 Java 绑定。
每套库有自己的对象模型、自己的坐标系处理方式、自己的异常体系。在业务代码里把它们揉在一起,写着写着就变成了一团浆糊------数据在四套 API 之间来回转换,每加一种新格式就要重写半套读写的逻辑。
这不是哪套库的问题。JTS 的几何模型设计严谨,GeoTools 的格式覆盖面广,ESRI 的算子性能优秀,GDAL 的底层能力无可替代------问题出在它们没有人为业务开发者做"减法"。
于是就有了这样一个想法:在底层库之上,构建一个只有几十个类的工具层,对外暴露统一的数据模型和操作接口。
二、核心思路:一个图层模型通吃所有格式
这个工具层的基石是一个叫 OguLayer 的类。它只有不到 160 行代码,但承载了整个库的设计哲学:
java
public class OguLayer {
private String name; // 图层名称
private Integer wkid; // 坐标系 EPSG Code
private GeometryType geometryType; // 几何类型
private List<OguField> fields; // 字段定义
private List<OguFeature> features; // 要素集合
private OguLayerMetadata metadata; // 扩展元数据
}
设计上的几个取舍:
1. 用 WKT 字符串而非对象存储几何
OguFeature 中,几何数据存的是 WKT 字符串,而不是 JTS 的 Geometry 对象。这样做有两个好处:一是序列化零成本------写到 JSON 或 PostGIS 都不需要额外转换;二是解耦了底层几何库------你可以用 JTS 做运算、用 ESRI API 做运算,甚至不做运算只做透传,模型本身不依赖任何一套。
代价是每次做空间分析时需要 GeometryUtil.wkt2Geometry() 转一下。工程上用一个静态工具方法包掉这点开销,实际影响很小。
2. 引入字段定义与值的分离
OguField 定义字段的元信息(名称、类型、长度),OguFieldValue 承载字段的具体值并提供类型转换方法。这种设计在处理异构数据源时特别有用------同一个 Shapefile 在 GeoTools 和 GDAL 里,字段的类型枚举和名称处理规则都不一样,统一模型在这里完成了归一化。
3. 函数式过滤
java
layer.filter(f -> {
Integer pop = f.getAttribute("population").getIntValue();
return pop != null && pop > 10000000;
});
这行代码背后是 OguFeatureFilter 这个函数式接口。不用引入复杂的查询 DSL,Java 的 lambda 已经足够表达绝大多数过滤场景。需要复杂空间查询时,底层的 CQL 表达式和空间过滤器也保留了透传通道。
三、双引擎架构:策略模式的务实落地
这是这个库中最值得展开讲的部分。
先看数据结构支持矩阵:
| 格式 | GeoTools | GDAL | 说明 |
|---|---|---|---|
| Shapefile | ✅ | ✅ | GeoTools 纯 Java 实现 |
| GeoJSON | ✅ | ✅ | 两套引擎都能读写 |
| PostGIS | ✅ | ✅ | 连接字符串统一格式 |
| FileGDB | ❌ | ✅ | 必须 GDAL 原生库 |
| 国土 TXT | ✅ | ❌ | 国产格式,自行解析 |
可以看到没有任何一套引擎能覆盖所有格式。FileGDB 是 ESRI 的二进制格式,GeoTools 没法处理;而国产的 TXT 坐标文件,GDAL 根本不认识。
传统做法是让调用方自己判断用什么引擎、自己处理不同 API。这里的做法是引入一个工厂层:
java
GisEngine engine = GisEngineFactory.getEngine(GisEngineType.AUTO);
OguLayer layer = engine.readLayer(DataFormatType.SHP, path, ...);
GisEngineType.AUTO 的策略是:如果 GDAL 可用,优先用 GDAL;否则回退到 GeoTools。如果指定了 FileGDB 格式,工厂会强制走 GDAL 路由,没有就抛明确的异常,而不是跑到一半再报错。
每个引擎内部实现了统一的 LayerReader / LayerWriter 接口,各自用各自的方式读写格式,但最终产出的都是同一个 OguLayer 对象。这就是典型的策略+适配器组合------对调用方来说,引擎切换只是改一个枚举值。
GisEngineFactory
│
┌──────────┼──────────┐
▼ ▼
GeoToolsEngine GdalEngine
(纯 Java, 始终可用) (需原生 GDAL 库)
│ │
┌──────────┼──────────┐ ┌───────┼───────┐
▼ ▼ ▼ ▼
GeoToolsLayerReader GeoToolsLayerWriter GdalLayerReader GdalLayerWriter
│ │ │ │
└──────────┬──────────┘ └───────┬───────┘
▼ ▼
OguLayer OguLayer
(统一输出) (统一输出)
四、几何工具层:WKT 和 JTS 双轨制
GeometryUtil 是一个写了 1200 多行、60+ 个方法的静态工具类。它值得单独讨论,因为它的 API 设计解决了一个实际痛点。
这个类分两套方法:
- 基于 JTS 对象 (
buffer(geom, dist)------ 传Geometry,返Geometry - 基于 WKT 字符串 (
bufferWkt(wkt, wkid, dist)------ 传字符串,返字符串
为什么要搞两套?
在实际业务中,大量的空间数据是以 WKT 字符串在系统中流转的------从数据库读出来是 WKT,写到前端接口也是 WKT。如果每个操作都要 wkt2Geometry → 运算 → geometry2Wkt 三个步骤,API 会很啰嗦。WKT 后缀方法把这个过程内化,让调用方少写两行转换代码。
内部实现上,WKT 方法走的是 ESRI Geometry API 的算子。这并不是说 ESRI 一定比 JTS 好,而是两套引擎各有侧重:JTS 的分析功能更全面(拓扑验证、布尔运算、DE-9IM 矩阵),ESRI 在格式转换和简化上更便捷。让调用方按场景选择,而不是绑死在一套实现上。
五、坐标系管理:CGCS2000 的开箱支持
在国内 GIS 开发中,CGCS2000(2000 国家大地坐标系)是绕不开的话题。EPSG 4490 是 CGCS2000 的地理坐标系,4502 到 4554 是其高斯-克吕格投影带(3 度分带)。
这个库在 CrsUtil 中预加载了这 65 个坐标系定义,首次调用时自动注册到 GeoTools 的 CRS 体系中:
java
for (int i = 4490; i < 4555; i++) {
supportedCRSList.put(i, CRS.decode("EPSG:" + i, true));
}
配合 getDh() 方法(根据几何范围自动推断带号),投影转换变成了几行代码的事:
java
OguLayer projected = CrsUtil.reproject(layer, 4528); // 转到 3 度带 39
这和 GeoTools 原生的 MathTransform API 比起来,少了很多样板代码。
六、那些不显眼但省时间的小细节
一个工具库好不好用,往往不在大功能上,而在那些不起眼的角落:
-
Shapefile 字段名截断 :Shapefile 的字段名不能超过 10 个字符(这是 dBase III 年代留下的遗产)。
ShpUtil.formatFieldName()自动处理截断和重名。 -
编码检测 :同一个 Shapefile,有人用 UTF-8 存属性,有人用 GBK。
EncodingUtil.getFileEncoding()自动嗅探,不用每次手写Charset.forName("GBK")然后祈祷。 -
自然排序 :
SortUtil.compareString("第5章", "第10章")返回 -1,而不是字典序的相反结果。在图层列表中排序时,这个小功能避免了一个常见坑。 -
科学计数法消去 :
NumUtil.getPlainString(1.234E10)返回"12340000000"。坐标数据在 BigDecial 和 String 之间转换时,这个场景出现频率比想象的高。 -
ZIP 工具:GIS 数据经常打包成 ZIP 分发(尤其是 Shapefile 的多文件结构),内置压缩/解压省掉了额外依赖。
七、不做什么,和做什么同样重要
这个库有意识地放弃了几个事情:
不做可视化 。不做地图渲染,不画图层。它就是数据的进出和空间运算,拿到 OguLayer 之后丢给前端或者制图工具。
不做 Web 服务。不绑定 Spring Boot,不提供 REST API。一个 JAR 包,Maven 引入就能用。
不做复杂的空间分析引擎。缓冲、相交、差集、凸包这些基本操作有,但更复杂的网络分析、空间统计留给专业工具。这个库的定位是"数据搬运工"和"格式转换枢纽",而不是替代 ArcGIS。
这些克制让它可以保持轻量------总共 44 个 Java 源文件,核心模型只有 7 个类,理解全部代码不需要超过一个下午。
八、一条典型的调用链
把上面的东西串起来,一个完整的 Shapefile 转 GeoJSON 并投影变换的流程:
java
// 1. 读 Shapefile(自动选引擎)
OguLayer layer = OguLayerUtil.readLayer(
DataFormatType.SHP, "/data/parcels.shp",
null, "area > 10000", // 属性过滤 + 空间过滤可选
"POLYGON((...))",
GisEngineType.AUTO
);
// 2. 坐标系转换(CGCS2000 地理 → 3 度带投影)
OguLayer projected = CrsUtil.reproject(layer, 4528);
// 3. 写 GeoJSON
OguLayerUtil.writeLayer(
DataFormatType.GEOJSON, projected,
"/data/output.geojson", null, null, GisEngineType.AUTO
);
三步,每步一行。底层发生了什么------引擎选择、格式解析、字段名截断、编码检测、坐标转换函数查找、GeoJSON Writer 初始化------对调用方完全透明。
九、总结
做 GIS 的 Java 开发,不是在跟业务逻辑较劲,而是在跟 API 碎片化较劲。这个工具库做的事情本质上就是"翻译"------在 JTS、GeoTools、GDAL、ESRI 四套语言体系之间,建立一套通用的"世界语"。
技术上看,它没有发明新算法,也没有创造新数据结构。它的价值在工程层:用工厂模式封装引擎选择,用适配器模式统一读写接口,用统一模型屏蔽底层差异。大约 4400 行 Java 代码解决了多个格式 × 多套引擎的组合爆炸问题。
如果你也在 Java 生态里做空间数据处理,希望能从这套设计中得到一些参考。
项目地址:github.com/znlgis/opengis-utils-for-java,基于 GeoTools 34.1、JTS 1.20、ESRI Geometry API 2.2.4 和 GDAL 3.11,采用 LGPL 2.1 许可,需要 Java 17 及以上版本运行。