Geotools库怎么用?Java读写Shapefile?

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

很多 Java GIS 项目都会遇到一个基础问题:Geotools库怎么用?Java读写Shapefile? 如果你正在做桌面 GIS、数据质检、空间数据入库或批量格式转换,GeoTools 读取和写入 Shapefile 是最常见的入门场景之一。本文以一个可复现的 Java 示例,讲清楚 GeoTools 依赖配置、读取属性、读取几何、创建 Shapefile、写入要素以及常见编码和坐标系问题。

引言:GeoTools库怎么用,先从 Java读写Shapefile 开始

GeoTools 是 Java 生态中常用的开源 GIS 工具库,可以处理矢量数据、坐标参考系、空间过滤、样式渲染和部分栅格数据。在实际工作中,很多人第一次接触 GeoTools,并不是从复杂的地图渲染开始,而是从一个简单需求开始:

  • 用 Java 读取 Shapefile 的字段和几何;
  • 批量检查 Shapefile 属性是否为空;
  • 把数据库或 CSV 中的坐标写成 Shapefile;
  • 读取 Shapefile 后转换坐标系或入库;
  • 解决中文字段乱码、坐标系丢失、写出文件打不开等问题。

本文围绕 GeoTools库怎么用 这个问题,重点演示 Java读写Shapefile 的完整流程。你可以直接把示例改成自己的数据路径和字段结构。

GeoTools库怎么用 Java读写Shapefile流程图
Java 使用 GeoTools 读写 Shapefile 的基本流程:配置依赖、读取要素、处理几何与属性、创建并写出新文件。

背景:为什么 Java 读写 Shapefile 经常出问题

Shapefile 看起来只是一个文件,实际上它通常由多个文件共同组成。最常见的有:

  • .shp:存储几何图形,例如点、线、面;
  • .shx:几何索引文件;
  • .dbf:属性表文件;
  • .prj:坐标参考系文件;
  • .cpg:字符编码说明文件,常用于中文编码识别。

因此,Java 读写 Shapefile 的问题通常不是“能不能读”,而是下面这些细节没有处理好:

  • 只复制了 .shp,没有复制 .dbf、.shx、.prj;
  • 中文字段或中文属性值乱码;
  • 写出的 Shapefile 没有坐标系;
  • 字段名超过 Shapefile 限制导致被截断;
  • 几何类型不一致,例如面图层中写入了点;
  • 没有正确关闭 FeatureIterator、DataStore 或事务。

GeoTools 可以帮我们处理大量底层细节,但仍然需要你理解 Shapefile 的文件结构、字段限制和坐标系机制。

原理:GeoTools 读写 Shapefile 的核心对象

使用 GeoTools 读写 Shapefile,通常会接触到以下几个核心对象。

对象 作用 常见用途
DataStore 数据源抽象 打开 Shapefile、PostGIS 等数据源
SimpleFeatureSource 只读要素源 读取 Shapefile 要素
SimpleFeatureStore 可写要素源 写入、追加、修改要素
SimpleFeatureType 图层结构定义 定义字段、几何类型、坐标系
SimpleFeature 单个空间要素 一条几何和属性记录
Geometry 几何对象 点、线、面、多面等空间图形

可以把它理解为:DataStore 打开数据,FeatureSource 读取数据,FeatureType 描述结构,Feature 表示每一条记录,Geometry 表示空间图形。

步骤:使用 Maven 配置 GeoTools 依赖

如果你使用 Maven 管理 Java 项目,可以先配置 GeoTools 相关依赖。GeoTools 的版本建议在项目中统一用变量管理,并以 Maven Central 或 OSGeo 仓库中可用版本为准。

<properties>
    <geotools.version>请替换为项目实际使用的GeoTools版本</geotools.version>
</properties>

<repositories>
    <repository>
        <id>osgeo</id>
        <name>OSGeo Release Repository</name>
        <url>https://repo.osgeo.org/repository/release/</url>
    </repository>
</repositories>

<dependencies>
    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-shapefile</artifactId>
        <version>${geotools.version}</version>
    </dependency>

    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-main</artifactId>
        <version>${geotools.version}</version>
    </dependency>

    <dependency>
        <groupId>org.geotools</groupId>
        <artifactId>gt-referencing</artifactId>
        <version>${geotools.version}</version>
    </dependency>
</dependencies>

如果只是 Java读写Shapefile,核心依赖是 gt-shapefile。如果还要处理坐标参考系、坐标转换和空间过滤,通常还会用到 gt-referencinggt-epsg-hsql 等模块。

