(依旧叠甲,我也是菜鸟,悉知我的功力还很浅薄,如果有哪里做的不行欢迎批评指正,大部分代码都是借助AI辅助编写的,可能会有些问题,遇到可留言指正)
        Day 3:

书接上回,咱是用写了一下基本代码(其实还有很多细节,但是不是很重要,大家可以看看老师的教程,自己ai补补),接下来咱们开始写一下这些代码。

"以上所有代码均已在本地跑通,JDK 21 + Spring Boot 3.5.14 + PostgreSQL 17,可以放心使用。"

一、搭建基础 CRUD 结构

1.model类

首先是PointModel,先加两个参数,与数据库对应上,先把流程跑通,springboot要善用注解,@Getter和@Setter,就不用写getter和setter了。

路径src/main/java/com/history/gismap/model/PointModel.java

package com.history.gismap.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.locationtech.jts.geom.Geometry;

@Getter
@Setter
@ToString
public class PointModel {

    private Integer gId;
    private String nameCh;
    private Geometry geometry;

}

说明:使用 Lombok 注解简化 getter/setter/toString,geometry 字段使用 JTS 的 Geometry 类型。

2.Dao类

注意把class改成了interface,加了@Service注释,只新建了一个getCntyPoint的方法,跑下流程,加@Param注释,注释里的gId是sql中的#{gId}。

路径src/main/java/com/history/gismap/dao/MapDao.java

package com.history.gismap.dao;

import com.history.gismap.model.GeometryModel;
import com.history.gismap.model.PointModel;
import org.apache.ibatis.annotations.Param;
import java.util.List;

public interface MapDao {

    PointModel getCntyPoint(@Param("gId") Integer gId);

    int insertCntyPoint(PointModel pointModel);

    int updateCntyPoint(PointModel pointModel);

    int deleteCntyPoint(@Param("gId") Integer gId);

    List<GeometryModel> getDynastyGeom(@Param("category") String category);

    List<GeometryModel> getAllGeometry();

}

3. 创建 Mapper XML 映射文件

