目录
-
- [一、开篇痛点:前端要的不是 WKT](#一、开篇痛点:前端要的不是 WKT)
- [二、为什么是 GeoJSON?](#二、为什么是 GeoJSON?)
- [三、JTS 1.19.0 的 GeoJSON API 真相](#三、JTS 1.19.0 的 GeoJSON API 真相)
-
- [3.1 代码的"地雷"](#3.1 代码的“地雷”)
- [3.2 真正的类路径与 API](#3.2 真正的类路径与 API)
- [3.3 精度控制的真相](#3.3 精度控制的真相)
- [四、架构设计:全局 WKT + 局部 GeoJSON](#四、架构设计:全局 WKT + 局部 GeoJSON)
-
- [4.1 核心原则](#4.1 核心原则)
- [4.2 为什么不用 Jackson 全局切换?](#4.2 为什么不用 Jackson 全局切换?)
- 五、完整代码实现
-
- [5.1 WKT Serializer 全局默认](#5.1 WKT Serializer 全局默认)
- [5.2 Jackson 全局配置(WKT 为默认)](#5.2 Jackson 全局配置(WKT 为默认))
- [5.3 GeoJSON Serializer](#5.3 GeoJSON Serializer)
- [5.4 地图详情 DTO(局部注解覆盖)](#5.4 地图详情 DTO(局部注解覆盖))
- [5.5 Service](#5.5 Service)
- [5.6 Controller](#5.6 Controller)
- 六、接口返回效果对比(示例)
-
- [6.1 列表页(WKT,全局默认)](#6.1 列表页(WKT,全局默认))
- [6.2 地图详情页(GeoJSON,局部覆盖)](#6.2 地图详情页(GeoJSON,局部覆盖))
- 七、前端对接示例
-
- [7.1 高德地图](#7.1 高德地图)
- [7.2 Leaflet](#7.2 Leaflet)
- 八、踩坑复盘
- 九、性能对比
- 十、与前面篇章的呼应
- 十一、小结
一、开篇痛点:前端要的不是 WKT
在 1.1 篇中,我们用 WKT 解决了 Swagger 展示的问题:
json
"geom": "POINT(116.397 39.903)"
但有时前端可能需要GeoJSON格式
本篇我们就来解决这个问题:如何在同一个项目里,有的接口返回 WKT,有的接口返回 GeoJSON,而且不冲突。
二、为什么是 GeoJSON?
| 格式 | 体积 | 前端解析成本 | 地图组件支持 |
|---|---|---|---|
| WKT | 最小 | 需手写 parser | ✖ 不支持 |
| GeoJSON | 中 | 原生支持 | 全部支持 |
| 完整 Geometry | 最大 | 高 | ⚠ 需适配 |
GeoJSON 是 Web GIS 的事实标准,前端地图组件可以直接消费,不需要任何转换。
三、JTS 1.19.0 的 GeoJSON API 真相
3.1 代码的"地雷"
下面这样的代码:
java
GeoJsonObjectMapper mapper = new GeoJsonObjectMapper();
String geoJson = mapper.writeValueAsString(geometry);
在 JTS 1.19.0 中,这段代码根本无法编译。
3.2 真正的类路径与 API
JTS 1.19.0 中,GeoJSON 相关类在以下包中:
java
import org.locationtech.jts.io.geojson.GeoJsonWriter;
1.19.0 中 GeoJsonWriter 的真实 API:
java
GeoJsonWriter writer = new GeoJsonWriter();
// 写 Geometry → GeoJSON 字符串
String geoJson = writer.write(geometry);
// 释放资源(可选)
writer.close();
3.3 精度控制的真相
java
GeoJsonWriter writer = new GeoJsonWriter();
writer.setPrecision(6); // 编译失败!
JTS 1.19.0 的 GeoJsonWriter 没有 setPrecision() 方法。
- 内部使用
DecimalFormat("0.######") - 默认 6 位小数(≈ 0.1 米精度),不可配置
- 如需控制精度 →
GeometryPrecisionReducer
GIS 技术栈的版本矩阵极其复杂,抄代码前,先确认自己的依赖版本。
四、架构设计:全局 WKT + 局部 GeoJSON
4.1 核心原则
- Entity 里永远是 Geometry,不关心展示格式
- 展示格式由 DTO / Response 决定
- 全局 Jackson 只配置一种(WKT)
- GeoJSON 只在需要地图的 DTO 上用局部注解
4.2 为什么不用 Jackson 全局切换?
Jackson 全局只能配置一个 Serializer。如果试图根据 URL / Header 切换 WKT 和 GeoJSON:
- 需要 ThreadLocal 传递上下文
- 复杂度爆炸
- 并发场景下极易出 Bug
答案:用 DTO 隔离展示格式,比任何"巧妙的 Serializer"都稳健。
五、完整代码实现
5.1 WKT Serializer 全局默认
java
package org.pgd.postgisdemo.handler;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKTWriter;
import java.io.IOException;
/**
* 全局默认:Geometry → WKT
*/
public class GeometryJsonSerializer extends JsonSerializer<Geometry> {
private static final WKTWriter WKT_WRITER = new WKTWriter();
@Override
public void serialize(Geometry value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(WKT_WRITER.write(value));
}
}
5.2 Jackson 全局配置(WKT 为默认)
java
package org.pgd.postgisdemo.config;
import com.fasterxml.jackson.databind.module.SimpleModule;
import org.locationtech.jts.geom.Geometry;
import org.pgd.postgisdemo.handler.GeometryJsonSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JacksonConfig {
@Bean
public SimpleModule geometryModule() {
SimpleModule module = new SimpleModule();
// 全局默认:Geometry → WKT
module.addSerializer(Geometry.class, new GeometryJsonSerializer());
return module;
}
}
5.3 GeoJSON Serializer
java
package org.pgd.postgisdemo.handler;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.geojson.GeoJsonWriter;
import java.io.IOException;
/**
* 局部使用:Geometry → GeoJSON
* JTS 1.19.0 正确实现
*/
public class GeometryGeoJsonSerializer extends JsonSerializer<Geometry> {
private static final GeoJsonWriter GEO_JSON_WRITER = new GeoJsonWriter();
@Override
public void serialize(Geometry value, JsonGenerator gen,
SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
// writeRawValue:直接写 JSON 对象,不是字符串
String geoJson = GEO_JSON_WRITER.write(value);
gen.writeRawValue(geoJson);
}
}
5.4 地图详情 DTO(局部注解覆盖)
java
package org.pgd.postgisdemo.dto;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import org.locationtech.jts.geom.Geometry;
import org.pgd.postgisdemo.handler.GeometryGeoJsonSerializer;
public class PoiMapDetailResp {
private Long id;
private String name;
private String category;
/**
* 局部覆盖:这个字段用 GeoJSON
*/
@JsonSerialize(using = GeometryGeoJsonSerializer.class)
private Geometry geom;
// getter / setter ...
}
5.5 Service
java
@Service
public class PoiServiceImpl extends ServiceImpl<PoiMapper, PoiEntity>
implements PoiService {
@Override
public PoiListResp listPoi(...) {
// 列表页:WKT(全局默认)
PoiEntity e = getById(id);
PoiListResp resp = new PoiListResp();
resp.setId(e.getId());
resp.setName(e.getName());
resp.setLon(e.getGeom().getCoordinate().x);
resp.setLat(e.getGeom().getCoordinate().y);
return resp;
}
@Override
public PoiMapDetailResp mapDetail(Long id) {
// 地图详情页:GeoJSON(局部注解)
PoiEntity e = getById(id);
PoiMapDetailResp resp = new PoiMapDetailResp();
resp.setId(e.getId());
resp.setName(e.getName());
resp.setGeom(e.getGeom());
return resp;
}
}
5.6 Controller
java
@RestController
@RequestMapping("/poi")
public class PoiController {
@GetMapping("/list")
public R<PoiListResp> list() {
return R.ok(poiService.listPoi(...));
}
@GetMapping("/map/detail/{id}")
public R<PoiMapDetailResp> mapDetail(@PathVariable Long id) {
return R.ok(poiService.mapDetail(id));
}
}
六、接口返回效果对比(示例)
6.1 列表页(WKT,全局默认)
json
{
"id": 1,
"name": "天安门",
"category": "景点",
"lon": 116.397,
"lat": 39.903
}
6.2 地图详情页(GeoJSON,局部覆盖)
json
{
"id": 1,
"name": "天安门",
"category": "景点",
"geom": {
"type": "Point",
"coordinates": [116.397, 39.903]
}
}
七、前端对接示例
7.1 高德地图
javascript
const marker = new AMap.Marker({
position: [data.geom.coordinates[0], data.geom.coordinates[1]]
});
marker.setMap(map);
7.2 Leaflet
javascript
L.geoJSON(data.geom).addTo(map);
一行代码渲染,无需任何解析
八、踩坑复盘
-
坑 1:
writeStringvswriteRawValue一开始用
gen.writeString(geoJson),前端拿到的是"{\"type\":\"Point\",...}"(带引号的字符串,不是 JSON 对象)。改成gen.writeRawValue(geoJson)才正确。 -
坑 2:
GeoJsonObjectMapper不存在*** ✖ 不存在
GeoJsonObjectMapper类* 正确类是
GeoJsonWriter* 包路径:
org.locationtech.jts.io.geojson -
坑 3:
setPrecision()不存在JTS 1.19.0 的
GeoJsonWriter没有setPrecision()方法。默认 6 位小数,不可配置。如需控制精度 →GeometryPrecisionReducer;前端展示 →toFixed(6)。 -
坑 4:Polygon 坐标顺序**
GeoJSON 标准是
(经度, 纬度),和 PostGIS 一致。但百度地图 BD-09 需要额外转换,不能直接用。
九、性能对比
| 格式 | 单条体积 | 10 万条序列化耗时 | 前端解析 |
|---|---|---|---|
| WKT | ~30 字节 | ~120ms | 低 |
| GeoJSON | ~60 字节 | ~180ms | 极低 |
| 完整 Geometry | ~500 字节 | ~800ms | 高 |
GeoJSON 体积只比 WKT 大一倍,但前端省掉整个 parser,性价比非常高。
十、与前面篇章的呼应
- 第一篇 :Geometry 字段 CRUD + TypeHandler,解决 Geometry 对象的存取。
- 1.1 篇 :Swagger 优雅展示,解决 WKT 格式的可读性问题。
- 第 4 篇 (本篇) :GeoJSON 序列化 + 前端对接,解决 GeoJSON 格式的地图可用性。
十一、小结
WKT 是给程序员看的,GeoJSON 是给地图看的。
- 1.1 篇解决了"Swagger 可读",本篇解决了"前端可用"。
- 展示格式是 DTO 的事,不是 Entity 的事,更不是 Jackson 的事。
- 用 DTO 隔离 WKT / GeoJSON,比任何"巧妙的 Serializer 切换"都稳健。
从 WKT 到 GeoJSON,只差一个 Serializer 的距离。