步骤:Java 读取 Shapefile 属性和几何

下面的代码演示如何用 GeoTools 读取 Shapefile。示例会输出每个要素的属性和几何类型。

import org.geotools.data.DataStore;
import org.geotools.data.DataStoreFinder;
import org.geotools.data.simple.SimpleFeatureCollection;
import org.geotools.data.simple.SimpleFeatureIterator;
import org.geotools.data.simple.SimpleFeatureSource;
import org.opengis.feature.Property;
import org.opengis.feature.simple.SimpleFeature;

import java.io.File;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

public class ReadShapefileDemo {

    public static void main(String[] args) throws Exception {
        File shpFile = new File("D:/gis/data/roads.shp");

        Map<String, Object> params = new HashMap<>();
        params.put("url", shpFile.toURI().toURL());
        params.put("charset", Charset.forName("UTF-8"));

        DataStore dataStore = DataStoreFinder.getDataStore(params);

        if (dataStore == null) {
            throw new RuntimeException("无法打开Shapefile,请检查文件路径和依赖配置。");
        }

        String typeName = dataStore.getTypeNames()[0];
        SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
        SimpleFeatureCollection collection = featureSource.getFeatures();

        System.out.println("图层名称:" + typeName);
        System.out.println("要素数量:" + collection.size());
        System.out.println("坐标参考系:" + featureSource.getSchema().getCoordinateReferenceSystem());

        try (SimpleFeatureIterator iterator = collection.features()) {
            while (iterator.hasNext()) {
                SimpleFeature feature = iterator.next();

                System.out.println("Feature ID: " + feature.getID());
                System.out.println("Geometry: " + feature.getDefaultGeometry());

                for (Property property : feature.getProperties()) {
                    System.out.println(property.getName() + " = " + property.getValue());
                }

                System.out.println("----------------------");
            }
        } finally {
            dataStore.dispose();
        }
    }
}

这段代码里有几个关键点:

  • url 参数指向 .shp 文件;
  • charset 参数用于指定属性表编码,中文数据常见为 UTF-8、GBK 或 GB2312;
  • SimpleFeatureSource 适合读取,不适合写入;
  • SimpleFeatureIterator 必须关闭,推荐使用 try-with-resources;
  • dataStore.dispose() 用于释放文件句柄,避免 Windows 下文件被占用。

步骤:Java 写入 Shapefile 文件

写入 Shapefile 的核心流程是:先定义图层结构,再创建文件,再构造要素,最后通过事务写入。下面示例创建一个点图层,字段包括名称和编号。

import org.geotools.data.DataUtilities;
import org.geotools.data.DefaultTransaction;
import org.geotools.data.Transaction;
import org.geotools.data.shapefile.ShapefileDataStore;
import org.geotools.data.shapefile.ShapefileDataStoreFactory;
import org.geotools.data.simple.SimpleFeatureStore;
import org.geotools.feature.DefaultFeatureCollection;
import org.geotools.feature.simple.SimpleFeatureBuilder;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;
import org.opengis.feature.simple.SimpleFeature;
import org.opengis.feature.simple.SimpleFeatureType;

import java.io.File;
import java.io.Serializable;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Map;

public class WriteShapefileDemo {

    public static void main(String[] args) throws Exception {
        File newFile = new File("D:/gis/output/sample_points.shp");

        SimpleFeatureType featureType = DataUtilities.createType(
                "sample_points",
                "the_geom:Point:srid=4326,name:String,id:Integer"
        );

        ShapefileDataStoreFactory dataStoreFactory = new ShapefileDataStoreFactory();

        Map<String, Serializable> params = new HashMap<>();
        params.put("url", newFile.toURI().toURL());
        params.put("create spatial index", Boolean.TRUE);

        ShapefileDataStore dataStore =
                (ShapefileDataStore) dataStoreFactory.createNewDataStore(params);

        dataStore.createSchema(featureType);
        dataStore.setCharset(Charset.forName("UTF-8"));

        GeometryFactory geometryFactory = new GeometryFactory();
        DefaultFeatureCollection featureCollection = new DefaultFeatureCollection();

        SimpleFeatureBuilder featureBuilder = new SimpleFeatureBuilder(featureType);

        Point point1 = geometryFactory.createPoint(new Coordinate(116.391, 39.907));
        featureBuilder.add(point1);
        featureBuilder.add("北京示例点");
        featureBuilder.add(1);
        SimpleFeature feature1 = featureBuilder.buildFeature(null);
        featureCollection.add(feature1);

        Point point2 = geometryFactory.createPoint(new Coordinate(121.473, 31.230));
        featureBuilder.add(point2);
        featureBuilder.add("上海示例点");
        featureBuilder.add(2);
        SimpleFeature feature2 = featureBuilder.buildFeature(null);
        featureCollection.add(feature2);

        String typeName = dataStore.getTypeNames()[0];

        Transaction transaction = new DefaultTransaction("create");

        SimpleFeatureStore featureStore =
                (SimpleFeatureStore) dataStore.getFeatureSource(typeName);

        featureStore.setTransaction(transaction);

        try {
            featureStore.addFeatures(featureCollection);
            transaction.commit();
            System.out.println("Shapefile写入完成:" + newFile.getAbsolutePath());
        } catch (Exception e) {
            transaction.rollback();
            throw e;
        } finally {
            transaction.close();
            dataStore.dispose();
        }
    }
}