路径src/main/resources/mapper/HistoryGISMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.history.gismap.dao.MapDao">

    <resultMap id="pointModelMap" type="com.history.gismap.model.PointModel">
        <id property="gId" column="gid"/>
        <result property="nameCh" column="name_ch"/>
        <result property="geometry" column="geom"
                typeHandler="com.history.gismap.mybatis.GeometryTypeHandler"/>
    </resultMap>

    <resultMap id="geometryModelMap" type="com.history.gismap.model.GeometryModel">
        <id property="gId" column="gid"/>
        <result property="namePy" column="name_py"/>
        <result property="nameCh" column="name_ch"/>
        <result property="nameFt" column="name_ft"/>
        <result property="presLoc" column="pres_loc"/>
        <result property="typePy" column="type_py"/>
        <result property="typeCh" column="type_ch"/>
        <result property="levRank" column="lev_rank"/>
        <result property="begYr" column="beg_yr"/>
        <result property="begRule" column="beg_rule"/>
        <result property="endYr" column="end_yr"/>
        <result property="endRule" column="end_rule"/>
        <result property="geoSrc" column="geo_src"/>
        <result property="compiler" column="compiler"/>
        <result property="gecomplr" column="gecomplr"/>
        <result property="checker" column="checker"/>
        <result property="entDate" column="ent_date"/>
        <result property="begChgTy" column="beg_chg_ty"/>
        <result property="endChgTy" column="end_chg_ty"/>
        <result property="geometry" column="geom"
                typeHandler="com.history.gismap.mybatis.GeometryTypeHandler"/>
    </resultMap>

    <select id="getCntyPoint" resultMap="pointModelMap">
        SELECT gid, name_ch, geom
        FROM v6_time_cnty_pts_utf_wgs84
        WHERE gid = #{gId}
    </select>

    <select id="getAllGeometry" resultMap="geometryModelMap">
        SELECT gid, name_py, name_ch, name_ft, pres_loc, type_py, type_ch,
               lev_rank, beg_yr, beg_rule, end_yr, end_rule, geo_src,
               compiler, gecomplr, checker, ent_date, beg_chg_ty, end_chg_ty, geom
        FROM v6_time_cnty_pts_utf_wgs84
    </select>

    <select id="getDynastyGeom" resultMap="geometryModelMap">
        SELECT gid, name_py, name_ch, name_ft, pres_loc, type_py, type_ch,
               lev_rank, beg_yr, beg_rule, end_yr, end_rule, geo_src,
               compiler, gecomplr, checker, ent_date, beg_chg_ty, end_chg_ty, geom
        FROM v6_time_cnty_pts_utf_wgs84
        WHERE name_ch = #{category}
    </select>

    <insert id="insertCntyPoint" parameterType="com.history.gismap.model.PointModel">
        INSERT INTO v6_time_cnty_pts_utf_wgs84 (name_ch, geom)
        VALUES (#{nameCh},
                #{geometry, typeHandler=com.history.gismap.mybatis.GeometryTypeHandler})
    </insert>

    <update id="updateCntyPoint" parameterType="com.history.gismap.model.PointModel">
        UPDATE v6_time_cnty_pts_utf_wgs84
        SET name_ch = #{nameCh},
            geom = #{geometry, typeHandler=com.history.gismap.mybatis.GeometryTypeHandler}
        WHERE gid = #{gId}
    </update>

    <delete id="deleteCntyPoint">
        DELETE FROM v6_time_cnty_pts_utf_wgs84
        WHERE gid = #{gId}
    </delete>

</mapper>

关键点:

  • namespace 必须等于 MapDao 的全限定名

  • 每个 resultMap 中 geometry 字段要关联 GeometryTypeHandler

  • INSERT/UPDATE 中 geometry 字段也要指定 typeHandler 属性

  • 数据库表名 v6_time_cnty_pts_utf_wgs84 根据实际情况替换

4. 创建 Service 接口 MapService

路径src/main/java/com/history/gismap/service/MapService.java

package com.history.gismap.service;

import com.history.gismap.model.GeometryModel;
import com.history.gismap.model.PointModel;
import java.util.List;

public interface MapService {

    PointModel getCntyPoint(Integer gId);

    int insertCntyPoint(PointModel pointModel);

    int updateCntyPoint(PointModel pointModel);

    int deleteCntyPoint(Integer gId);

    List<GeometryModel> getDynastyGeom(String category);

    List<GeometryModel> getAllGeometry();

}

5 创建 Service 实现 MapServiceImpl

路径src/main/java/com/history/gismap/service/impl/MapServiceImpl.java

package com.history.gismap.service.impl;

import com.history.gismap.dao.MapDao;
import com.history.gismap.model.GeometryModel;
import com.history.gismap.model.PointModel;
import com.history.gismap.service.MapService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;

@Service
public class MapServiceImpl implements MapService {

    @Autowired
    private MapDao mapDao;

    @Override
    public PointModel getCntyPoint(Integer gId) {
        return mapDao.getCntyPoint(gId);
    }

    @Override
    public int insertCntyPoint(PointModel pointModel) {
        return mapDao.insertCntyPoint(pointModel);
    }

    @Override
    public int updateCntyPoint(PointModel pointModel) {
        return mapDao.updateCntyPoint(pointModel);
    }

    @Override
    public int deleteCntyPoint(Integer gId) {
        return mapDao.deleteCntyPoint(gId);
    }

    @Override
    public List<GeometryModel> getDynastyGeom(String category) {
        return mapDao.getDynastyGeom(category);
    }

    @Override
    public List<GeometryModel> getAllGeometry() {
        return mapDao.getAllGeometry();
    }

}

说明:@Service 注册为 Spring Bean,@Autowired 注入 MapDao(MyBatis 通过 @MapperScan 自动生成代理对象)。

6. 创建 Controller MapController

路径src/main/java/com/history/gismap/controller/MapController.java

package com.history.gismap.controller;

import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.history.gismap.model.GeometryModel;
import com.history.gismap.model.PointModel;
import com.history.gismap.service.MapService;
import com.history.gismap.service.impl.GeometryCache;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.geotools.geojson.geom.GeometryJSON;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.io.StringWriter;
import java.util.List;

@Slf4j
@Tag(name = "历史GIS接口")
@Controller
@RequestMapping("/history")
public class MapController {

    @Autowired
    private MapService mapService;

    @Autowired
    private GeometryCache geometryCache;

    @Operation(summary = "根据ID查询点位")
    @GetMapping("/pointmodel")
    @ResponseBody
    public JSONObject getPoint(@Parameter(description = "点位ID") @RequestParam("gid") Integer gid) {
        JSONObject result = new JSONObject();
        try {
            PointModel point = mapService.getCntyPoint(gid);
            if (point != null) {
                JSONObject data = new JSONObject();
                data.put("gId", point.getGId());
                data.put("nameCh", point.getNameCh());
                if (point.getGeometry() != null) {
                    GeometryJSON geometryJSON = new GeometryJSON();
                    StringWriter writer = new StringWriter();
                    geometryJSON.write(point.getGeometry(), writer);
                    data.put("geometry", JSONObject.parseObject(writer.toString()));
                }
                result.put("code", 200);
                result.put("data", data);
            } else {
                result.put("code", 404);
                result.put("message", "Point not found for gid=" + gid);
            }
        } catch (Exception e) {
            log.error("getPoint failed, gid={}", gid, e);
            result.put("code", 500);
            result.put("message", e.getMessage());
        }
        return result;
    }

    @Operation(summary = "根据地名查询GeoJSON")
    @GetMapping("/geometry")
    @ResponseBody
    @CrossOrigin
    public JSONObject getGeometry(@Parameter(description = "地名") @RequestParam String category) {
        JSONObject result = new JSONObject();
        try {
            List<GeometryModel> list = geometryCache.getByCategory(category);
            if (list == null) {
                log.warn("Cache miss for category={}, falling back to database", category);
                list = mapService.getDynastyGeom(category);
            }
            JSONArray jsonArray = new JSONArray();
            GeometryJSON geometryJSON = new GeometryJSON();
            for (GeometryModel model : list) {
                JSONObject item = new JSONObject();
                item.put("gId", model.getGId());
                item.put("namePy", model.getNamePy());
                item.put("nameCh", model.getNameCh());
                item.put("nameFt", model.getNameFt());
                item.put("presLoc", model.getPresLoc());
                item.put("typePy", model.getTypePy());
                item.put("typeCh", model.getTypeCh());
                item.put("levRank", model.getLevRank());
                item.put("begYr", model.getBegYr());
                item.put("begRule", model.getBegRule());
                item.put("endYr", model.getEndYr());
                item.put("endRule", model.getEndRule());
                item.put("geoSrc", model.getGeoSrc());
                item.put("compiler", model.getCompiler());
                item.put("gecomplr", model.getGecomplr());
                item.put("checker", model.getChecker());
                item.put("entDate", model.getEntDate());
                item.put("begChgTy", model.getBegChgTy());
                item.put("endChgTy", model.getEndChgTy());
                if (model.getGeometry() != null) {
                    StringWriter writer = new StringWriter();
                    geometryJSON.write(model.getGeometry(), writer);
                    item.put("geometry", JSONObject.parseObject(writer.toString()));
                }
                jsonArray.add(item);
            }
            result.put("number", list.size());
            result.put("list", jsonArray);
            log.info("getGeometry success, category={}, count={}", category, list.size());
        } catch (Exception e) {
            log.error("getGeometry failed, category={}", category, e);
            result.put("number", 0);
            result.put("list", new JSONArray());
            result.put("error", e.getMessage());
        }
        return result;
    }

    @Operation(summary = "新增点位")
    @PostMapping("/add")
    @ResponseBody
    public int addPoint(PointModel pointModel) {
        return mapService.insertCntyPoint(pointModel);
    }

    @Operation(summary = "修改点位")
    @PostMapping("/modify")
    @ResponseBody
    public int modifyPoint(PointModel pointModel) {
        return mapService.updateCntyPoint(pointModel);
    }

    @Operation(summary = "删除点位")
    @GetMapping("/remove")
    @ResponseBody
    public int removePoint(@Parameter(description = "点位ID") @RequestParam("gid") Integer gid) {
        return mapService.deleteCntyPoint(gid);
    }

}

注意 1:getPoint 方法中不能直接把 point 对象传给 FastJSON,因为 JTS 的 Geometry 类有 getEnvelope() 方法导致无限递归。必须手动构造 JSONObject,用 GeometryJSON 把 geometry 转为标准 GeoJSON 格式。注意 2:使用 @Tag@Operation@Parameter 注解可为 Knife4j/Swagger 页面提供中文描述,方便前端查看和调试。

7. 修改启动类 GismapApplication

路径src/main/java/com/history/gismap/GismapApplication.java

package com.history.gismap;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.history.gismap.dao")
public class GismapApplication {

    public static void main(String[] args) {
        SpringApplication.run(GismapApplication.class, args);
    }

}

8 .配置文件

路径src/main/resources/application.yml

server:
  port: 8080

spring:
  datasource:
    driver-class-name: org.postgresql.Driver
    url: jdbc:postgresql://127.0.0.1:5432/postgres
    username: postgres
    password: "0164523"
    druid:
      initial-size: 8
      min-idle: 5
      max-active: 10
      query-timeout: 6000
      transaction-query-timeout: 6000
      remove-abandoned-timeout: 1800
      filters: stat,config

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.history.gismap.model

logging:
  config: classpath:logback-boot.xml

注意 1:password 必须加引号,否则以 0 开头的数字会被 YAML 解析为八进制。注意 2:不需要手动指定 spring.datasource.typedruid-spring-boot-3-starter 会自动配置。

路径src/main/resources/logback-boot.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
        </encoder>
    </appender>
    <root level="INFO">
        <appender-ref ref="CONSOLE"/>
    </root>
</configuration>

二、编写 TypeHandler 处理 PostgreSQL geometry 字段

1. 创建 GeometryTypeHandle

路径src/main/java/com/history/gismap/mybatis/GeometryTypeHandler.java

package com.history.gismap.mybatis;

import org.apache.ibatis.type.BaseTypeHandler;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.MappedTypes;
import org.locationtech.jts.geom.Geometry;
import org.locationtech.jts.io.WKBReader;
import org.postgresql.util.PGobject;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;

@MappedTypes(Geometry.class)
public class GeometryTypeHandler extends BaseTypeHandler<Geometry> {

    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                                    Geometry parameter, JdbcType jdbcType) throws SQLException {
        PGobject pgObject = new PGobject();
        pgObject.setType("geometry");
        pgObject.setValue(parameter.toText());
        ps.setObject(i, pgObject);
    }

    @Override
    public Geometry getNullableResult(ResultSet rs, String columnName) throws SQLException {
        PGobject pgObject = (PGobject) rs.getObject(columnName);
        if (pgObject == null) return null;
        return convertToGeometry(pgObject.getValue());
    }

    @Override
    public Geometry getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
        PGobject pgObject = (PGobject) rs.getObject(columnIndex);
        if (pgObject == null) return null;
        return convertToGeometry(pgObject.getValue());
    }

    @Override
    public Geometry getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
        PGobject pgObject = (PGobject) cs.getObject(columnIndex);
        if (pgObject == null) return null;
        return convertToGeometry(pgObject.getValue());
    }

    private Geometry convertToGeometry(String hexWkb) throws SQLException {
        if (hexWkb == null || hexWkb.isEmpty()) return null;
        try {
            byte[] wkbBytes = WKBReader.hexToBytes(hexWkb);
            return new WKBReader().read(wkbBytes);
        } catch (Exception e) {
            throw new SQLException("Failed to parse WKB geometry: " + e.getMessage(), e);
        }
    }

}
核心转换逻辑
方向 方法 说明
Java → 数据库 setNonNullParameter PGobject 包装 Geometry 的 WKT 字符串,type 设为 "geometry"
数据库 → Java convertToGeometry PGobject 取出 hex WKB 字符串,用 WKBReader.hexToBytes 转 byte 数组,再 WKBReader.read 还原为 Geometry
在 Mapper XML 中使用

