☰
SpringBoot+Vue前后端分离图书管理系统实战:从数据库到部署全记录
2026/10/5 7:10:18 网站建设 项目流程

前后端分离的练手项目,我见过太多人选图书管理系统了。原因很简单:业务够经典,模块够完整,但又不像电商、秒杀系统那样涉及复杂的分布式问题,非常适合拿来完整走一遍“SpringBoot+Vue+MyBatis+MySQL”这套前后端分离技术栈。最近我又完整做了一遍这个项目,从数据库设计、后端接口、前端页面一路做到打包部署上线,过程中踩了不少坑,也总结了一些比源码本身更值钱的经验,这里完整分享出来,希望能给正在做毕设、或者准备上手前后端分离实站的朋友一点参考。

这个项目说到底是解决一个很实际的问题:图书馆里的书怎么管、借出去的书怎么跟踪、用户数据怎么维护。用前后端分离的方式来做,就是把“管理员操作的界面”和“后端数据服务的逻辑”彻底拆开,前端只负责交互展示,后端只负责业务处理和存储。SpringBoot承担后端接口,Vue负责前端页面,MyBatis处理数据库访问,MySQL负责数据落地。这套组合的好处是每个环节都有非常成熟的生态,网上能找到极其丰富的问题答案,对新手极其友好。

1. 项目整体设计:从需求到技术选型的拆解

1.1 图书管理系统到底要管哪些事

开始动手之前,先把业务边界划清楚。一个标准的前后端分离图书管理系统,核心模块至少包含这些:用户登录与权限管理、图书分类管理、图书信息维护、图书借阅与归还、借阅记录查询、图书统计。听起来不多,但每一个模块展开来都足够写一篇完整的博文。

举个例子,图书借阅这个动作,表面上就是“点一下借书按钮”,实际上后端要处理的事情包括:检查图书是否存在、检查库存是否大于0、检查该用户是否有未归还的借阅记录、生成一条借阅记录、扣减图书库存。归还的时候要做对称操作:更新借阅记录的归还时间、增加库存。这些业务逻辑如果全部堆在Controller里,代码会变得非常难看。所以在设计阶段就得把数据表和接口规划好,后面coding才能顺畅。

我的建议是先在纸上画一遍流程图,把“谁在什么条件下对什么数据做什么操作”理清楚。千万别上来就写代码,前后端分离项目最怕的就是前端写完了发现后端接口对不上,后端做完了发现前端要的数据结构不匹配。

1.2 为什么偏偏是SpringBoot+Vue这套组合

选择技术栈不是越新越好,而是越稳妥越好。SpringBoot之所以成为后端主流,是因为它把Spring家族繁杂的XML配置彻底简化了,内嵌Tomcat,一个jar包就能跑起来。Vue作为前端框架,核心优势是组件化和响应式,一个图书列表页拆成表格组件、搜索组件、分页组件,每个组件都能独立维护。MyBatis在持久层里讲究“SQL可控”,图书管理的很多场景都有多表关联查询,MyBatis可以写自己完全掌控的SQL,而不需要像JPA那样通过方法名猜语义。

做个简单对比可能更直观:

技术组件核心优势在本项目中承担的角色
SpringBoot配置简洁、生态成熟、内嵌服务器提供RESTful接口,处理业务逻辑
Vue组件化开发、数据双向绑定管理端页面渲染与用户交互
MyBatisSQL灵活可控、支持动态SQL数据持久化操作,多表联查
MySQL免费稳定、使用广泛图书、用户、借阅记录等数据存储

另外提一嘴若依框架,很多人搜“若依框架前后端分离”,那是因为若依把权限体系、代码生成、部门管理都内置好了,开箱即用。但如果你是为了学习前后端分离的原理,我还是建议从零搭建一遍。自己搭一遍之后,你会对“接口-服务-数据库”这条链路有更深的体感,跳过了这个过程,就算用了若依,出了问题也不知道从哪里排查。

1.3 数据库表设计与初始化数据

我设计数据库时遵循一个原则:能拆的表尽量拆,能加约束的地方不要省。这个项目我用了四张核心表:管理员用户表(user)、图书分类表(category)、图书信息表(book)、借阅记录表(borrow_record)。