运行完成后,输出目录中应该至少生成 .shp、.shx、.dbf、.prj 等文件。如果只看到一个 .shp 文件,通常说明写入流程没有正常完成,或者程序在事务提交前异常退出。

步骤:检查写出的 Shapefile 是否正确

写出文件后,不建议只看“有没有生成文件”。建议按下面方式检查:

  1. 用 QGIS 或 ArcGIS Pro 打开输出的 Shapefile;
  2. 检查图层是否能正常加载;
  3. 打开属性表,确认字段名、字段类型和中文属性是否正常;
  4. 查看图层属性中的坐标参考系是否为 EPSG:4326;
  5. 缩放到图层,确认点位是否落在预期位置;
  6. 检查输出目录中是否存在 .prj 和 .cpg 文件。

如果点位出现在非预期位置,不一定是 GeoTools 写错了,也可能是经纬度顺序、坐标系定义或数据源坐标本身有问题。

常见坑:GeoTools 读取 Shapefile 中文乱码

Java读写Shapefile 最常见的问题之一是中文乱码。原因通常在于 .dbf 文件本身没有统一的编码标准,历史数据可能使用 GBK、GB2312、UTF-8 或其他编码。

读取时可以通过参数指定编码:

params.put("charset", Charset.forName("GBK"));

写入时可以设置 ShapefileDataStore 的编码:

dataStore.setCharset(Charset.forName("UTF-8"));

如果你不确定原始数据编码,可以尝试以下排查顺序:

  • 先查看同目录下是否有 .cpg 文件;
  • 用 QGIS 打开,手动尝试不同编码;
  • 确认数据来源系统导出时的编码设置;
  • 读取时分别尝试 UTF-8、GBK、GB2312;
  • 统一项目输出编码,避免不同环节混用编码。

常见坑:Shapefile 字段名被截断

Shapefile 的属性表基于 dBASE 格式,字段名长度有限。很多情况下,字段名超过限制会被截断。例如 administrative_code 可能在输出后变成较短的字段名。

建议在写出 Shapefile 前主动设计字段名:

  • 字段名尽量使用英文、数字和下划线;
  • 字段名保持简短,例如 name、code、type、area;
  • 不要依赖很长的中文字段名;
  • 如果需要完整字段说明,可以另存一份字段字典表。

如果你的项目需要长字段名、复杂字段类型和更稳定的编码,建议考虑 GeoPackage、PostGIS 或 File Geodatabase,而不是继续强行使用 Shapefile。

常见坑:坐标系和经纬度顺序不一致

示例中使用的坐标为:

new Coordinate(116.391, 39.907)

这里的顺序是 x, y,对于 EPSG:4326 的常见 GIS 使用场景,通常对应 经度, 纬度。很多错误来自把纬度写在前面,经度写在后面。

如果你写出的点跑到海里、国外或地图外,优先检查:

  • 坐标值是否为经度在前、纬度在后;
  • 原始数据到底是 WGS84 经纬度还是投影坐标;
  • 是否把米制坐标误当成经纬度写入 EPSG:4326;
  • .prj 文件是否正确生成;
  • 在 QGIS 中图层坐标系和项目坐标系是否被混淆。

常见坑:FeatureIterator 和 DataStore 没有关闭

在 Windows 环境下,如果你发现 Shapefile 写完后无法删除、无法覆盖,常见原因是文件句柄没有释放。GeoTools 读取 Shapefile 后,一定要关闭迭代器,并调用 DataStore 的 dispose 方法。

try (SimpleFeatureIterator iterator = collection.features()) {
    while (iterator.hasNext()) {
        SimpleFeature feature = iterator.next();
        // 处理要素
    }
} finally {
    dataStore.dispose();
}

如果是写入操作,还要关闭事务:

transaction.close();
dataStore.dispose();

方法比较:GeoTools 读写 Shapefile 与其他方案怎么选

方法 适合场景 优点 限制
GeoTools Java 项目内直接读写 GIS 数据 Java 集成方便,支持数据源多,适合工程化 API 相对复杂,需要理解 Feature 和 CRS
GDAL/OGR 命令行转换、批处理、跨格式转换 格式支持非常广,工具成熟 Java 集成通常需要额外环境配置
QGIS 人工检查、少量数据编辑、可视化验证 直观,适合检查坐标系和属性 不适合纯后端自动化流程
PostGIS 空间数据入库、查询、服务端分析 适合大数据量、多用户和空间索引 不是文件读写工具,需要数据库环境
GeoPackage 替代 Shapefile 的现代文件格式 单文件、字段限制少、编码更稳定 部分老系统仍要求 Shapefile

如果你的目标是 Java 后端系统中直接处理 Shapefile,GeoTools 是合适选择。如果只是一次性格式转换,GDAL 可能更简单。如果是长期存储和空间查询,建议进入 PostGIS。

检查清单:Java读写Shapefile 前后要确认什么

  • 是否引入了 gt-shapefile 依赖;
  • GeoTools 各模块版本是否一致;
  • 输入路径是否指向 .shp 文件,而不是目录;
  • .shp、.shx、.dbf 是否在同一目录;
  • 中文乱码时是否指定了 charset;
  • 写出字段名是否过长;
  • 写出几何类型是否与 schema 定义一致;
  • 是否设置了正确的坐标参考系;
  • 坐标顺序是否为 x, y;
  • FeatureIterator、Transaction、DataStore 是否关闭;
  • 输出文件是否能在 QGIS 或 ArcGIS Pro 中正常打开;
  • 输出目录是否生成 .prj 和 .cpg 文件。

FAQ:GeoTools库怎么用的常见问题

GeoTools 可以直接读取 shp 文件吗?

可以。GeoTools 通过 ShapefileDataStore 或 DataStoreFinder 读取 .shp 文件。实际读取时要确保 .shp、.shx、.dbf 等配套文件完整,否则可能出现无法打开、属性缺失或要素异常。

Java读写Shapefile 必须安装 QGIS 或 ArcGIS 吗?

不必须。GeoTools 是 Java 库,可以在不安装 QGIS 或 ArcGIS 的情况下读写 Shapefile。但建议安装 QGIS 用于结果检查,因为它可以直观看到几何位置、属性表、编码和坐标系是否正常。

GeoTools 读取 Shapefile 中文乱码怎么办?

优先检查 .cpg 文件。如果没有 .cpg,可以在读取参数中指定 charset,例如 UTF-8 或 GBK。不同数据来源的编码可能不同,不能假设所有 Shapefile 都是 UTF-8。

GeoTools 写出的 Shapefile 为什么没有坐标系?

通常是创建 SimpleFeatureType 时没有指定 CRS,或者没有正确生成 .prj 文件。可以在 schema 中设置 srid,例如 Point:srid=4326,也可以通过 CRS 对象构造更明确的 SimpleFeatureType。

GeoTools 能写面 Shapefile 吗?

可以。把 schema 中的几何类型从 Point 改为 Polygon 或 MultiPolygon,并构造对应的 JTS Geometry 对象即可。需要注意,一个 Shapefile 图层只能保持一种主几何类型,不建议在同一个图层中混写点、线、面。

Shapefile 适合存储大量数据吗?

Shapefile 可以存储一定规模的矢量数据,但它有字段名、编码、文件大小和复杂类型方面的限制。对于长期业务系统,建议优先考虑 PostGIS 或 GeoPackage。

结论:用 GeoTools 读写 Shapefile,关键在结构、编码和坐标系

GeoTools库怎么用 这个问题,最好的入门方式就是完成一次完整的 Java读写Shapefile 流程。读取时重点关注 DataStore、FeatureSource、FeatureIterator;写入时重点关注 FeatureType、FeatureCollection、FeatureStore 和 Transaction。

在真实项目中,GeoTools 读写 Shapefile 的难点通常不在 API 调用本身,而在 Shapefile 文件配套、中文编码、字段限制、坐标参考系和资源释放。只要把这些问题逐项检查清楚,Java 处理 Shapefile 就会稳定很多。

如果你只是做简单文件转换,可以考虑 GDAL;如果你要在 Java 服务中集成 GIS 数据处理,GeoTools 更适合;如果数据需要长期管理和高效空间查询,则建议进一步学习 PostGIS。