resultMap 中关联 TypeHandler:

<result property="geometry" column="geom"
        typeHandler="com.history.gismap.mybatis.GeometryTypeHandler"/>

三、将数据库查询结果转为 GeoJSON 格式返回

1. 添加 GeoTools 依赖

pom.xml 中添加:

<properties>
    <geotools.version>31.0</geotools.version>
</properties>

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

<repositories>
    <repository>
        <id>osgeo</id>
        <name>OSGeo Release Repository</name>
        <url>https://repo.osgeo.org/repository/release/</url>
        <snapshots><enabled>false</enabled></snapshots>
        <releases><enabled>true</enabled></releases>
    </repository>
</repositories>

完整 的pom.xml如下:

路径pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.5.14</version>
        <relativePath/>
    </parent>

    <groupId>com.history</groupId>
    <artifactId>gismap</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>gismap</name>

    <properties>
        <java.version>21</java.version>
        <geotools.version>31.0</geotools.version>
    </properties>

    <dependencies>
        <!-- Spring Boot -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!-- MyBatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.5</version>
        </dependency>

        <!-- PostgreSQL -->
        <dependency>
            <groupId>org.postgresql</groupId>
            <artifactId>postgresql</artifactId>
        </dependency>

        <!-- Druid 连接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-3-starter</artifactId>
            <version>1.2.25</version>
        </dependency>

        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

        <!-- JTS 空间数据处理 -->
        <dependency>
            <groupId>org.locationtech.jts</groupId>
            <artifactId>jts-core</artifactId>
            <version>1.20.0</version>
        </dependency>

        <!-- FastJSON -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>2.0.61</version>
        </dependency>

        <!-- GeoTools GeoJSON 输出 -->
        <dependency>
            <groupId>org.geotools</groupId>
            <artifactId>gt-geojson</artifactId>
            <version>${geotools.version}</version>
        </dependency>

        <!-- 分页插件 -->
        <dependency>
            <groupId>com.github.pagehelper</groupId>
            <artifactId>pagehelper-spring-boot-starter</artifactId>
            <version>2.1.1</version>
        </dependency>

        <!-- 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
        </dependency>

        <!-- DevTools -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <!-- 测试 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

        <!-- Knife4j API 文档(内置 Swagger) -->
        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-openapi3-jakarta-spring-boot-starter</artifactId>
            <version>4.5.0</version>
        </dependency>
    </dependencies>

    <repositories>
        <repository>
            <id>osgeo</id>
            <name>OSGeo Release Repository</name>
            <url>https://repo.osgeo.org/repository/release/</url>
            <snapshots><enabled>false</enabled></snapshots>
            <releases><enabled>true</enabled></releases>
        </repository>
    </repositories>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