管理员用户表很简单,字段包括id、username、password、create_time。密码一定不能存明文,我用的是BCrypt加密,后面鉴权部分细说。图书分类表也就id、name两个核心字段。图书信息表稍微复杂一点,包含isbn、书名、作者、出版社、分类id、库存数量、总库存、简介、图片地址、上架状态。这里有个小细节:isbn字段建议设置为唯一索引,同一个ISBN号的图书在系统中视为同一本书,在入库时如果重复录入,可以直接更新库存数量。

借阅记录表是关联关系的核心,字段包括id、user_id(关联用户表)、book_id(关联图书表)、borrow_time、return_time、status。status字段我用0表示借出中,1表示已归还。为什么不用直接删除记录呢?因为借阅历史是有统计价值的,后面做“最热图书”“读者借阅排行”都要靠这些历史记录。

建表SQL我这里给出核心部分:

CREATE DATABASE IF NOT EXISTS library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE library; CREATE TABLE `user` ( `id` int(11) NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL COMMENT '登录名', `password` varchar(100) NOT NULL COMMENT 'BCrypt加密后的密码', `real_name` varchar(50) DEFAULT NULL COMMENT '真实姓名', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; CREATE TABLE `category` ( `id` int(11) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '分类名称', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; CREATE TABLE `book` ( `id` int(11) NOT NULL AUTO_INCREMENT, `isbn` varchar(20) NOT NULL COMMENT 'ISBN编号', `name` varchar(200) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL, `publisher` varchar(100) DEFAULT NULL, `category_id` int(11) DEFAULT NULL, `stock` int(11) DEFAULT '0' COMMENT '当前库存', `total_stock` int(11) DEFAULT '0' COMMENT '总库存', `cover_image` varchar(255) DEFAULT NULL COMMENT '封面图URL', `status` tinyint(1) DEFAULT '1' COMMENT '1上架 0下架', PRIMARY KEY (`id`), UNIQUE KEY `uk_isbn` (`isbn`), KEY `idx_category_id` (`category_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4; CREATE TABLE `borrow_record` ( `id` int(11) NOT NULL AUTO_INCREMENT, `user_id` int(11) NOT NULL, `book_id` int(11) NOT NULL, `borrow_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '借书时间', `return_time` datetime DEFAULT NULL COMMENT '还书时间', `status` tinyint(1) DEFAULT '0' COMMENT '0借出中 1已归还', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_book_id` (`book_id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4;

注意一个细节:所有表的字符集我都用了utf8mb4而不是utf8,因为utf8mb4才是真正的四字节编码,能完整支持生僻字和特殊字符。图书管理里可能会出现“𬱖”这类生僻字作者名,用utf8可能直接入库失败。

2. 后端实现:SpringBoot整合MyBatis的完整链路

2.1 版本选型与Maven依赖配置

后端搭建第一步是选版本。这里我想重点强调一个最近很多人踩的问题:springboot版本太高。Spring Boot 3.x发布之后,确实很吸引人,但随之而来的是JDK最低要求17、javax包全部迁移到jakarta、大量的starter也要对应升级。对于图书管理系统这种项目,完全没必要追求最新,我在实操中统一使用Spring Boot 2.7.x + JDK 8/11的组合,稳定、资料多、兼容性好。

Maven依赖这里我贴出pom中核心的部分,注意mybatis和mysql驱动的版本要跟Spring Boot版本匹配。如果不小心配了Spring Boot 3.x,下面这段依赖里的javax.sql相关内容就要改成jakarta,会多出不少工作量:

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <dependencies> <!-- Web 支持 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis 整合 SpringBoot 的官方starter --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <!-- BCrypt 密码加密 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-crypto</artifactId> </dependency> <!-- JWT 工具 --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <!-- Lombok 简化实体类,注意IDEA要装插件 --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

Maven的构建方式这里多说一句,很多新手在IDEA里搞了半天项目起不来,问题大多出在Maven没有配置好。IDEA里设置Maven的User settings file指向你自己下载的settings.xml,把本地仓库路径也配置一下。项目下载依赖的时候,优先用阿里云镜像仓库,不然某些依赖可能拉取极其缓慢甚至失败。

2.2 application.yml配置文件里的“坑”

配置文件是整个项目最容易装神弄鬼的地方,我见过不少项目后端本身代码没问题,纯粹因为配置不对起不来。图书管理系统这份application.yml,我建议按下面的基础结构来加自己的内容:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 你的数据库密码 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.library.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl logging: level: com.example.library.mapper: debug

这里有三个关键点值得展开讲一下。第一,url里的useSSL=false和allowPublicKeyRetrieval=true,前者解决MySQL 8.0的SSL连接错误,后者解决MySQL 8.0的Public Key Retrieval报错。这两行是无数人遇到“MySQL ssl连接错误”“Public Key Retrieval is not allowed”之后搜出来的解决方案,建议直接写进去。第二,serverTimezone=Asia/Shanghai不能省,否则日期类型的字段会出现八小时时差。第三,map-underscore-to-camel-case: true一开,数据库字段user_id就能自动映射为Java实体里的userId,少写一堆resultMap映射,但复杂SQL我仍然建议用显式的resultMap,后面会讲。

关于“mybatis配置打印SQL”,就是log-impl配置成StdOutImpl。在控制台就能看到每次请求执行的完整SQL和参数值。这个配置在联调阶段非常关键,前端说数据不对,你把后端的SQL日志一贴出来,问题在哪儿一目了然。

2.3 实体类、Mapper接口与XML映射

后端代码的分层是我比较在意的部分。这个项目我用的是经典的五层结构:Controller接收请求、Service写业务逻辑、Mapper接口定义数据方法、XML文件写SQL、Entity载体数据。另外加一个common包放统一返回结果、异常处理、JWT工具等。

实体类我以Book为例,用Lombok简化getter/setter,代码量大大减少:

@Data public class Book { private Integer id; private String isbn; private String name; private String author; private String publisher; private Integer categoryId; private Integer stock; private Integer totalStock; private String coverImage; private Boolean status; }

Mapper接口和XML的配合是MyBatis的核心玩法。接口只定义方法签名和注解参数,真正执行的SQL全部写在XML文件里:

@Mapper public interface BookMapper { List<Book> selectBookList(BookQuery query); Book selectBookById(Integer id); int insertBook(Book book); int updateBook(Book book); int deleteBook(Integer id); int updateStock(@Param("bookId") Integer bookId, @Param("amount") Integer amount); }

对应的XML文件里,图书列表查询我用的是动态SQL。为什么?因为列表页的搜索条件是可变的:用户可能只按书名搜,可能只按分类筛,也可能两者一起上。每个条件都写一条SQL会疯掉,用动态SQL组合条件才是正解:

<select id="selectBookList" resultType="com.example.library.entity.Book"> SELECT * FROM book <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> <if test="status != null"> AND status = #{status} </if> </where> ORDER BY id DESC </select>

有人问过我,数据量大了以后这里要不要加索引。关于分页,这个项目数据量不大,用limit做手动分页,或者PageHelper分页插件都可以。如果笔试面试考到MyBatis动态SQL,上面这个if+where的写法就是标准答案。

我还想特别提一下typehandler。热词里有“mybatis中typehandler的工作流程图”,这在MyBatis源码面试题里经常出现。简单理解,typehandler就是负责Java类型和JDBC类型之间转换的适配器,默认已经处理了String、Integer、Date这些常见类型。图书分类的id、图书的ISBN其实都用默认就够,不建议在这个项目里自定义typehandler。但你要知道有这个机制,面试官问起来的时候至少能说上来。

2.4 登录鉴权与拦截器

图书管理系统的后台必须要有登录功能。我的实现方案很标准:登录接口校验用户名密码后签发一个JWT,前端把token存到localStorage,之后每次请求都在请求头里带上Authorization字段,后端通过拦截器校验token是否合法。

JWT工具类的核心代码不算复杂,网上有大量模板,但有几个关键点:签名密钥要足够复杂,不要把密钥写死在代码里,而是放到配置文件里;过期时间设定为24小时比较合理,太短用户体验差,太长有安全风险。密码存储用BCrypt加密,就是之前引入spring-security-crypto的原因。BCrypt每次加密的盐都不同,所以数据库中存储的密文即使两个用户明文密码相同,密文也不同。

拦截器实现上需要注意放行路径的配置。登录接口本身、静态资源路径、以及前端的入口路径default.html都要放行,其他接口全部拦截,未携带token或者token过期,统一返回401状态码和提示信息。这里如果配置不对,最容易出现的现象是前端页面能打开,但所有数据请求都报“未授权”。遇到这个问题,先检查拦截器的排除路径有没有写对。

统一返回格式也很有必要。我定义了一个Result类,包含code、message、data三个字段。成功返回Result.success(data),失败返回Result.error()。这样前端axios响应拦截器里只需要判断code是不是200,如果是再取data,不是则弹出错误提示。这个设计虽然简单,但能省掉前端大量重复的错误处理代码。

3. 前端Vue实现:从环境搭建到页面联调

3.1 Vue开发环境安装与项目初始化

前端这一侧第一道坎就是环境配置。很多新手上来就安装最新版Node.js,然后发现跟一些老项目冲突。我这次使用的是Node.js 16 LTS,配合Vite 4.0创建Vue 3项目。这里说明一下,Vue有2和3两个大版本,新项目我强烈建议直接用Vue 3 + Vite,如果是毕设或者参考老教程,也可以用Vue 2 + Vue CLI,但Vue 2生态已经进入维护尾声,新写的代码不建议再迁就它了。

环境安装的完整流程如下:

# 1. 检查node版本,建议使用16.x或18.x node -v # 2. 使用Vite初始化Vue3项目 npm create vite@latest library-ui -- --template vue # 3. 进入项目目录并安装依赖 cd library-ui npm install # 4. 安装路由、状态管理、网络请求库和UI组件 npm install vue-router@4 axios element-plus # 5. 启动开发服务器 npm run dev

初始化的项目会在http://localhost:5173启动,这就带来一个CORS跨域问题:前端跑在5173端口,后端跑在8080端口,两个源不同,直接请求会被浏览器拦下来。解决方案之一是在Vite的配置文件中配置代理,把/api路径的请求转发到后端:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

这样前端请求中只要统一发起/api/xxx请求,浏览器实际访问的是5173端口的/api/xxx,Vite再把请求转发到8080端口,绕开了跨域问题。

3.2 前后端接口规范与axios封装

前后端联调最容易出现的问题就是“前端接口调通了,后端却拿不到数据”。原因大多集中在参数传递格式不一致上。我做这个项目的时候,前后端约定:查询参数一律放在query里,提交数据一律采用JSON格式放在request body里。这个约定在axios封装中直接落实。

我封装了一个request.js,统一处理请求前缀、token携带和响应拦截:

import axios from 'axios' import { ElMessage } from 'element-plus' // 创建axios实例 const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:每次请求自动携带token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理后端返回结构和错误码 request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') window.location.href = '/login' } return Promise.reject(new Error(res.message)) } return res.data }, error => { ElMessage.error('网络异常,请稍后重试') return Promise.reject(error) } ) export default request

注意事项:响应拦截器里我把res.data直接返回了,也就是说在业务代码里调用getBookList()拿到的直接就是data字段的内容,不用再层层解包。

3.3 路由守卫与页面结构

前端路由设计,我用的是嵌套路由配合动态组件的方式。最外层是布局组件Layout,包含侧边栏和顶部栏,内部再嵌入每个业务页面。登录页和404页面不放在Layout里面,是独立页面。vue-router@4的写法与Vue2时略有不同,尤其是createRouter/createWebHistory这些API的引入方式。

路由守卫是登录功能的最后一环。页面刷新的时候,前端不能直接认为用户已经登录,需要校验localStorage里是否存在token。如果不存在,一律重定向到/login页面。但这里有个小坑:直接刷新某个深链接时,前端并不知道用户是否登录,如果token存在,普通校验即可通过,可如果后端登录态过期了,后端接口返回401,此时响应拦截器会删除本地token并跳回登录页。整体闭环是OK的。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.path === '/login') { next() } else { if (token) { next() } else { next('/login') } } })

3.4 图书管理核心页面实现

图书列表页是整个前端最典型的CRUD页面。我用Element Plus的el-table展示列表数据,el-pagination实现分页,el-dialog嵌套el-form实现新增和编辑。

这里有一个值得说的细节:表格里展示图书分类名称而不是分类ID。后端返回的数据里是categoryId,而前端期望展示的是“文学”“科技”这样的名称。解决办法有几种:第一种是后端SQLjoin联查,把categoryName一起查出来;第二种是前端拿到全部分类后做映射。在我的实际项目中,图书列表接口直接做了表关联查询,返回数据里自带categoryName。这就是为什么当初设计数据库时category_id字段单独拆一张表,联查时方便得多。

借书和还书按钮放在表格的“操作”列里,点击借书时调用后端接口,成功后刷新当前页数据即可。这些交互如果拆开来讲每一块都能展开不少内容,但在项目里它们都是标准的弹窗和接口调用,属于“写熟了就快”的部分。

4. 部署发布:后端打包与前端的两种集成方式

4.1 后端Maven打包与运行参数

项目开发完成后,第一步是后端打包。在IDEA右侧Maven面板中执行clean,然后双击package。如果你想在命令行操作,可以这样:

mvn clean package -DskipTests

打包成功后,target目录下会生成一个library-backend-1.0.0.jar。这里有一个细节:Spring Boot的Maven插件会生成一个可执行jar和原有的jar,可执行jar通过java -jar命令直接运行,这是SpringBoot内嵌Tomcat带来的便利。以前SSM项目部署要单独装Tomcat,把war包丢进webapps目录,现在一个jar全搞定。

启动后端的命令,我习惯写成这样:

java -jar library-backend-1.0.0.jar --server.port=8080

如果你在服务器上,就需要配合nohup命令在后台运行:

nohup java -jar library-backend-1.0.0.jar > app.log 2>&1 &

日志输出重定向到app.log文件,方便随时查看运行状态。

4.2 前端打包后如何整合进SpringBoot

前端开发模式下是通过Vite dev server跑在5173端口,但到了线上,你不能指望服务器再单独跑一个Node进程前端服务。生产环境下,最省事的方式是执行前端打包,把dist产物复制到SpringBoot的静态资源目录下,与后端一起发布。SpringBoot会优先从classpath下的META-INF/resources、resources、static、public这几个目录寻找静态资源。

具体操作过程:

# 第一步:前端构建生产包 npm run build # 第二步:把dist目录的内容复制到后端项目的static目录 cp -r dist/* ../library-backend/src/main/resources/static/ # 第三步:重新打包后端项目 mvn clean package -DskipTests

整合后的jar包自带前端静态页面,访问http://服务器IP:8080/就能直接打开管理系统,不用再启动任何额外服务。这个方案对图书管理系统这种中小型应用来说,几乎是性价比最高的部署方式。前端打包对应的就是热词里“vue打包放进springboot中”,这件事做熟练了之后,你会在nginx配置一类问题上省下大量时间。

注意:如果前端使用了history路由模式,比如访问/book-list这个前端路由,直接刷新页面时SpringBoot会返回404,因为它找不到对应的静态文件。解决办法有多种,一是把registerWebServerComponents的静态资源映射调整,另一种更简单的做法是在后端写一个控制器,把非/api开头的请求都转发到index.html。后面第5节会详细说。

4.3 服务器MySQL初始化

服务器上必须有一个数据库实例。我用的MySQL 8.0,初始化过程其实不难,但有几个地方容易踩坑。安装完成后,用mysql_secure_installation做一个基础的安全加固,设置root密码、禁用匿名用户、删除test库。接着创建图书管理系统的专用数据库和用户:

CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; CREATE USER 'lib_user'@'%' IDENTIFIED BY '你的密码'; GRANT ALL PRIVILEGES ON library.* TO 'lib_user'@'%'; FLUSH PRIVILEGES;

我建议不要直接用root账号跑业务。用专用账号的好处是权限可控,如果应用被注入或信息泄露,攻击者拿到的也不是数据库最高权限。导入表结构时,把本地导出的library.sql上传到服务器执行即可。

MySQL 5.7和8.0的安装过程细节上有差异,特别是8.0默认的认证插件是caching_sha2_password,如果你的JDBC驱动版本太老,会出现认证方式不兼容的问题。所以驱动一定要用8.0.33这个较新的版本。

4.4 防火墙、端口与进程管理

服务器上有一个经验:打不开页面时,90%的情况不是程序问题,而是防火墙没放行端口。CentOS系的服务器用firewalld,Ubuntu系用ufw。8080端口需要通过防火墙:

# CentOS / AlmaLinux sudo firewall-cmd --permanent --add-port=8080/tcp sudo firewall-cmd --reload # Ubuntu sudo ufw allow 8080/tcp

有些云服务器厂商还在安全组层面做了网络隔离,单改服务器防火墙还不够,还要去云控制台放行8080端口。这个坑我踩过多次,每次换新服务器都要检查一遍。

进程管理的几个常用操作,附上我一直在用的命令:

# 查看jar进程是否在运行 ps -ef | grep library-backend # 杀掉旧进程(PID根据上面查询结果替换) kill -9 PID # 方便起见,把启动命令写成一个脚本 start.sh nohup java -jar library-backend-1.0.0.jar > app.log 2>&1 & echo $! > app.pid

这个start.sh脚本里echo $!把进程号写入app.pid,后面要停服务时,直接cat app.pid就能找到进程号,不用再去ps里翻了。

5. 常见问题与排查技巧实录

5.1 MySQL连接报错排查

我把这次部署过程中切切实实遇到的报错整理成了一张速查表,基本都是高频问题:

报错信息原因解决方案
Public Key Retrieval is not allowedMySQL 8.0的caching_sha2_password认证机制JDBC url中加allowPublicKeyRetrieval=true
SSL connection error / SSL 握手失败客户端与服务器SSL协议协商失败JDBC url中加useSSL=false,或配置SSL证书
Access denied for user用户密码错误或权限不足GRANT语句重新授权,注意账号的host匹配
The server time zone value 'XXX' is unrecognized时区配置缺失url中加serverTimezone=Asia/Shanghai
Communication link failure数据库连接被断开或防火墙拦截检查云安全组和服务器防火墙是否放行3306端口

关于MySQL的SSL问题再多说一句。本地开发环境mysql-connector-java 8.0版本默认会尝试建立SSL连接,如果你的MySQL服务器没有相应配置,就会报错。开发环境直接useSSL=false是最快的解决办法,生产环境如果要安全通信,正确做法是配置SSL证书而不是关掉SSL。但对于图书管理系统这种内网应用,关闭SSL不影响实际使用。

5.2 SpringBoot版本过高引发的兼容性问题

热词里有一条“springboot版本太高”,这确实是有道理的。Spring Boot 3.x和2.x之间不只是大版本号变了,底层有几处非常硬核的改动。最直观的是javax到jakarta的迁移,你原本的代码里import javax.servlet.,到3.x全部要改成jakarta.servlet.。如果项目里写了一些传统的Servlet Filter或者拦截器,这一波改动会波及很多文件。

MyBatis官方starter也一样,mybatis-spring-boot-starter在SpringBoot 3.x下需要新版本,配合的mybatis-spring版本也要对应升级。数据库驱动mysql-connector-java在SpringBoot 3.x中默认变成com.mysql:mysql-connector-j这个新坐标,如果沿用了旧坐标,会有依赖冲突。

我的建议:如果只是用图书管理系统来实践或者做毕设,老老实实停在SpringBoot 2.7.x。所有资料、教程、视频里给的代码你都直接能用,不用花额外时间做迁移。如果是面试中聊到SpringBoot版本选型,你也可以直接回答“稳定优先,宁可保守,不要激进”。

5.3 MyBatis常见“坑”:缓存、日志与多参数

MyBatis的缓存机制是面试里被问烂了的点,也是实际开发中最容易造成困惑的地方。一级缓存是SqlSession级别的,默认开启。二级缓存是namespace级别的,默认关闭。在图书管理系统中,如果开启二级缓存,并且对图书表做了更新操作,旧缓存不会自动失效,就可能出现“数据库已经改了,但查询结果还是老数据”的情况。

避免这个坑的方法是:第一,默认不要开启二级缓存,MyBatis默认就是关闭的;第二,如果你确实要开启二级缓存,那么增删改操作所在的mapper里一定要配置flushCache=true。这个经验在实际项目中比理论知识值钱得多,很多人一开始都会被缓存问题搞得云里雾里,其实关掉是最省心的。

SQL日志打印这块在2.2已经提到过。再补充一个细节点:控制台打印出来的Preparing和Parameters两行,前者是最终SQL骨架,后者是参数值。排查“为什么查出来是空”这类问题时,把SQL放到数据库客户端手动执行,就能区分是SQL本身写错了,还是传参传错了。

5.4 前端显示PDF与路由刷新404

热词里有“vue image能显示pdf吗”,这个我在项目中其实遇到过。图书详情、论文附件这类场景,前端最常见的错误是用img标签去加载一个PDF文件。img标签确实能打开部分浏览器内置的PDF插件,但兼容性不可控,尤其在Chrome和Edge上有差异。

我的建议是:图片用img标签显示,PDF文件不要硬塞给img标签,而是提供一个按钮或链接,通过点击触发window.open(url)打开新页面展示PDF。如果要在页面内嵌预览,用iframe或embed标签更为靠谱。具体来说:

<!-- 图片正常显示用img --> <img :src="book.coverImage" alt="封面" /> <!-- PDF展示不再用img,而是用iframe或window.open --> <iframe :src="pdfUrl" style="width:100%;height:600px"></iframe>

另一个高频问题是路由刷新404。当前端使用history模式时,浏览器访问/books这个路由,请求会被发送到服务器,但SpringBoot并不知道这个路径是什么,就返回404。解决办法是在后端加一个转发规则,把非API的请求全部转发到index.html:

@Controller public class PageForwardController { @RequestMapping(value = {"/", "/books", "/borrow", "/users", "/login", "/404"}) public String forward() { return "forward:/index.html"; } // 更严谨的做法是通过WebMvcConfigurer实现view controller }

后端加完这一段之后,刷新任意前端路由,请求都会被转发到index.html,由Vue Router接管并渲染正确页面。注意:如果前端无法预知所有路由路径,可以用一个兜底的WebMvcConfigurer来对非/api路径做转发,但不建议把接口路径也纳入转发范围,否则后端接口会直接被前端SPA吞掉。

5.5 跨域问题排查完整思路

跨域问题可能发生在三个层面:开发环境、生产环境、浏览器缓存。开发环境我用Vite代理解决生产环境因为前端资源已经打包到jar里,同源部署不存在跨域。但如果你选择了前后端分服务器部署,前端在Nginx上,后端在8080端口,那么Nginx需要加一段proxy_pass反向代理配置,让/api请求打到后端。这种方式相当于把Vite dev proxy搬到了Nginx上,原理是一样的。

排查跨域问题时,先打开浏览器开发者工具,看Network里失败请求的响应。如果响应头里没有Access-Control-Allow-Origin,那就是后端没给CORS头;如果连请求都没发出去,那要检查前端代理配置是不是没生效或写错了target地址。

最后分享一点个人体会

做完这个项目,我最深的感受是:前后端分离的核心不是界面有多漂亮,而是数据如何规范地流动。你定好了一个接口规范,前后端各管一头,后面所有页面都按这个规范来写,效率会高很多。图书管理系统虽然业务不复杂,但它把“增删改查+登录鉴权+部署上线”这条主流程完整覆盖了。顺着这个项目走一遍,你会对SpringBoot和Vue是怎么协同工作的有真正具体的理解,而不是停留在概念上。

代码再多也会忘,但排查问题的思路不会忘。如果大家在做这个项目的过程中卡在某个报错上,记得先看后端日志,再看控制台网络请求,最后看数据库执行记录,绝大多数问题都能定位出来。祝各位顺利跑起来,也欢迎交流踩坑经历。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询