一、前言
数据结构课设写一个校园导航图,正好舍友会安卓,于是我就用SpringBoot和他合作写了一个前后端交互的项目,由于时间之后3天,所以就没有加一些用户模块、权限模块,也就简单实现了景点信息、路径信息的增删改查。
二、所需技术
- SpringBoot搭建项目的整体框架
- 景点数据、路径数据采用MySQL数据库
- 查询数据添加缓存使用Redis数据库
- DAO层使用MyBatis框架
都是一些基本的开发框架,也没有涉及到分布式方面的东西,比较简单。
三、数据库构建
由于功能简单,数据库设计的也比较简单
spot景点表结构:
path表结构:
四、代码展示
1.相关bean类以及相应用法
有些bean类实现了Serializable接口,这是因为在springboot整合Redis时,需要加入缓存的对象序列化。
(由于代码较多,一些无关紧要的代码就会省略)
DealResult.java
用来封装返回的数据,里面记录了返回是否成功、返回信息、返回数据。
public class DealResult {
private boolean isSucceed;
private String resultInfo;
private Object data;
public DealResult() {}
public DealResult(boolean isSucceed, String resultInfo) {
this.isSucceed = isSucceed;
this.resultInfo = resultInfo;
}
}
Coord.java
用来封装一个景点的坐标类,可以将数据库查询到的路径中经过的景点坐标进行封装。
public class Coord implements Serializable {
private double x;
private double y;
public Coord() {
}
public Coord(double x, double y) {
this.x = x;
this.y = y;
}
Spot.java
景点类,用来封装景点信息
public class Spot implements Serializable {
// 景点编号
private Integer spotId;
// 景点名称
private String spotName;
// 景点横坐标
private double coordX;
// 景点纵坐标
private double coordY;
// 景点信息
private String spotInfo;
// 连接景点数组
private Coord[] coords;
public Spot() {}
}
Path.java
路径类,记录该条路径的起点、终点、长度信息
public class Path implements Serializable {
// 路径编号
private Integer pathId;
// 路径起点
private Integer startSpotId;
// 路径终点
private Integer endSpotId;
// 路径长度
private Double pathLength;
public Path() {}
public Path(Integer startSpotId, Integer endSpotId, Double pathLength) {
this.startSpotId = startSpotId;
this.endSpotId = endSpotId;
this.pathLength = pathLength;
}
}
2.springboot整合Redis配置类
在使用Redis时,需要注意的如果同类里面的方法需要调用另外一个方法的缓存,那么需要使用新的代理对象调用该方法才能实现数据的缓存,后面会遇到这种情况。
RedisConfig.java
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(60))
.disableCachingNullValues();
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.transactionAware()
.build();
}
@Bean
public RedisTemplate redisTemplate(RedisConnectionFactory factory) {
StringRedisTemplate template = new StringRedisTemplate(factory);
RedisSerializer keySerializer = new StringRedisSerializer(); // 设置key序列化类,否则key前面会多了一些乱码
template.setKeySerializer(keySerializer);
setValueSerializer(template);//设置value序列化
template.afterPropertiesSet();
template.setEnableTransactionSupport(true);
return template;
}
private void setValueSerializer(StringRedisTemplate template) {
@SuppressWarnings({"rawtypes", "unchecked"})
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
template.setValueSerializer(jackson2JsonRedisSerializer);
}
}
3.景点相关功能
- Controller层
SpotServlet.java
@RestController
public class SpotServlet {
@Autowired
SpotService spotService;
@GetMapping("/querySpotInfo")
public DealResult querySpotInfo() {
DealResult dealResult = new DealResult();
try {
List<Spot> spotList = spotService.querySpots();
if (spotList != null) {
dealResult.setSucceed(true);
dealResult.setData(spotList);
}
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("查询景点信息失败!");
}
return dealResult;
}
@GetMapping("/querySpot")
public Spot querySpotById(Integer id) {
return spotService.querySpotById(id);
}
}
- Service层
SpotServiceImpl.java
@Service
public class SpotServiceImpl implements SpotService {
@Autowired
SpotMapper spotMapper;
@Autowired
PathMapper pathMapper;
@Autowired
SpotService spotService;
/**
* 查询景点
* 将该景点为起点的路经的终点坐标封装为Coord存储到 coords[] 数组中
*/
@Cacheable(cacheNames = "spots")
@Override
public List<Spot> querySpots() {
List<Spot> spotList = spotMapper.querySpots();
for (Spot spot :
spotList) {
List<Path> nextSpotsList = pathMapper.queryPathsByStartId(spot.getSpotId());
spot.setCoords(nextSpotsList.size());
Coord[] coords = spot.getCoords();
int i = 0;
for (Path path :
nextSpotsList) {
Spot spot1 = spotService.querySpotById(path.getEndSpotId());
coords[i++] = new Coord(spot1.getCoordX(), spot1.getCoordY());
}
}
return spotList;
}
/**
* 根据编号查询景点
*/
@Cacheable(cacheNames = "spot")
@Override
public Spot querySpotById(Integer id) {
return spotMapper.querySpotById(id);
}
}
可以在方法querySpots()中看到需要调用下面的方法querySpotById(),那么就将本类对象注入,然后调用才能将查询结果加入缓存。
- Mapper层
因为没有复杂的SQL语句,就没有使用xml文件,使用了注解SQL
SpotMapper.java
@Mapper
@Component
public interface SpotMapper {
@Select("select * from spot where spotName=#{spotName}")
Spot querySpotByName(String spotName);
@Select("select * from spot")
List<Spot> querySpots();
@Select("select * from spot where spotId=#{spotId}")
Spot querySpotById(int spotId);
}
4.路径相关功能
在编写路径相关的逻辑代码之前我们需要实现路径查询中需要用到的数据结构和算法,这些东西可以查看我数据结构专栏的别的文章,里面都详细的讲到了需要用到的算法,这里就不再赘述。
这里就简单的贴一下Dijsktra算法的代码:
Dijsktra.java
public class Dijkstra {
// 图
EdgeWeightGraph graph;
// 标记是否访问
private boolean[] marked;
// 记录起点到每个顶点之间的权值
private double[] dis;
// 记录最短路径
private int[] path;
public Dijkstra(EdgeWeightGraph graph) {
this.graph = graph;
}
/**
* 初始化
* 更新起点到其他各点之间的距离
*/
private void init(int startId) {
this.marked = new boolean[graph.adjSize];
this.dis = new double[graph.adjSize];
this.path = new int[graph.adjSize];
path[startId] = startId;
dis[startId] = 0.0;
for (Edge edge = graph.adj[startId].firstEdge; edge != null; edge = edge.next) {
dis[edge.id] = edge.weight;
path[edge.id] = startId;
}
for (int i = 0; i < dis.length; i++) {
if (dis[i] == 0.0 && i != startId)
dis[i] = Double.MAX_VALUE;
}
}
/**
* 计算最短路径
*/
public List<Integer> dijkstraCal(int startId, int endId) {
init(startId);
PriorityQueue<Edge> queue = new PriorityQueue<>(new Comparator<Edge>() {
@Override
public int compare(Edge o1, Edge o2) {
Double d1 = new Double(o1.weight);
Double d2 = new Double(o2.weight);
return d1.compareTo(d2);
}
});
queue.add(new Edge(startId, 0.0, null));
while (!queue.isEmpty()) {
Edge now = queue.peek();
marked[now.id] = true;
queue.remove();
for (Edge edge = graph.adj[now.id].firstEdge; edge != null; edge = edge.next) {
Integer id = edge.id;
if (!marked[id]) {
if (dis[id] > dis[now.id] + edge.weight) {
dis[id] = dis[now.id] + edge.weight;
path[id] = now.id;
}
queue.add(edge);
}
}
}
return pathTo(startId, endId);
}
private List<Integer> pathTo(int x, int y) {
List<Integer> list = new ArrayList<>();
list.add(y);
int cur = y;
while (path[cur] != x) {
list.add(path[cur]);
cur = path[cur];
}
list.add(x);
Collections.reverse(list);
return list;
}
}
- Controller层
PathServlet.java
@RestController
public class PathServlet {
@Autowired
SpotService spotService;
@Autowired
PathService pathService;
/**
* 最佳布网方案(Prim)
* 最小生成树
*/
@PostMapping("/queryBestPlan")
public DealResult queryBestPlan(@RequestParam("startSpotName") String startSpotName) {
DealResult dealResult = new DealResult();
try {
List<Spot> pathList = pathService.queryBestPlan(startSpotName);
dealResult.setSucceed(true);
dealResult.setData(pathList);
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("查询失败!");
}
return dealResult;
}
/**
* 最少路径(BFS)
* 起点到终点路径中经过的景点数量最少
*/
@PostMapping("/queryLessPath")
public DealResult queryLessPath(@RequestParam("startSpotName") String startSpotName,
@RequestParam("endSpotName") String endSpotName) {
DealResult dealResult = new DealResult();
try {
List<Spot> pathList = pathService.queryLessPath(startSpotName, endSpotName);
dealResult.setSucceed(true);
dealResult.setData(pathList);
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("查询失败!");
}
return dealResult;
}
/**
* 简单路径(DFS)
* 起点到终点的所有路径
*/
@PostMapping("/querySimplePaths")
public DealResult querySimplePaths(@RequestParam("startSpotName") String startSpotName,
@RequestParam("endSpotName") String endSpotName) {
DealResult dealResult = new DealResult();
try {
List<List<Spot>> pathsList = pathService.querySimplePaths(startSpotName, endSpotName);
dealResult.setSucceed(true);
dealResult.setData(pathsList);
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("查询失败!");
}
return dealResult;
}
/**
* 查询最短路径(Dijkstra)
*/
@PostMapping("/queryMinPath")
public DealResult queryMinPath(@RequestParam("startSpotName") String startSpotName,
@RequestParam("endSpotName") String endSpotName) {
DealResult dealResult = new DealResult();
try {
List<Spot> spotList = pathService.queryMinPath(startSpotName, endSpotName);
dealResult.setData(spotList);
dealResult.setSucceed(true);
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("查询失败!");
}
return dealResult;
}
/**
* 增加路径
*/
@PostMapping("/addPath")
public DealResult addPath(@RequestParam("startSpotName") String startSpotName,
@RequestParam("endSpotName") String endSpotName) {
DealResult dealResult = new DealResult();
try {
boolean flag = pathService.addPath(startSpotName, endSpotName);
if (flag) {
dealResult.setSucceed(true);
dealResult.setData(flag);
dealResult.setResultInfo("添加路径成功!");
}
} catch (Exception e) {
dealResult.setSucceed(false);
dealResult.setResultInfo("添加路径失败!");
}
return dealResult;
}
}
- Service层
PathServiceImpl.java
这里面查询每种路径,使用的逻辑代码大同小异,没什么难度。
@Service
public class PathServiceImpl implements PathService {
@Autowired
SpotMapper spotMapper;
@Autowired
PathMapper pathMapper;
@Autowired
SpotService spotService;
@Autowired
PathService pathService;
/**
* 查询最短路径
* 1.读取数据库路径,创建地图信息(可以直接读取缓存)
* 2.调用Dijkstra算法计算最短路径,并返回最短路径景点编号集合
* 3.根据编号查询景点信息,并返回景点集合
*/
@Override
public List<Spot> queryMinPath(String startSpotName, String endSpotName) {
List<Spot> spots = spotService.querySpots();
List<Path> paths = pathService.getPaths();
EdgeWeightGraph graph = pathService.createGraph(spots, paths);
Dijkstra dijkstra = new Dijkstra(graph);
Integer startId = spotMapper.querySpotByName(startSpotName).getSpotId();
Integer endId = spotMapper.querySpotByName(endSpotName).getSpotId();
List<Spot> spotList = new ArrayList<>();
List<Integer> spotIdList = dijkstra.dijkstraCal(startId, endId);
for (int spotId : spotIdList
) {
spotList.add(spotService.querySpotById(spotId));
}
return spotList;
}
// 获取所有路径 - 添加缓存
@Override
@Cacheable(cacheNames = "paths")
public List<Path> getPaths() {
return pathMapper.queryPaths();
}
// 创建地图 - 添加缓存
@Override
@Cacheable(cacheNames = "graph")
public EdgeWeightGraph createGraph(List<Spot> spots, List<Path> paths) {
return new EdgeWeightGraph(spots, paths);
}
/**
* 添加路径
* 需要更新地图缓存
*/
@Override
@CacheEvict(cacheNames = {"graph", "paths"})
public boolean addPath(String startSpotName, String endSpotName) {
Spot startSpot = spotMapper.querySpotByName(startSpotName);
Spot endSpot = spotMapper.querySpotByName(endSpotName);
if (startSpot == null || endSpot == null)
return false;
Path path_ = pathMapper.queryPath(startSpot.getSpotId(), endSpot.getSpotId());
if (path_ != null)
return false;
Double startSpotCoordX = startSpot.getCoordX();
Double startSpotCoordY = startSpot.getCoordY();
Double endSpotCoordX = endSpot.getCoordX();
Double endSpotCoordY = endSpot.getCoordY();
Double pathLength = Math.sqrt(Math.pow(startSpotCoordX - endSpotCoordX, 2)
+ Math.pow(startSpotCoordY - endSpotCoordY, 2));
Path path = new Path(startSpot.getSpotId(), endSpot.getSpotId(), pathLength);
pathMapper.addPath(path);
return true;
}
/**
* 查询简单路径(DFS)
* 使用DFS查询简单路径
*/
@Override
public List<List<Spot>> querySimplePaths(String startSpotName, String endSpotName) {
List<Spot> spots = spotService.querySpots();
List<Path> paths = pathService.getPaths();
EdgeWeightGraph graph = pathService.createGraph(spots, paths);
Integer startId = spotMapper.querySpotByName(startSpotName).getSpotId();
Integer endId = spotMapper.querySpotByName(endSpotName).getSpotId();
List<List<Integer>> pathsList = new DFS(graph).dfsPathsCal(startId, endId);
List<List<Spot>> lists = new ArrayList<>();
for (List<Integer> list1 :
pathsList) {
List<Spot> spotList = new ArrayList<>();
for (Integer id :
list1) {
spotList.add(spotService.querySpotById(id));
}
lists.add(spotList);
}
return lists;
}
/**
* 转机最少路径(BFS)
* 路径中经过景点个数最少
*/
@Override
public List<Spot> queryLessPath(String startSpotName, String endSpotName) {
List<Spot> spots = spotService.querySpots();
List<Path> paths = pathService.getPaths();
EdgeWeightGraph graph = pathService.createGraph(spots, paths);
Integer startId = spotMapper.querySpotByName(startSpotName).getSpotId();
Integer endId = spotMapper.querySpotByName(endSpotName).getSpotId();
List<Integer> list = new BFS(graph).bfsPathCal(startId, endId);
List<Spot> spotList = new ArrayList<>();
for (int spotId : list
) {
spotList.add(spotService.querySpotById(spotId));
}
return spotList;
}
/**
* 最佳布网方案(Prim)
* 最小生成树
*/
@Override
public List<Spot> queryBestPlan(String startSpotName) {
List<Spot> spots = spotService.querySpots();
List<Path> paths = pathService.getPaths();
EdgeWeightGraph graph = pathService.createGraph(spots, paths);
Integer startId = spotMapper.querySpotByName(startSpotName).getSpotId();
List<Integer> list = new Prim(graph).primCal(startId);
List<Spot> spotList = new ArrayList<>();
for (int spotId : list
) {
spotList.add(spotService.querySpotById(spotId));
}
return spotList;
}
}
- Mapper层
PathMapper.java
@Mapper
@Component
public interface PathMapper {
@Insert("insert into path(startSpotId, endSpotId, pathLength) values(#{startSpotId}, #{endSpotId}, #{pathLength})")
void addPath(Path path);
@Select("select * from path where startSpotId=#{startSpotId} and endSpotId=#{endSpotId}")
Path queryPath(Integer startSpotId, Integer endSpotId);
@Select("select * from path")
List<Path> queryPaths();
@Select("select * from path where startSpotId=#{spotId}")
List<Path> queryPathsByStartId(Integer spotId);
}
5.application.yml
这里配置了springboot的全局配置,里面主要是一些数据库的配置、以及热部署等。
spring:
datasource:
# 数据源的基本配置
username: root
password: root
url: jdbc:mysql://localhost:3306/schoolmap
driver-class-name: com.mysql.cj.jdbc.Driver
# 使用druid连接池
type: com.alibaba.druid.pool.DruidDataSource
# initialSize: 5
# minIdle: 5
# maxActive: 20
# maxWait: 60000
# timeBetweenEvictionRunsMills: 60000
# validationQuery: selelct 1 from dual
# testWhileIdle: true
# testOnBorrow: false
# testOnReturn: false
# poolPreparedStatements: true
## 配置监控统计拦截的filters,去掉后监控界面sql无法统计,‘wall’用于防火墙
# filters: stat,wall,log4j
# maxPoolPreparedStatementPerConnectionSize: 20
# useGlobalDataSourceStat: true
# connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
# 程序启动时会执行的sql脚本
# schema:
# classpath: department.sql
# springboot2.x需要加的配置
initialization-mode: always
# 配置redis配置
redis:
#数据库索引
database: 0
host: 127.0.0.1
port: 6379
password:
jedis:
pool:
#最大连接数
max-active: 8
#最大阻塞等待时间(负数表示没限制)
max-wait: -1
#最大空闲
max-idle: 8
#最小空闲
min-idle: 0
#连接超时时间
timeout: 10000
devtools:
restart:
enabled: true # 设置开启热部署
freemarker:
cache: false # 页面不加载缓存,修改即时生效
debug: true
6.pom中引入的依赖
<dependencies>
<!-- spring2.0集成redis所需common-pool2 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.10</version>
</dependency>
<!-- 缓存依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- redis依赖,2.0以上使用这个依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 热部署 -->
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>-->
<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>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
五、总结
这次的项目比较仓促,很多东西都没有考虑到,也算是学完springboot之后的一个小Demo吧,但不能止步于此,对于后台开发,如何做到分布式,让软件高并发、高可用才是重点,这些也仅仅只是边缘工作。