2. 创建 GeometryModel

路径src/main/java/com/history/gismap/model/GeometryModel.java

package com.history.gismap.model;

import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import org.locationtech.jts.geom.Geometry;

@Getter
@Setter
@ToString
public class GeometryModel {

  private Integer gId;
  private String namePy;
  private String nameCh;
  private String nameFt;
  private String presLoc;
  private String typePy;
  private String typeCh;
  private String levRank;
  private Integer begYr;
  private String begRule;
  private Integer endYr;
  private String endRule;
  private String geoSrc;
  private String compiler;
  private String gecomplr;
  private String checker;
  private String entDate;
  private String begChgTy;
  private String endChgTy;
  private Geometry geometry;

}

3. GeoJSON 转换核心代码

在 Controller 的 getGeometry 方法中:

GeometryJSON geometryJSON = new GeometryJSON();
for (GeometryModel model : list) {
    JSONObject item = new JSONObject();
    item.put("gId", model.getGId());
    item.put("namePy", model.getNamePy());
    item.put("nameCh", model.getNameCh());
    item.put("nameFt", model.getNameFt());
    if (model.getGeometry() != null) {
        StringWriter writer = new StringWriter();
        geometryJSON.write(model.getGeometry(), writer);
        item.put("geometry", JSONObject.parseObject(writer.toString()));
    }
    jsonArray.add(item);
}

