GeoTools 入门实战(一):Shapefile 读取与写入全解析
目录
- 一、前言
- 二、环境准备
- 三、GeoTools 核心概念
- 四、读取 Shapefile
- 五、创建新 Shapefile
- 六、完整可运行代码
- 七、常见坑位与注意事项
- 八、工程实践建议
- 九、小结
一、前言
GeoTools 是 Java 生态中最重要的开源 GIS 库,它基于 JTS 提供了完整的空间数据读写能力。其中 Shapefile 作为 GIS 行业最通用的数据格式,是每个 GIS 开发者都会接触的第一个格式。
本文将从工程视角出发,系统讲解 GeoTools 读取和写入 Shapefile 的全过程,包含 DataStore 体系、Feature 模型、中文编码处理以及 GeoTools 29.x 版本的 API。
二、环境准备
Maven 依赖
本文使用 GeoTools 29.3 + JTS 1.19.0,依赖配置如下:
<dependencies><dependency><groupId>org.geotools</groupId><artifactId>gt-shapefile</artifactId><version>29.3</version></dependency><dependency><groupId>org.geotools</groupId><artifactId>gt-main</artifactId><version>29.3</version></dependency><dependency><groupId>org.geotools</groupId><artifactId>gt-epsg-hsql</artifactId><version>29.3</version></dependency><dependency><groupId>org.locationtech.jts</groupId><artifactId>jts-core</artifactId><version>1.19.0</version></dependency></dependencies>三、GeoTools 核心概念
在开始写代码之前,先理解 GeoTools 的核心概念,这对后续使用其他数据格式(GeoJSON、PostGIS 等)也非常有用。
DataStore —— 数据存储抽象
GeoTools 不直接操作文件,而是通过 DataStore 进行统一管理。它是所有空间数据源的抽象层,支持文件、数据库、Web 服务等多种形式。
FeatureSource / FeatureCollection / SimpleFeature
这三个接口是 GeoTools 的核心数据模型:
| 接口 | 职责 |
|---|---|
| FeatureSource | 数据源的只读视图,用于查询和获取元数据 |
| FeatureCollection | 元素集合,支持遍历和过滤 |
| SimpleFeature | 单个元素,包含几何字段和属性字段 |
SimpleFeatureType —— 属性结构定义
SimpleFeatureType 定义了一个图层的完整结构,包含:
- 几何字段名称和类型(如 the_geom: Point)
- 属性字段名称和类型(如 name: String)
- 坐标系信息(CRS)
四、读取 Shapefile
读取 Shapefile 是 GeoTools 最基本的操作,流程如下:
- 通过 DataStoreFinder 创建 DataStore 实例
- 获取图层名称(typeName)
- 通过 FeatureSource 获取元素集合
- 使用 FeatureIterator 遍历元素
读取代码示例
StringinputShp="your_path.shp";Map<String,Serializable>params=newHashMap<>();params.put("url",newFile(inputShp).toURI().toURL());params.put("charset",StandardCharsets.UTF_8.name());DataStoreinputStore=DataStoreFinder.getDataStore(params);if(inputStore==null){thrownewRuntimeException("无法识别的 Shapefile 格式");}StringtypeName=inputStore.getTypeNames()[0];FeatureSource<SimpleFeatureType,SimpleFeature>source=inputStore.getFeatureSource(typeName);FeatureCollection<SimpleFeatureType,SimpleFeature>collection=source.getFeatures();System.out.println("要素数量: "+collection.size());System.out.println("属性结构: "+source.getSchema());遍历元素
try(FeatureIterator<SimpleFeature>it=collection.features()){intcount=0;while(it.hasNext()&&count<3){SimpleFeaturefeature=it.next();System.out.println("ID: "+feature.getID());System.out.println("Geometry: "+feature.getDefaultGeometry());for(Objectattr:feature.getAttributes()){System.out.println("Attribute: "+attr);}count++;}}// 关闭 DataStoreinputStore.dispose();关键点说明
- DataStoreFinder 会自动识别数据源类型,Shapefile 对应 ShapefileDataStore
- charset 参数解决中文属性乱码问题
- FeatureIterator 必须在 try-with-resources 中使用,避免内存泄漏
- dispose() 释放资源,生产环境一定不能遗漏
五、创建新 Shapefile
写入 Shapefile 比读取复杂一些,因为需要先定义属性结构(Schema),再逐个写入元素。
GeoTools 29.x 写入步骤
- 创建 ShapefileDataStore 并设置编码
- 使用 SimpleFeatureTypeBuilder 定义 Schema
- 调用 createSchema() 创建层
- 使用 FeatureWriter 写入元素
完整写入代码
privatestaticvoidcreateShapefile()throwsException{Filefile=newFile("output.shp");Map<String,Serializable>params=newHashMap<>();params.put(ShapefileDataStoreFactory.URLP.key,file.toURI().toURL());params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key,Boolean.TRUE);ShapefileDataStoreFactoryfactory=newShapefileDataStoreFactory();ShapefileDataStoreds=(ShapefileDataStore)factory.createNewDataStore(params);ds.setCharset(StandardCharsets.UTF_8);// 1. 构建 SchemaSimpleFeatureTypeBuildertypeBuilder=newSimpleFeatureTypeBuilder();typeBuilder.setName("test_layer");typeBuilder.setCRS(CRS.decode("EPSG:4326"));typeBuilder.add("the_geom",Point.class);typeBuilder.add("name",String.class);typeBuilder.add("value",Double.class);SimpleFeatureTypeschema=typeBuilder.buildFeatureType();ds.createSchema(schema);// 2. 写入要素GeometryFactorygf=newGeometryFactory();String[]names={"点A","点B","点C"};double[][]coords={{116.407,39.904},{116.415,39.912},{116.423,39.908}};try(FeatureWriter<SimpleFeatureType,SimpleFeature>writer=ds.getFeatureWriterAppend(ds.getTypeNames()[0],Transaction.AUTO_COMMIT)){for(inti=0;i<names.length;i++){SimpleFeaturefeature=writer.next();feature.setDefaultGeometry(gf.createPoint(newCoordinate(coords[i][0],coords[i][1])));feature.setAttribute("name",names[i]);feature.setAttribute("value",100.0+i*10);writer.write();}}ds.dispose();}GeoTools 29.x 写入核心三步曲
这是 GeoTools 29.x 中创建 Shapefile 的写法:
SimpleFeaturefeature=writer.next();// 获取占位 Feature(FID 在此生成)feature.setAttribute(...);// 填充字段writer.write();// 写入这三步是严格顺序的,任何变式都会导致异常。
六、完整可运行代码
下面是读取 + 写入的完整代码,可直接复制到 IDE 中运行:
importorg.geotools.data.*;importorg.geotools.data.shapefile.ShapefileDataStore;importorg.geotools.data.shapefile.ShapefileDataStoreFactory;importorg.geotools.feature.FeatureCollection;importorg.geotools.feature.FeatureIterator;importorg.geotools.feature.simple.SimpleFeatureTypeBuilder;importorg.locationtech.jts.geom.Point;importorg.locationtech.jts.geom.GeometryFactory;importorg.opengis.feature.simple.SimpleFeature;importorg.opengis.feature.simple.SimpleFeatureType;importjava.io.File;importjava.io.IOException;importjava.io.Serializable;importjava.nio.charset.StandardCharsets;importjava.util.HashMap;importjava.util.Map;publicclassShapefileReadWriteDemo{publicstaticvoidmain(String[]args)throwsException{StringinputShp="D:/testdata/testshp/ne_110m_admin_0_tiny_countries/ne_110m_admin_0_tiny_countries.shp";// 替换为你的 shp 路径readShapefile(inputShp);// ===============================// 第二部分:创建新的 Shapefile// ===============================createShapefile();System.out.println("\n新 Shapefile 已生成:output.shp");}privatestaticvoidreadShapefile(StringinputShp)throwsIOException{// ===============================// 第一部分:读取 Shapefile// ===============================Map<String,Serializable>params=newHashMap<>();params.put("url",newFile(inputShp).toURI().toURL());params.put("charset",StandardCharsets.UTF_8.name());DataStoreinputStore=DataStoreFinder.getDataStore(params);if(inputStore==null){thrownewRuntimeException("无法识别的 Shapefile 格式");}StringtypeName=inputStore.getTypeNames()[0];FeatureSource<SimpleFeatureType,SimpleFeature>source=inputStore.getFeatureSource(typeName);FeatureCollection<SimpleFeatureType,SimpleFeature>collection=source.getFeatures();System.out.println("==== Shapefile 读取结果 ====");System.out.println("要素数量: "+collection.size());System.out.println("属性结构: "+source.getSchema());try(FeatureIterator<SimpleFeature>it=collection.features()){intcount=0;while(it.hasNext()&&count<3){SimpleFeaturefeature=it.next();System.out.println("\nFeature #"+(count+1));System.out.println(" ID: "+feature.getID());System.out.println(" Geometry: "+feature.getDefaultGeometry());for(Objectattr:feature.getAttributes()){System.out.println(" Attribute: "+attr);}count++;}}inputStore.dispose();}privatestaticvoidcreateShapefile()throwsException{Filefile=newFile("output_fixed.shp");Map<String,Serializable>params=newHashMap<>();params.put(ShapefileDataStoreFactory.URLP.key,file.toURI().toURL());params.put(ShapefileDataStoreFactory.CREATE_SPATIAL_INDEX.key,Boolean.TRUE);ShapefileDataStoreFactoryfactory=newShapefileDataStoreFactory();ShapefileDataStoreds=(ShapefileDataStore)factory.createNewDataStore(params);ds.setCharset(StandardCharsets.UTF_8);// 构建 SchemaSimpleFeatureTypeBuildertypeBuilder=newSimpleFeatureTypeBuilder();typeBuilder.setName("test_layer");typeBuilder.setCRS(org.geotools.referencing.CRS.decode("EPSG:4326"));typeBuilder.add("the_geom",Point.class);// Shapefile 几何字段推荐 the_geomtypeBuilder.add("name",String.class);typeBuilder.add("value",Double.class);SimpleFeatureTypeschema=typeBuilder.buildFeatureType();ds.createSchema(schema);// 写入要素GeometryFactorygf=newGeometryFactory();String[]names={"点A","点B","点C"};double[][]coords={{116.407,39.904},{116.415,39.912},{116.423,39.908}};try(FeatureWriter<SimpleFeatureType,SimpleFeature>writer=ds.getFeatureWriterAppend(ds.getTypeNames()[0],Transaction.AUTO_COMMIT)){for(inti=0;i<names.length;i++){// next() 返回当前可编辑 Feature(FID 在此生成)SimpleFeaturefeature=writer.next();feature.setDefaultGeometry(gf.createPoint(neworg.locationtech.jts.geom.Coordinate(coords[i][0],coords[i][1])));feature.setAttribute("name",names[i]);feature.setAttribute("value",100.0+i*10);writer.write();}}ds.dispose();}}创建出来的shapefile在QGIS中打开,效果如下:
七、常见坑位与注意事项
1. 中文乱码问题
Shapefile 的 DBF 文件默认使用 ISO-8859-1 编码,中文属性会乱码。解决方法:
- 读取时:
params.put("charset", StandardCharsets.UTF_8.name()) - 写入时:
ds.setCharset(StandardCharsets.UTF_8) - 建议同时生成 .cpg 文件声明编码
2. GeoTools 28.x → 29.x 破坏性变更
FeatureWriter.write() 方法在 29.x 中变为无参,这是最常见的升级坑:
| 版本 | write 方法 | 写法 |
|---|---|---|
| 28.x | write(feature) | writer.write(builder.buildFeature(null)) |
| 29.x | write() | feature = writer.next(); writer.write(); |
3. 几何字段名称必须为 the_geom
Shapefile 规范中,几何字段名称建议使用 the_geom,否则可能导致 QGIS / ArcGIS 无法正确识别几何字段。
4. FID 索引异常
提示 Current fid index is null 时,说明 writer.next() 没有被调用就直接 write() 了。记住固定模式:
SimpleFeaturefeature=writer.next();// 不能省略feature.setAttribute(...);writer.write();5. 资源泄露风险
DataStore 和 FeatureIterator 都必须显式关闭:
- DataStore.dispose() 释放文件锁和连接池
- FeatureIterator 用 try-with-resources 自动关闭
长期不关闭会导致文件被锁定,无法删除或覆盖。
八、工程实践建议
- 读取前先判断 DataStore 是否为 null,避免空指针异常
- 生产环境建议将 Shapefile 路径配置化,不要硬编码
- 批量写入时使用 Transaction 控制事务,出错时 rollback
- 建议将属性名称和类型抽取为常量管理
- 大文件处理时注意内存,考虑分块读取
- 写入完成后立即 dispose,释放文件锁
九、小结
本文介绍了 GeoTools 读取和写入 Shapefile 的完整流程,覆盖了以下核心知识点:
- DataStore 体系与 ShapefileDataStore 的使用
- FeatureSource / FeatureCollection / SimpleFeature 的关系
- Schema 定义与属性结构构建
- GeoTools 29.x 的 FeatureWriter 无参 write() 写法
- 中文编码解决方案
- 常见异常与调试技巧
