MyBatis-Plus + PostGIS 实战(四):GeoJSON 序列化与前端地图对接,全局 WKT + 局部 GeoJSON,两种格式优雅共存

目录

    • [一、开篇痛点:前端要的不是 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:writeString vs writeRawValue

    一开始用 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 的距离。

相关推荐
峥嵘life1 小时前
Andorid 打开热点,关闭有线网,ifconfig卡死分析解决
java·服务器·网络
HillVue1 小时前
Token 叙事退潮,DAA 重新定义 AI 价值标准
java·人工智能·eclipse
行者全栈架构师1 小时前
【码动四季】Spring Boot 可观测性体系:Micrometer + OpenTelemetry + Grafana 全链路搭建
java·算法·架构
TDengine (老段)2 小时前
TDengine JOIN 完整语法 — Inner/Outer/ASOF/Window 全语法详解
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
@灯神2 小时前
SpringAI系列|第1篇:SpringAI概述与快速上手
java·人工智能·spring·ai·ai编程
中国搜索直付通3 小时前
避开直付通选型暗礁:二级商户的合规生存与背景甄别
java·大数据·开发语言·人工智能·游戏
不才不才不不才3 小时前
Spring AI 实战(7):向量库怎么选?PgVector/Redis/Milvus 横向对比
java·人工智能·spring·ai
Qimooidea3 小时前
祁木 CAD Translator 深度评测:从参数解析到工程交付实战
java·开发语言·人工智能·机器翻译