输出json文件演示:

{
  "number": 5,
  "list": [
    {
      "gId": 1,
      "namePy": "Baoding Fu",
      "nameCh": "保定府",
      "nameFt": "...",
      "geometry": {
        "type": "MultiPolygon",
        "coordinates": [[[[116.4, 39.9], ...]]]
      }
    }
  ]
}

四、启动时把部分表数据加载为本地缓存

1. 创建 GeometryCache

路径src/main/java/com/history/gismap/service/impl/GeometryCache.java

package com.history.gismap.service.impl;

import com.history.gismap.model.GeometryModel;
import com.history.gismap.service.MapService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Slf4j
@Service
public class GeometryCache implements InitializingBean {

  @Autowired
  private MapService mapService;

  private final Map<String, List<GeometryModel>> cacheMap = new ConcurrentHashMap<>();

  @Override
  public void afterPropertiesSet() {
    try {
      List<GeometryModel> allData = mapService.getAllGeometry();
      for (GeometryModel model : allData) {
        String key = model.getNameCh();
        if (key == null || key.isEmpty()) {
          key = "unknown";
        }
        cacheMap.computeIfAbsent(key, k -> new ArrayList<>()).add(model);
      }
      log.info("GeometryCache initialized: {} records, {} categories", allData.size(),
          cacheMap.size());
    } catch (Exception e) {
      log.warn("GeometryCache init failed, starting with empty cache: {}", e.getMessage());
    }
  }

