1. 项目概述
在GIS(地理信息系统)开发中,处理SHP(Shapefile)文件是一项常见需求。SHP文件是ESRI公司开发的一种空间数据开放格式,广泛应用于地理空间数据的存储和交换。本文将详细介绍如何在Spring Boot项目中实现上传ZIP压缩包并解析其中的SHP文件,统计地块(多边形要素)数量的完整解决方案。
这个功能在实际业务场景中非常实用,比如:
- 国土空间规划中的地块数据上传与分析
- 不动产登记系统中的宗地信息处理
- 农业信息化中的农田地块统计
- 城市规划中的用地性质分析
2. 环境准备与依赖配置
2.1 项目基础环境
首先确保你已经创建了一个基本的Spring Boot项目(建议使用Spring Boot 2.7.x或3.x版本)。我们将使用Maven作为构建工具,但Gradle配置也是类似的。
2.2 关键依赖说明
在pom.xml中添加以下核心依赖:
xml复制<!-- GeoTools核心库 -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>29.0</version>
</dependency>
<!-- Shapefile支持 -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-shapefile</artifactId>
<version>29.0</version>
</dependency>
<!-- 坐标系统支持 -->
<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-epsg-hsql</artifactId>
<version>29.0</version>
</dependency>
<!-- Spring Web MVC -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
注意:GeoTools版本需要保持一致(这里使用29.0)。不同版本间API可能有变化,建议查看官方文档确认兼容性。
2.3 依赖冲突解决
GeoTools可能会与其他库产生依赖冲突,特别是JTS(几何计算库)版本。如果遇到问题,可以添加以下排除:
xml复制<dependency>
<groupId>org.geotools</groupId>
<artifactId>gt-main</artifactId>
<version>29.0</version>
<exclusions>
<exclusion>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
</exclusion>
</exclusions>
</dependency>
然后显式指定JTS版本:
xml复制<dependency>
<groupId>org.locationtech.jts</groupId>
<artifactId>jts-core</artifactId>
<version>1.19.0</version>
</dependency>
3. 核心代码实现
3.1 文件上传控制器
创建FileController.java文件,实现SHP文件上传和解析的核心逻辑:
java复制@RestController
@RequestMapping("/api/shp")
public class ShpFileController {
// 临时目录基础路径
private static final String BASE_TEMP_DIR = "temp";
@PostMapping("/upload")
public ResponseEntity<Map<String, Object>> uploadAndAnalyzeShp(
@RequestParam("file") MultipartFile zipFile) {
Map<String, Object> result = new HashMap<>();
Path tempDir = null;
try {
// 1. 创建临时目录
Path baseTempPath = Paths.get(BASE_TEMP_DIR).toAbsolutePath().normalize();
Files.createDirectories(baseTempPath);
tempDir = Files.createTempDirectory(baseTempPath, "shp-upload-");
// 2. 解压ZIP文件
unzipFile(zipFile, tempDir.toString());
// 3. 查找SHP文件
String shpPath = findShpFile(tempDir.toFile());
if (shpPath == null) {
throw new IllegalArgumentException("ZIP文件中未找到.shp文件");
}
// 4. 解析SHP文件
Map<String, Object> analysis = analyzeShpFile(shpPath);
// 5. 构建响应
result.put("success", true);
result.put("data", analysis);
} catch (Exception e) {
result.put("success", false);
result.put("message", "处理失败: " + e.getMessage());
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(result);
} finally {
// 6. 清理临时文件
if (tempDir != null) {
try {
deleteDirectoryRecursively(tempDir);
} catch (IOException e) {
log.warn("临时目录清理失败: {}", tempDir, e);
}
}
}
return ResponseEntity.ok(result);
}
// 其他辅助方法将在下面详细介绍...
}
3.2 文件解压方法
实现安全的ZIP文件解压功能,包含路径遍历攻击防护:
java复制private void unzipFile(MultipartFile zipFile, String destDir) throws IOException {
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry entry;
Path destPath = Paths.get(destDir).normalize();
while ((entry = zis.getNextEntry()) != null) {
Path filePath = destPath.resolve(entry.getName()).normalize();
// 安全检查:防止路径遍历攻击
if (!filePath.startsWith(destPath)) {
throw new IOException("非法文件路径: " + entry.getName());
}
if (entry.isDirectory()) {
Files.createDirectories(filePath);
} else {
Files.createDirectories(filePath.getParent());
Files.copy(zis, filePath, StandardCopyOption.REPLACE_EXISTING);
}
zis.closeEntry();
}
}
}
重要提示:路径规范化(normalize())和安全检查是必须的,可以防止恶意构造的ZIP文件覆盖系统文件。
3.3 SHP文件查找
递归查找解压目录中的.shp文件(不区分大小写):
java复制private String findShpFile(File dir) {
File[] files = dir.listFiles();
if (files == null) return null;
for (File file : files) {
if (file.isFile() && file.getName().toLowerCase().endsWith(".shp")) {
return file.getAbsolutePath();
} else if (file.isDirectory()) {
String found = findShpFile(file);
if (found != null) return found;
}
}
return null;
}
3.4 SHP文件解析
核心的SHP文件解析逻辑:
java复制private Map<String, Object> analyzeShpFile(String shpPath) throws IOException {
File file = new File(shpPath);
ShapefileDataStore dataStore = new ShapefileDataStore(file.toURI().toURL());
dataStore.setCharset(StandardCharsets.UTF_8); // 支持中文属性
try {
SimpleFeatureSource source = dataStore.getFeatureSource();
SimpleFeatureCollection features = source.getFeatures();
Map<String, Object> result = new HashMap<>();
// 空文件检查
if (features.isEmpty()) {
result.put("geometryType", "EMPTY");
result.put("featureCount", 0);
return result;
}
// 获取几何类型
String geomType = determineGeometryType(features);
result.put("geometryType", geomType);
result.put("featureCount", features.size());
// 如果是多边形,计算总面积
if ("POLYGON".equalsIgnoreCase(geomType) || "MULTIPOLYGON".equalsIgnoreCase(geomType)) {
result.put("totalArea", calculateTotalArea(features));
}
return result;
} finally {
dataStore.dispose(); // 释放文件锁
}
}
private String determineGeometryType(SimpleFeatureCollection features) throws IOException {
try (SimpleFeatureIterator it = features.features()) {
if (it.hasNext()) {
Object geom = it.next().getDefaultGeometry();
if (geom instanceof Geometry) {
return ((Geometry) geom).getGeometryType().toUpperCase();
}
}
}
return "UNKNOWN";
}
private double calculateTotalArea(SimpleFeatureCollection features) throws IOException {
double total = 0;
try (SimpleFeatureIterator it = features.features()) {
while (it.hasNext()) {
Object geom = it.next().getDefaultGeometry();
if (geom instanceof Polygon) {
total += ((Polygon) geom).getArea();
} else if (geom instanceof MultiPolygon) {
total += ((MultiPolygon) geom).getArea();
}
}
}
return total;
}
4. 高级功能扩展
4.1 批量处理多个SHP文件
实际项目中可能需要处理包含多个SHP文件的ZIP包:
java复制private List<Map<String, Object>> batchAnalyzeShpFiles(File dir) throws IOException {
List<Map<String, Object>> results = new ArrayList<>();
File[] files = dir.listFiles();
if (files == null) return results;
for (File file : files) {
if (file.isFile() && file.getName().toLowerCase().endsWith(".shp")) {
results.add(analyzeShpFile(file.getAbsolutePath()));
} else if (file.isDirectory()) {
results.addAll(batchAnalyzeShpFiles(file));
}
}
return results;
}
4.2 属性字段提取
除了几何信息,我们还可以提取SHP文件的属性数据:
java复制private Map<String, Object> extractAttributes(SimpleFeature feature) {
Map<String, Object> attributes = new LinkedHashMap<>();
for (Property property : feature.getProperties()) {
if (!(property instanceof GeometryAttribute)) {
attributes.put(property.getName().getLocalPart(),
property.getValue());
}
}
return attributes;
}
4.3 坐标系转换
处理不同坐标系的SHP文件:
java复制private SimpleFeatureCollection reprojectFeatures(
SimpleFeatureCollection features, String targetCRS) throws Exception {
CoordinateReferenceSystem sourceCRS = features.getSchema()
.getCoordinateReferenceSystem();
CoordinateReferenceSystem target = CRS.decode(targetCRS);
MathTransform transform = CRS.findMathTransform(sourceCRS, target, true);
return FeatureUtilities.transform(features, transform);
}
5. 性能优化与注意事项
5.1 内存管理
处理大型SHP文件时需要注意内存使用:
- 使用FeatureIterator后必须关闭:防止内存泄漏
- 分块处理:对于超大文件,可以分批读取
- 及时释放资源:ShapefileDataStore使用后调用dispose()
5.2 文件处理最佳实践
- 临时文件清理:确保上传处理后删除临时文件
- 文件大小限制:Spring Boot默认文件上传限制为1MB,需要配置:
properties复制spring.servlet.multipart.max-file-size=50MB spring.servlet.multipart.max-request-size=50MB - 并发处理:如果并发量大,考虑使用唯一临时目录名
5.3 常见错误处理
| 错误类型 | 原因 | 解决方案 |
|---|---|---|
| 文件锁定 | Windows系统下SHP文件被锁定 | 确保调用dataStore.dispose() |
| 中文乱码 | 属性字段编码问题 | 设置dataStore.setCharset() |
| 空指针异常 | 几何字段为空 | 检查feature.getDefaultGeometry() |
| 坐标系错误 | CRS未识别 | 添加gt-epsg-hsql依赖 |
6. 前端集成示例
6.1 基本HTML表单
html复制<form id="uploadForm" enctype="multipart/form-data">
<input type="file" name="file" accept=".zip" required>
<button type="submit">上传分析</button>
</form>
<div id="result"></div>
6.2 AJAX上传处理
javascript复制document.getElementById('uploadForm').addEventListener('submit', async (e) => {
e.preventDefault();
const formData = new FormData();
formData.append('file', document.querySelector('input[type=file]').files[0]);
try {
const response = await fetch('/api/shp/upload', {
method: 'POST',
body: formData
});
const result = await response.json();
if (result.success) {
displayResults(result.data);
} else {
alert('处理失败: ' + result.message);
}
} catch (error) {
alert('上传出错: ' + error.message);
}
});
function displayResults(data) {
const resultDiv = document.getElementById('result');
let html = `<h3>分析结果</h3>
<p>几何类型: ${data.geometryType}</p>
<p>要素数量: ${data.featureCount}</p>`;
if (data.totalArea) {
html += `<p>总面积: ${data.totalArea.toFixed(2)} 平方米</p>`;
}
resultDiv.innerHTML = html;
}
7. 部署注意事项
- 临时目录权限:确保应用有权限创建和写入临时目录
- 文件系统监控:定期清理旧的临时文件
- 日志记录:记录文件处理过程中的关键事件
- 安全防护:
- 限制上传文件类型
- 扫描恶意文件
- 设置合理的超时时间
在实际项目中,我曾遇到一个案例:用户上传的SHP文件包含数千个复杂多边形,导致内存溢出。解决方案是实现了分块处理机制,每次只加载部分要素进行分析。这也提醒我们,在生产环境中必须考虑极端情况下的系统稳定性。