记录我的webgis项目(3):从零开始,构建电子地图网站 spring boot3 +vue3 + leaflet之后端搭建(2)
(依旧叠甲,我也是菜鸟,悉知我的功力还很浅薄,如果有哪里做的不行欢迎批评指正,大部分代码都是借助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.type,druid-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 |
删除点位 | 数据库 |
使用步骤:
-
启动项目:
mvn spring-boot:run -
浏览器打开 Knife4j API 文档页面:http://localhost:8080/doc.html
-
在文档页面中可看到所有接口的中文说明,点击 "Try it out" 直接测试
-
或者直接访问:
-
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 的项目, 项目教程:知乎的才华横溢吴道简 大佬的教程
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)