一、Maven 依赖
xml
<!-- GDAL Java 绑定(解析 ESRI File Geodatabase) -->
<dependency>
<groupId>org.gdal</groupId>
<artifactId>gdal</artifactId>
<version>${gdal.version}</version>
</dependency>
<!-- GeoTools(解析 Shapefile / GeoJSON / GPKG) -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>${geotools.version}</version>
</dependency>
底层通过 GDAL + GeoTools 双引擎解析地理空间数据格式(.gdb、.shp、.geojson、.gpkg 等)。
二、接口定义
文件路径: /.../xzq/controller/JcXzqController.java
java
@ApiOperation("导入ZIP文件(包含地理空间数据GBD格式文件)")
@CommonLog("导入行政区ZIP文件")
@PostMapping("/base/xzq/importZip")
public CommonResult<Map<String, Object>> importZip(
@RequestParam("file") MultipartFile file,
@RequestParam(value = "overwriteMode", defaultValue = "false") Boolean overwriteMode) {
JcXzqZipImportParam param = new JcXzqZipImportParam();
param.setFile(file);
param.setOverwriteMode(overwriteMode);
return CommonResult.data(jcXzqService.importZipFile(param));
}
- 请求方式: POST
- 参数:
file(MultipartFile) +overwriteMode(boolean, 默认 false) - 返回:
{totalCount, successCount, errorCount, layerCounts, errorDetail}
三、服务入口方法
java
@Transactional(rollbackFor = Exception.class)
@Override
public Map<String, Object> importZipFile(JcXzqZipImportParam jcXzqZipImportParam) {
MultipartFile file = jcXzqZipImportParam.getFile();
boolean overwriteMode = Boolean.TRUE.equals(jcXzqZipImportParam.getOverwriteMode());
Map<String, Object> result = new HashMap<>();
List<String> errorList = new ArrayList<>();
Map<String, Integer> layerCounts = new HashMap<>();
int totalCount = 0, successCount = 0, errorCount = 0;
// 创建临时目录
String tempDir = System.getProperty("java.io.tmpdir") + File.separator + "xzq_import_" + IdUtil.simpleUUID();
Path tempPath = Paths.get(tempDir);
try {
Files.createDirectories(tempPath);
// 1. 校验文件格式
if (!file.getOriginalFilename().toLowerCase().endsWith(".zip")) {
throw new CommonException("文件格式错误,仅支持ZIP格式文件");
}
// 2. 解压ZIP
unzipFile(file, tempPath);
// 3. 查找GDB文件
List<File> gdbFiles = findGdbFiles(tempPath.toFile());
if (gdbFiles.isEmpty()) {
throw new CommonException("ZIP文件中未找到GDB格式文件");
}
// 4. 解析每个GDB文件
for (File gdbFile : gdbFiles) {
GbdFileParser.MultiLayerGdbResult gdbResult = GbdFileParser.parseMultiLayerGdbWithGdal(gdbFile);
for (String layerName : gdbResult.getLayerNames()) {
List<?> layerData = gdbResult.getLayerDataMap().get(layerName);
if (layerData == null || layerData.isEmpty()) continue;
int layerSuccess = processLayerData(layerName, layerData, overwriteMode, errorList);
layerCounts.put(layerName, layerSuccess);
totalCount += layerData.size();
successCount += layerSuccess;
errorCount += (layerData.size() - layerSuccess);
}
}
result.put("totalCount", totalCount);
result.put("successCount", successCount);
result.put("errorCount", errorCount);
result.put("layerCounts", layerCounts);
result.put("errorDetail", errorList);
} catch (Exception e) {
throw new CommonException("GDB文件导入失败:{}", e.getMessage());
} finally {
FileUtil.del(tempDir);
}
return result;
}
四、核心方法详解
4.1 ZIP 解压
java
private void unzipFile(MultipartFile file, Path destPath) throws IOException {
try (ZipInputStream zipInputStream = new ZipInputStream(file.getInputStream(), StandardCharsets.UTF_8)) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
if (!entry.isDirectory()) {
Path filePath = destPath.resolve(entry.getName());
Files.createDirectories(filePath.getParent());
try (FileOutputStream fos = new FileOutputStream(filePath.toFile())) {
IoUtil.copy(zipInputStream, fos);
}
}
zipInputStream.closeEntry();
}
}
}
逻辑: 使用 ZipInputStream + UTF-8 逐条解压 ZIP 中的文件/目录到临时目录,保留原始目录结构(.gdb 内部包含多个系统文件,必须完整保留)。
4.2 GDB 文件递归查找
java
private List<File> findGdbFiles(File directory) {
List<File> gdbFiles = new ArrayList<>();
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
if (file.getName().toLowerCase().endsWith(".gdb")) {
gdbFiles.add(file); // .gdb 在文件系统中是一个目录
} else {
gdbFiles.addAll(findGdbFiles(file)); // 递归子目录
}
} else if (isGdbFile(file.getName())) {
gdbFiles.add(file);
}
}
}
return gdbFiles;
}
逻辑: ESRI File Geodatabase 在磁盘上表现为一个目录(xxx.gdb/),内部包含 .gdbtable、.freelist、.gdbindexes 等文件。递归遍历解压目录,找到所有以 .gdb 结尾的目录/文件。
4.3 GDB 多图层解析(GDAL 方式)
核心类: GbdFileParser.java
路径: /.../xzq/util/GbdFileParser.java
java
/**
* 使用GDAL解析ESRI File Geodatabase (.gdb)文件
*
* 注意:此方法需要额外的依赖和配置:
* 1. 添加GDAL Java绑定依赖
* 2. 安装GDAL本地库
* 3. 配置GDAL环境变量
*
* Maven依赖示例:
* <dependency>
* <groupId>org.gdal</groupId>
* <artifactId>gdal</artifactId>
* <version>3.6.0</version>
* </dependency>
*
* @param gdbFile GDB文件(实际上是文件夹)
* @return 解析后的行政区数据列表
* @throws IOException 解析异常
*/
/**
* 使用GDAL解析多图层GDB文件
*
* @param gdbFile GDB文件路径
* @return 多图层解析结果
* @throws IOException 解析异常
*/
public static MultiLayerGdbResult parseMultiLayerGdbWithGdal(File gdbFile) throws IOException {
checkGdalEnvironment();
MultiLayerGdbResult result = new MultiLayerGdbResult();
// 初始化GDAL/OGR
ogr.RegisterAll();
gdal.AllRegister();
// 打开GDB数据源(只读模式)
DataSource dataSource = ogr.Open(gdbFile.getAbsolutePath(), 0);
// 遍历所有图层
int layerCount = dataSource.GetLayerCount();
for (int i = 0; i < layerCount; i++) {
Layer layer = dataSource.GetLayer(i);
String layerName = layer.GetName();
List<?> layerData = createLayerDataList(layerName);
// 遍历图层中的要素
layer.ResetReading();
org.gdal.ogr.Feature feature;
while ((feature = layer.GetNextFeature()) != null) {
Object entity = convertGdalFeatureToEntity(feature, layerName);
if (entity != null) addEntityToList(layerData, entity);
feature.delete(); // 必须手动释放
}
result.addLayerData(layerName, layerData);
}
dataSource.delete();
return result;
}
逻辑:
- 检测 GDAL 环境变量(
GDAL_DATA、PROJ_LIB、java.library.path) ogr.RegisterAll()+gdal.AllRegister()注册所有驱动ogr.Open()打开 GDB 数据源(只读模式 0)dataSource.GetLayerCount()+GetLayer(i)遍历所有数据图层- 每图层中
GetNextFeature()遍历要素,转为业务实体 feature.delete()和dataSource.delete()必须显式调用释放 GDAL 内存
4.4 GDAL Feature → 业务实体转换
java
private static Object convertGdalFeatureToEntity(org.gdal.ogr.Feature gdalFeature, String layerName) {
FeatureDefn featureDefn = gdalFeature.GetDefnRef();
int fieldCount = featureDefn.GetFieldCount();
// 获取几何数据(WKT格式)
String geomWkt = null;
org.gdal.ogr.Geometry geometry = gdalFeature.GetGeometryRef();
if (geometry != null) {
geomWkt = geometry.ExportToWkt();
}
// 按图层名称分流转换
switch (layerName.toLowerCase()) {
case "xzq": return convertGdalFeatureToXzq(gdalFeature, featureDefn, fieldCount, geomWkt);
case "dxssyjjbxx": return convertGdalFeatureToDxssyjjbxx(gdalFeature, featureDefn, fieldCount, geomWkt);
case "dxszy_1_5jfq":return convertGdalFeatureToDxszy15jfq(gdalFeature, featureDefn, fieldCount, geomWkt);
case "gczjd": return convertGdalFeatureToGczjd(gdalFeature, featureDefn, fieldCount, geomWkt);
case "pdt": return convertGdalFeatureToPdt(gdalFeature, featureDefn, fieldCount, geomWkt);
case "szy_1_3jfq": return convertGdalFeatureToSzy13jfq(gdalFeature, featureDefn, fieldCount, geomWkt);
default: return convertGdalFeatureToXzq(gdalFeature, featureDefn, fieldCount, geomWkt);
}
}
字段映射示例(以 Xzq 为例):
java
private static JcXzq convertGdalFeatureToXzq(org.gdal.ogr.Feature gdalFeature,
FeatureDefn featureDefn,
int fieldCount, String geomWkt) {
JcXzq xzq = new JcXzq();
for (int i = 0; i < fieldCount; i++) {
FieldDefn fieldDefn = featureDefn.GetFieldDefn(i);
String fieldName = fieldDefn.GetName().toLowerCase();
if (gdalFeature.IsFieldSet(i)) {
String value = gdalFeature.GetFieldAsString(i);
if (value != null && !value.trim().isEmpty()) {
mapAttributeToXzq(xzq, fieldName, value);
}
}
}
xzq.setGeom(geomWkt);
return isValidXzq(xzq) ? xzq : null;
}
逻辑: 遍历 Feature 的字段定义,根据字段名(支持中文/英文模糊匹配如 xzqdm / 行政区代码 / adcode)映射到实体属性;几何数据通过 ExportToWkt() 转为 WKT 字符串存入 geom 字段。
4.5 GeoTools 方式解析(非 GDB 格式)
对于 Shapefile、GeoPackage、GeoJSON 等格式,使用 GeoTools 库解析(无需 GDAL):
java
// Shapefile 解析
Map<String, Object> map = new HashMap<>();
map.put("url", shpFile.toURI().toURL());
map.put("charset", "UTF-8"); // 失败时自动降级 GBK
DataStore dataStore = DataStoreFinder.getDataStore(map);
SimpleFeatureSource featureSource = dataStore.getFeatureSource(typeName);
SimpleFeatureCollection features = featureSource.getFeatures();
try (SimpleFeatureIterator iterator = features.features()) {
while (iterator.hasNext()) {
SimpleFeature feature = iterator.next();
JcXzq xzq = convertFeatureToXzq(feature);
// 坐标系自动转换到 WGS84
CoordinateReferenceSystem sourceCRS = feature.getFeatureType()
.getCoordinateReferenceSystem();
CoordinateReferenceSystem targetCRS = CRS.decode("EPSG:4326", true);
MathTransform transform = CRS.findMathTransform(sourceCRS, targetCRS, true);
Geometry geometry = JTS.transform((Geometry) feature.getDefaultGeometry(), transform);
xzq.setGeom(new WKTWriter().write(geometry));
}
}
逻辑: GeoTools 通过 DataStoreFinder 自动识别格式,读取 Feature 后完成字段映射 + 坐标系转换(自动转为 WGS84)。
五、注意事项
- GDAL 环境 --- 解析 .gdb 格式必须安装 GDAL 本地库并配置
PATH、GDAL_DATA、PROJ_LIB环境变量及java.library.pathJVM 参数。如环境配置困难可使用ogr2ogr命令行将 GDB 转 Shapefile 后通过 GeoTools 解析。 - GDB 目录结构 --- ESRI File Geodatabase 在文件系统上是一个目录,解压时必须保留完整目录结构和所有内部文件,否则 GDAL 无法打开。
- 编码问题 --- 中国 Spatial Data 常用 GBK 编码,Shapefile 的
.dbf属性文件可能是 GBK。GeoTools 自动降级:UTF-8 失败后自动切 GBK。 - 坐标转换 --- 中国常用坐标系(Xian80 / Beijing54 / CGCS2000)需配置 EPSG 参数才能正确转为 WGS84。
CRS.AxisOrder.NORTH_EAST(如 EPSG:4326)需设置true参数启用强制 XY 顺序。 - GDAL 内存释放 --- GDAL Java 绑定的 Feature 和 DataSource 对象必须显式调用
delete()释放,否则会导致内存泄漏。 - 大文件事务 ---
@Transactional标注在整个方法上,GDB 数据量大时可能导致事务超时,建议在processLayerData内部分段批量提交。