JAVA-GDAL实战:SHP/GeoJSON/DXF转MySQL与PostgreSQL空间数据入库
空间数据入库这件事,说难不难,说简单也真能让人掉一层皮。我最早接触这类需求,是帮一个做市政管网巡检的团队把历年积累的SHP文件塞进数据库,当时天真地以为写个循环读要素、拼INSERT就完事了,结果坐标系一塌糊涂、中文字段名乱码、几何类型混杂、几万个面要素入库跑了快两个小时。后来陆续又碰上GeoJSON和DXF的转换需求,踩的坑多了,才慢慢摸出一套相对稳当的做法。这篇就围绕JAVA-GDAL把SHP、GeoJSON、DXF转成MySQL和PostgreSQL数据库数据这条主线,把我这些年攒下来的思路、参数、代码骨架和避坑经验摊开讲清楚。
先说清楚这篇东西适合谁看:如果你手头有一批矢量数据文件,需要批量落到关系型数据库里给业务系统查询、做空间分析或者做地图服务的数据源,那这篇基本能覆盖你的核心诉求;如果你只是想临时看看某个SHP里有什么,那用QGIS点两下更快,不必上这套重武器。读者最好对Java有一定基础,对GDAL有个模糊印象就行,空间数据库的基本概念我会在用到的地方顺手解释。
1. 整体方案设计与技术选型拆解
1.1 为什么是JAVA-GDAL而不是纯Java库
做矢量数据解析,Java生态里其实有不少选择,比如GeoTools、JTS,甚至一些轻量的GeoJSON解析库。但一旦格式扩展到DXF,选择面立刻收窄。GeoTools对SHP和GeoJSON支持不错,DXF这块要么依赖不完整的插件,要么干脆没有;而GDAL作为地理空间领域事实上的“万能读卡器”,SHP、GeoJSON、DXF、KML、GML、FileGDB几乎通吃,底层OGR库对上百种矢量格式的支持是经过十几年实战检验的。
JAVA-GDAL是GDAL官方提供的Java绑定,通过JNI调用本地库。它的价值在于:一套API读所有格式。你不需要为SHP写一套解析逻辑、为GeoJSON再写一套、为DXF又找第三方库,统一用OGRDataSource打开、遍历OGRLayer、读OGRFeature,格式差异被GDAL屏蔽掉了。这对需要长期维护、格式可能不断增加的项目来说,维护成本优势非常明显。
代价也要说清楚:JAVA-GDAL依赖本地动态库,部署时要把对应平台的.so/.dll/.dylib带上,JNI调用有轻微开销,而且不同GDAL版本的API有差异,升级时要留意。但综合权衡下来,多格式统一处理的收益远大于这些麻烦。
1.2 目标数据库的选择逻辑
MySQL和PostgreSQL都能存空间数据,但定位差别不小。PostgreSQL配PostGIS是空间数据库的标杆,几何类型丰富、空间函数齐全、索引(GiST)成熟,做缓冲区分析、叠加分析、路径规划这类重空间计算几乎无脑选它。MySQL从5.7起提供了空间数据类型和ST_系列函数,8.0之后支持了SRID约束和空间索引(R-Tree),做简单的存储和范围查询完全够用,而且很多业务系统本来就在MySQL上,不想为空间数据单独再养一套库。
所以我的方案设计是抽象出统一的写入接口,底层按目标库分派。几何数据在PostgreSQL侧走geometry类型(需要PostGIS扩展),在MySQL侧走GEOMETRY类型,写入时统一转成WKB(Well-Known Binary)二进制,用PreparedStatement的setBytes塞进去,两边都能吃。这个设计的关键在于:几何的序列化格式统一用WKB,避免在Java层做字符串拼接WKT再让数据库解析,既快又不容易出错。
1.3 整体数据流的骨架
整个流程可以拆成四段:打开数据源 → 读取图层元信息 → 遍历要素并转换 → 批量写入数据库。元信息这一步很多人会忽略,但它决定了后面建表语句怎么生成——字段名、字段类型、几何类型、坐标系SRID都要从图层定义里拿。要素遍历阶段要做几何有效性检查、坐标系转换(如果需要)、属性字段类型映射。写入阶段用批处理加事务,控制好批次大小。
下面这张表把三个源格式在GDAL里的打开方式和常见坑做了对照,先有个整体印象:
| 源格式 | 打开方式 | 几何类型特点 | 常见坑 |
|---|---|---|---|
| SHP | 目录或.shp路径 | 单图层,几何类型固定 | 字段名截断为10字符,DBF编码问题 |
| GeoJSON | 文件路径 | 可混合几何类型 | 坐标顺序、嵌套属性、大文件内存 |
| DXF | 文件路径 | 点线面混合,含块引用 | 图层映射、单位、Z轴坐标 |
2. 环境搭建与依赖配置的实操细节
2.1 GDAL本地库的安装与Java绑定
这一步是新手最容易卡住的地方。JAVA-GDAL不是纯Java包,它需要本地GDAL库配合。以Linux为例,最省事的路径是用包管理器装GDAL,再让Java绑定指向它。Ubuntu下大致是装gdal-bin和libgdal-dev,然后确认gdalinfo --version能输出。Java侧需要gdal.jar和JNI库libgdalalljni.so,这两个通常在GDAL安装目录下能找到,或者从对应版本的构建产物里取。
Windows下相对麻烦,官方不直接提供预编译的Java绑定,一般用GISInternals之类的预编译包,里面带gdal.jar和gdalxxx.dll。关键动作是把包含DLL的目录加到PATH或者启动参数-Djava.library.path里,否则运行时报UnsatisfiedLinkError。我见过太多人jar包引对了、代码写对了,就是跑不起来,最后发现是JNI库没加载到。
注意:GDAL版本要和JNI库版本严格一致,jar包和本地库版本错配会引发各种诡异崩溃,不是抛异常而是直接段错误,排查起来非常痛苦。
Maven里可以这样引(版本号按你本地实际调整):
<dependency> <groupId>org.gdal</groupId> <artifactId>gdal</artifactId> <version>3.6.0</version> </dependency>如果中央仓库拉不到,就把gdal.jar手动install到本地仓库,或者用system作用域指向本地文件。生产环境我更推荐后者配合构建脚本统一管理,避免依赖源不稳定。
2.2 数据库驱动与连接池
MySQL用mysql-connector-j,PostgreSQL用postgresql驱动,版本尽量选和目标库大版本匹配的。连接池用HikariCP,配置上有个细节:空间数据批量写入时单条连接会长时间占用,连接池的maxLifetime要设得比数据库端的超时长一些,connectionTimeout也别太短,否则大批量入库时容易在获取连接阶段超时。
<dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <version>42.6.0</version> </dependency> <dependency> <groupId>com.zaxxer</groupId> <artifactId>HikariCP</artifactId> <version>5.0.1</version> </dependency>2.3 初始化GDAL与编码设置
程序启动时调一次gdal.AllRegister()注册所有驱动,这一步不做的话后面GetDriverByName可能返回null。另外中文环境有个高频坑:SHP的DBF属性表编码。老数据很多是GBK编码,GDAL默认按UTF-8读会乱码。解决办法是打开数据源前设置配置项:
gdal.SetConfigOption("SHAPE_ENCODING", "GBK"); gdal.AllRegister();如果数据源编码不统一,可以在打开后通过OGR_L_GetLayerDefn检查,或者干脆在读取属性时做一次编码探测和转换。我一般会在配置里留一个可切换的编码参数,遇到乱码先怀疑这里。
3. 核心转换逻辑的逐层拆解
3.1 打开数据源与图层元信息提取
GDAL的打开逻辑统一走ogr.Open(path, 0),第二个参数0表示只读。返回的DataSource可能是单图层(SHP、GeoJSON、DXF基本都是),也可能是多图层(如FileGDB)。稳妥的做法是遍历GetLayerCount(),对每个图层单独处理,而不是假设只有一个。
拿到Layer后,GetLayerDefn()返回FeatureDefn,这里面有字段定义。遍历字段时要注意:几何字段在GDAL里不算普通字段,它通过GetGeomType()单独拿。字段类型映射是重点,GDAL的OFTInteger、OFTReal、OFTString、OFTDate等要映射到数据库类型。下面是我常用的映射表:
| GDAL字段类型 | MySQL类型 | PostgreSQL类型 |
|---|---|---|
| OFTInteger | INT | INTEGER |
| OFTInteger64 | BIGINT | BIGINT |
| OFTReal | DOUBLE | DOUBLE PRECISION |
| OFTString | VARCHAR(n) | VARCHAR(n) |
| OFTDate | DATE | DATE |
| OFTDateTime | DATETIME | TIMESTAMP |
字符串长度n从字段定义的GetWidth()拿,拿不到或为0时给个默认值比如255。这里有个经验:SHP的字段名会被截断到10个字符,如果业务方要求保留完整字段名,得在源头用别名映射表补回来,否则入库后字段名全是缩写,业务方看不懂。
3.2 几何对象的读取与WKB序列化
读要素用layer.GetNextFeature()循环,每个Feature通过GetGeometryRef()拿几何。这里要处理几个问题:几何可能为null(属性表里有空几何记录),几何可能无效(自相交、环不闭合),坐标系可能和目标库要求的不一致。
几何有效性检查用geometry.IsValid(),无效的可以选择跳过、记录日志或者尝试MakeValid()修复。我一般先记录到一张异常表里,人工确认后再决定,不盲目修复,因为修复可能改变几何形状影响业务。
坐标系转换用OGRCoordinateTransformation,源SRS从图层拿GetSpatialRef(),目标SRS按数据库约定构造(比如EPSG:4326)。转换前要确认两边SRS都不为null,否则转换会失败。转换完再序列化:
byte[] wkb = geometry.ExportToWkb(OGRwkbByteOrder.wkbNDR);wkbNDR是小端序,数据库普遍能识别。这里有个细节:如果几何带Z坐标或M坐标,普通WKB会丢,要用ExportToIsoWkb指定维度,或者确认目标库字段类型支持。DXF数据经常带Z值,这点要特别注意。
3.3 属性值的类型安全读取
GDAL的Feature.GetFieldAsString(index)几乎万能,但会丢失类型信息,数字变字符串再入库要二次转换,容易出错。更稳的做法是按字段类型分派读取:整数用GetFieldAsInteger,浮点用GetFieldAsDouble,字符串用GetFieldAsString,日期用GetFieldAsDateTime再拼。这样能保证类型准确,也便于处理null——GDAL对null字段的读取行为在不同版本有差异,显式判断IsFieldSetAndNotNull(index)更可靠。
属性值里还有一类坑是特殊字符和超长字符串。字符串里带单引号、反斜杠时,如果用字符串拼接SQL会直接炸,所以必须用PreparedStatement参数化。超长字符串超过字段定义宽度时,MySQL严格模式会报错,PostgreSQL也会截断或报错,入库前做一次长度校验和截断策略配置比较稳妥。
4. 数据库写入的完整实现与参数调优
4.1 建表语句的动态生成
表结构不该手写,应该从图层定义动态生成。核心逻辑是遍历字段拼列定义,最后加一列几何字段。PostgreSQL侧几何列用geometry(GEOMETRY, 4326),MySQL侧用GEOMETRY NOT NULL SRID 4326(8.0支持SRID约束)。几何类型如果源数据是单一类型(比如全是面),可以精确指定POLYGON,混合类型就用通用的GEOMETRY。
-- PostgreSQL 示例 CREATE TABLE IF NOT EXISTS target_table ( id BIGSERIAL PRIMARY KEY, name VARCHAR(255), geom GEOMETRY(GEOMETRY, 4326) ); CREATE INDEX idx_target_geom ON target_table USING GIST (geom); -- MySQL 示例 CREATE TABLE IF NOT EXISTS target_table ( id BIGINT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(255), geom GEOMETRY NOT NULL SRID 4326, SPATIAL INDEX idx_target_geom (geom) );建索引的时机有讲究:大批量入库时先不建空间索引,数据写完再建,否则每插一条都要维护索引,速度能差好几倍。我实测过十万级面要素,先建索引入库要十几分钟,后建索引入库两三分钟就完事,建索引本身也就几十秒。
4.2 批量写入与事务控制
写入用PreparedStatement加addBatch,每攒够一批(比如1000条)执行一次executeBatch并提交事务。批次大小不是越大越好:太大内存压力大、失败回滚代价高;太小事务开销占比高。我一般从1000起步,根据单条记录大小调整,几何特别大的(比如复杂多边形)降到200到500。
String sql = "INSERT INTO target_table (name, geom) VALUES (?, ST_GeomFromWKB(?, 4326))"; // PostgreSQL 用 ST_GeomFromWKB;MySQL 8 用 ST_GeomFromWKB 也支持 try (PreparedStatement ps = conn.prepareStatement(sql)) { conn.setAutoCommit(false); int count = 0; while ((feature = layer.GetNextFeature()) != null) { ps.setString(1, feature.GetFieldAsString(nameIdx)); ps.setBytes(2, geometry.ExportToWkb(OGRwkbByteOrder.wkbNDR)); ps.addBatch(); if (++count % 1000 == 0) { ps.executeBatch(); conn.commit(); } } ps.executeBatch(); conn.commit(); }事务边界要和批次对齐,别整个文件一个事务,几百万条记录一个事务日志能把库拖垮。也别每条一个事务,那IO开销受不了。
4.3 坐标系与SRID的一致性保障
这是空间入库最容易出隐性错误的地方。源数据可能是各种坐标系,数据库字段声明了SRID 4326,如果写入的几何实际是别的坐标系,数据能进去但空间查询全错,而且不报错,非常隐蔽。我的做法是:入库前统一转换到目标SRID,转换后用geometry.GetSpatialReference().GetAuthorityCode(null)确认,和目标不一致就拒绝写入并记录。另外MySQL的SRID约束在8.0才严格,5.7写入不匹配SRID的几何不会报错,这点要心里有数。
5. 三类源格式的差异化处理要点
5.1 SHP的特殊性:字段截断与编码
SHP是老三样里最“规矩”的,单图层、几何类型固定,但DBF属性表的两个历史包袱绕不开:字段名10字符限制和编码不确定。字段名截断前面提过,编码问题除了SHAPE_ENCODING配置,还可以在读取后对字符串做一次探测转换。我遇到过一批数据,部分记录是GBK部分是UTF-8,统一配置解决不了,最后是按记录做编码探测才搞定。这种脏数据虽然少见,但真碰上很折磨人。
5.2 GeoJSON的灵活性:混合几何与嵌套属性
GeoJSON规范允许一个FeatureCollection里混着点线面,这在入库时是个麻烦——如果目标表几何列声明了具体类型(比如POLYGON),遇到点要素就插不进去。处理策略有两种:一是入库前按几何类型分组,分别写入不同的表;二是目标表用通用GEOMETRY类型全收。我倾向后者,简单直接,查询时再用ST_GeometryType过滤。
GeoJSON的属性可能是嵌套对象或数组,GDAL读出来会转成字符串(JSON文本),入库时如果目标列是VARCHAR就直接存,需要结构化查询的话得在应用层再解析。另外GeoJSON的坐标顺序是经度在前纬度在后,和某些库的约定相反,转换时要确认清楚,别把经纬度搞反了——这种错误数据看着正常,地图上一画全跑到南极去了。
5.3 DXF的工程属性:图层映射与Z坐标
DXF是CAD交换格式,和GIS数据的思维差异很大。它的“图层”概念对应GDAL里的Layer,但一个DXF文件里往往有几十个CAD图层,全量入库还是按需筛选要提前定策略。DXF的几何可能是块引用(Block Reference),GDAL读的时候不一定展开,需要确认GetGeometryRef拿到的是展开后的几何还是引用本身。
Z坐标是另一个重点。DXF数据普遍带高程,普通WKB会丢Z值。如果业务需要三维,目标列要用geometry(POINTZ, 4326)这类带维度的类型,序列化用ExportToIsoWkb并指定wkbZ。如果业务只要平面,入库前把Z抹掉,避免维度不匹配报错。单位也要留意,DXF可能是毫米或米,和地理坐标系的度完全不是一个量级,转换前必须搞清楚。
6. 常见问题排查与性能优化实录
6.1 典型报错与排查路径
下面这张表是我这些年遇到的高频问题速查,按现象、原因、解决整理:
| 现象 | 可能原因 | 解决方向 |
|---|---|---|
| UnsatisfiedLinkError | JNI库未加载或版本错配 | 检查java.library.path和库版本 |
| 中文乱码 | DBF编码非UTF-8 | 设置SHAPE_ENCODING或做编码转换 |
| 几何插入报SRID错误 | 几何SRID与列声明不符 | 入库前统一坐标系转换 |
| 大批量入库超时 | 连接池或事务配置不当 | 调大超时、缩小事务粒度 |
| 空间查询结果为空 | 坐标系错误或索引缺失 | 校验SRID、确认空间索引存在 |
| 内存溢出 | 一次性加载全部要素 | 流式读取、控制批次 |
排查顺序我一般遵循“先环境后逻辑”:先确认GDAL能正常打开文件(用gdalinfo命令行验证),再确认数据库连接和表结构,最后才怀疑代码逻辑。很多问题其实出在环境层,代码本身没问题。
6.2 性能优化的几个关键杠杆
性能这块,我总结下来影响最大的三个杠杆是:批次大小、索引时机、几何序列化方式。批次大小前面说了,1000左右起步调优;索引时机是后建比先建快数倍;几何序列化用WKB二进制比WKT字符串快,而且省空间。
还有一个容易被忽略的点是字段裁剪。源数据可能有几十个字段,业务只用其中几个,入库时只写需要的字段,能显著减少IO和存储。另外对于超大文件,可以考虑多线程分片读取——GDAL的Layer读取不是线程安全的,但可以按要素范围切分,每个线程打开独立的数据源实例处理不同区间,最后合并。这个方案复杂度高,只在数据量特别大(百万级以上)时才值得上。
6.3 数据质量校验的兜底策略
入库不是终点,数据质量得有兜底。我通常会在入库后跑一轮校验:统计要素总数和源文件对比、检查空几何和无效几何数量、抽样验证坐标系、确认空间索引生效(用EXPLAIN看执行计划)。这些校验脚本固化成流程,每次入库都跑,能提前发现大部分问题。
提示:几何有效性检查在大数据量下开销不小,可以抽样检查或者只在异常日志里记录,不必每条都做完整校验。
7. 我在实际项目里踩过的坑与经验沉淀
说几个印象深的。有一次入库后业务方反馈地图上数据整体偏移了几百米,排查半天发现源SHP的.prj文件缺失,GDAL按默认坐标系处理了,而实际数据是某个地方坐标系。教训是:源数据坐标系不能只信文件声明,要抽样用已知地物验证。后来我加了一道流程,入库前用几个已知坐标的控制点做交叉验证。
还有一次DXF入库后面积全不对,查下来是单位问题——CAD里按毫米画的,入库当成米了,面积差了百万倍。这种错误数据本身“合法”,不报错,但业务结果全错。所以单位换算必须在转换阶段显式处理,不能想当然。
另一个高频坑是MySQL的max_allowed_packet。几何数据大的时候单条INSERT的包超过默认值(通常4MB或16MB),直接报错。解决办法是调大这个参数,或者把超大几何拆分成多条。PostgreSQL这边对应的是TOAST机制,大字段会自动压缩存储,一般不用操心,但超大几何写入时也要留意内存。
最后分享一个实用技巧:把整个转换流程做成配置驱动。源文件路径、目标库类型、连接信息、字段映射、坐标系、批次大小全放配置文件里,代码只负责执行。这样同一套程序能处理不同项目的数据,不用每次改代码重新打包。我现在的做法是配一个YAML,里面定义source和target两段,中间加可选的字段映射和过滤规则,跑起来很省心。
这套东西搭起来之后,日常的数据入库基本就是改改配置、跑一下、看校验报告,稳定性和效率都比早期手写脚本强太多。GDAL的API初看有点绕,但把打开、读要素、序列化、写入这四步的骨架记住,剩下的就是按格式和数据库的差异做适配,套路是固定的。