  public List<GeometryModel> getByCategory(String category) {
    return cacheMap.get(category);
  }

}

关键设计:

特性 实现
启动时自动加载 实现 InitializingBean 接口,在 afterPropertiesSet() 中加载数据
线程安全 使用 ConcurrentHashMap 存储
按 category 字段分组 category 字段作为 key 分组存储
优雅降级 try-catch 包裹,加载失败时使用空缓存,不影响应用启动

缓存工作机制:

应用启动
  └─ Spring 调用 afterPropertiesSet()
       └─ mapService.getAllGeometry()  →  查全表
       └─ 按 category 字段分组 → ConcurrentHashMap<String, List<GeometryModel>>

API 请求
  └─ GET /history/geometry?category=保定府
       └─ geometryCache.getByCategory("保定府")  →  直接内存返回
       └─ 缓存未命中 → 回退查数据库(WHERE name_ch = #{category})

项目完整文件结构

gismap/
├── pom.xml
├── src/main/
│   ├── java/com/history/gismap/
│   │   ├── GismapApplication.java              # 启动类 @MapperScan
│   │   ├── controller/
│   │   │   └── MapController.java              # REST API 控制器
│   │   ├── dao/
│   │   │   └── MapDao.java                     # MyBatis Mapper 接口
│   │   ├── model/
│   │   │   ├── PointModel.java                 # 点数据模型
│   │   │   └── GeometryModel.java              # 几何数据模型(含字段)
│   │   ├── mybatis/
│   │   │   └── GeometryTypeHandler.java        # PostgreSQL geometry ↔ JTS 转换
│   │   └── service/
│   │       ├── MapService.java                 # 业务接口
│   │       └── impl/
│   │           ├── MapServiceImpl.java         # 业务实现
│   │           └── GeometryCache.java          # 启动缓存
│   └── resources/
│       ├── application.yml                     # 主配置
│       ├── logback-boot.xml                    # 日志配置
│       └── mapper/
│           └── HistoryGISMapper.xml            # SQL 映射文件

API 端点汇总

方法 路径 参数 说明 数据来源
GET /history/pointmodel gid=1 按 ID 查询单条记录,geometry 为 GeoJSON 格式 数据库
GET /history/geometry category=保定府 按地名查询 GeoJSON 数据 缓存(优先)→ 数据库(回退)
POST /history/add PointModel JSON 新增点位 数据库
POST /history/modify PointModel JSON 修改点位 数据库
GET /history/remove gid=1 删除点位 数据库

使用步骤:

  1. 启动项目:mvn spring-boot:run

  2. 浏览器打开 Knife4j API 文档页面:http://localhost:8080/doc.html

  3. 在文档页面中可看到所有接口的中文说明,点击 "Try it out" 直接测试

  4. 或者直接访问:

    • http://localhost:8080/history/pointmodel?gid=1 查询单条

    • http://localhost:8080/history/geometry?category=保德州 获取 GeoJSON

Knife4j 页面路径: http://localhost:8080/doc.html(国产增强版 Swagger UI)原生 Swagger 页面路径: http://localhost:8080/swagger-ui/index.html

接口文档

OK,今天就到这了,主要就是构建一下后端,下篇开始写前端,大家心急的可以先看大佬的教程,我们下篇见,拜拜!

该项目并非原创(侵权了私聊删QAQ),而是翻新一个知乎上的大佬的spring boot 2 + vue 2 的项目, 项目教程:知乎的才华横溢吴道简 大佬的教程 
 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