每年这个时候,都有大量同学卡在毕业设计选题和实现上。如果你正在找一个“技术栈主流、功能边界清晰、可展示性强、答辩有话说”的题目,那 SpringBoot 小说阅读平台 绝对值得考虑。这类题目市面上叫法很多——在线文学阅览系统、数字化网络小说管理平台,本质都是一个东西:用 SpringBoot 做后端,围绕小说资源的展示、阅读、管理,把用户系统和后台管理串起来。我前后带过好几个学弟学妹做类似题目,也帮人改过不少代码,今天就把完整的设计思路、核心模块拆解、开发中容易踩的坑,以及答辩时老师大概率会问的问题,一次性讲清楚。
这个项目我给的定位是“中大型单体应用”的典型代表:它不像电商那样涉及复杂的订单状态和支付回调,但登录鉴权、权限控制、文件上传、缓存优化、搜索、数据分页、前端后分离这些毕设核心考核点全部覆盖。你说它复杂,它没有一堆中间件要调;你说它简单,要做得好、有亮点、能扛住质疑,需要打磨的细节非常多。这篇文章适合准备做 Java 方向毕设的同学,也适合想系统梳理 SpringBoot 实战知识点的初学者,我会把每一步的设计理由和踩坑记录都写出来。
1. 项目选题与整体架构设计
1.1 为什么小说阅读平台是毕设的“稳健型选手”
我见过太多人选题翻车:要么太简单,比如只做一个图书管理 CRUD,答辩时被老师说“工作量不足”;要么太复杂,比如微服务 + 分布式事务,代码还没写完就发现时间不够了。小说阅读平台恰好卡在中间——基础功能可以做得很快,深度又可以无限延伸。
从功能边界看,它的核心实体很清晰:用户、小说、章节、分类、书架、评论。这些实体之间关系明确,E-R 图好画,数据库表设计有东西可写。从技术覆盖看,它天然需要用户认证(JWT)、权限区分(普通用户/管理员)、全文搜索(关键字查书名/作者)、缓存优化(热门榜单)、文件处理(小说封面上传)、前后端交互(分页/搜索/阅读进度记录)。这些知识点随便展开一个都能写两页论文。
更重要的是演示效果好。给老师演示的时候,你不是在那里点按钮看表格数据,而是有一个完整的阅读器界面、翻页交互、书架展示,视觉上的完成度非常高。我在实际指导中总结的经验是:毕设项目能不能拿高分,演示体验占四成,论文和答辩占三成,代码质量占三成。小说平台在这三项里都不会吃亏。
1.2 技术选型:SpringBoot 版本、数据库、ORM 与中间件如何搭配
技术选型是第一步,也是最容易纠结的一步。很多同学上来就问“用 SpringBoot 2 还是 3”,我的建议很直接:如果现在刚开始做,且 JDK 环境是新装的,直接用 SpringBoot 2.7.x + JDK 8,这是最稳的组合。为什么不用 3.x?SpringBoot 3 是基于 JDK 17 的,javax 包名改成 jakarta,很多老教程代码直接复制会报错,而毕设最怕的就是资料匹配不上。等把 2.7 这套跑熟了、答辩通过了,再研究 3.x 也不迟。
ORM 框架我推荐 MyBatis-Plus。它对比 Spring Data JPA 的优势在于:学习成本低、SQL 可控性强、分页插件好用。JPA 虽然写起来省事,但复杂查询时多表关联容易绕晕,而且很多同学其实没系统学过 JPA 的懒加载和级联策略,出了 bug 很难排查。MyBatis-Plus 就是“升级版 MyBatis”,单表 CRUD 零 SQL 写完,多表查询就自己写 XML,符合大多数毕设代码的认知范畴。
| 技术方向 | 推荐方案 | 备选方案 | 理由 |
|---|---|---|---|
| 后端框架 | SpringBoot 2.7.x | SpringBoot 3.x | JDK8 生态成熟,资料多,避坑容易 |
| ORM | MyBatis-Plus 3.5.x | Spring Data JPA | 分页好用,SQL 可控制,学业内普及度高 |
| 数据库 | MySQL 8.x | MySQL 5.7 | 8.0 性能更好,窗口函数等特性可做亮点 |
| 缓存 | Redis | Caffeine | Redis 可以讲缓存穿透/击穿/雪崩,面试加分 |
| 权限认证 | JWT + 拦截器 | Spring Security | Security 难学,拦截器+JWT 足以演示 |
| 文件存储 | 本地磁盘 + 资源映射 | MinIO | 毕设阶段本地存储最简单,MinIO 可做亮点 |
| 前端 | Vue 2/3 + Element UI | Thymeleaf | 前后端分离更容易展示,也符合企业现状 |
数据库这块,MySQL 8.0 是当前主流。要注意 8.0 的连接驱动是 com.mysql.cj.jdbc.Driver,不是 5.x 时代的 com.mysql.jdbc.Driver,很多人在这里报错找不到驱动类。字符集全程使用 utf8mb4,因为小说内容可能会包含特殊表情符号,utf8 存不下。
1.3 系统模块划分:用户端、管理端、阅读核心共享一套后端
系统整体采用前后端分离架构,前端拆成用户端和管理端两个工程,后端是一个 SpringBoot 单体应用。
用户端的核心是阅读体验:用户注册登录后,可以浏览分类、搜索小说、查看详情、加入书架、阅读章节,阅读时记录进度和最近阅读列表。书城首页可以做轮播图推荐、热门榜单、新书速递。管理端则负责内容运营:小说分类管理、书籍信息维护、章节发布、用户禁用启用。两个端口通过同一个后端接口层对接,用 JWT 做身份识别,用拦截器区分用户角色。
从这个设计可以看出,后端虽然是一个工程,但代码组织上必须分层。我习惯把包结构按 com.example.novel 划分成如下:controller 放接口层,service 放业务逻辑,mapper 放数据操作,entity 放数据库实体,dto 放前端交互对象,common 放统一返回结果、异常处理、工具类,config 放配置类。如果你把业务逻辑全部堆在 controller 里,前期写起来快,但到答辩的时候一旦被问“你为什么不在 service 层做事务控制”,就会非常被动。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SpringBoot 核心机制与项目配置实操
2.1 自动装配原理:为什么引入依赖就能用
很多同学写 SpringBoot 项目,框架跑起来了但说不清原理。答辩时老师非常喜欢问“SpringBoot 的自动装配是怎么实现的”,所以这块必须吃透。SpringBoot 能够“开箱即用”,核心在于启动类上的 @SpringBootApplication 注解,它实际上是三个注解的合成:@SpringBootConfiguration(标识这是配置类)、@ComponentScan(扫描当前包及子包的组件)、@EnableAutoConfiguration(开启自动装配)。
自动装配的真相是:SpringBoot 在启动时会去加载 META-INF/spring.factories(SpringBoot 2.7)或 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports(SpringBoot 3.x)文件里的所有自动配置类,然后通过 @ConditionalOnClass、@ConditionalOnMissingBean 等条件注解判断“当前项目有没有引入对应依赖、有没有手动配置过 Bean”,满足条件才装配。
我用项目里的 Redis 举个例子:你只要在 pom.xml 里引入 spring-boot-starter-data-redis,启动时 RedisAutoConfiguration 就会自动创建 RedisTemplate 和 StringRedisTemplate 的实例;你什么都没配的时候,它用的是 localhost:6379;你在 application.yml 里改了地址,配置属性通过 @ConfigurationProperties(prefix = "spring.redis") 就自动绑定进去了。MyBatis-Plus 也是同理,引入了 starter 之后,SqlSessionFactory、MapperScannerConfigurer 会自动装配,所以你的 Mapper 接口没有任何实现类也能被注入。理解了这套机制,你在配置多数据源、自定义 Starter 时才能游刃有余。
2.2 配置文件治理:多环境切换、参数绑定与 banner 细节
项目中,我习惯把配置拆成三份:application-dev.yml 用于本地开发(数据库、Redis 指向 localhost),application-prod.yml 用于服务器部署(使用云数据库连接串),application.yml 中只保留公共信息并用 spring.profiles.active: dev 指定当前环境。这样换环境测试的时候只需改一行,不用每次改连接字符串。上传文件大小限制、Redis 连接池参数这些写死在公共配置里,避免两份环境配置漏改。
参数绑定也是一处容易出彩的细节。比如文件上传路径,我会自定义一个 FileUploadProperties 类标上 @ConfigurationProperties(prefix = "my.upload"),然后在启动类加 @EnableConfigurationProperties(FileUploadProperties.class)。这样 controller 里通过构造器注入这个配置类,而不是用 @Value 一个一个取值,代码会清爽很多。我见过不少代码里散落着十几个 @Value("${xxx}"),改动一个配置要全局搜索,维护体验非常差。
Banner 属于锦上添花的操作,但确实能体现用心程度。SpringBoot 启动时会扫描 classpath 下的 banner.txt 文件,如果没有就打印默认的 Spring 标志。网上有 banner 在线生成器,把 ASCII 艺术字复制进去,启动的时候终端就会显示你自己的项目名。虽然不参与功能,但演示截屏的时候观感好不少,而且启动日志里带上项目名,后续排查日志也直观一点。
2.3 资源映射:本地封面存储与访问路径打通
小说平台一定要有封面图,封面文件不可能存到数据库里(除非你转 base64,但那样浪费空间且查询慢),常规方案是上传到服务器本地磁盘,然后通过 URL 访问。SpringBoot 默认静态资源目录是 classpath:/static/,但你不可能把用户上传的图片塞进 jar 包里,所以必须做本地磁盘资源的映射。
在 WebMvcConfigurer 里重写 addResourceHandlers 方法,配置形如 /upload/** 的访问路径,指向真实的磁盘目录:
java复制@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:" + uploadPath + "/");
}
注意最后那个斜杠必须加,否则路径拼接会出问题。文件存储路径我一般放在项目根目录的 upload 文件夹,按日期分子目录:/upload/2025/05/12/uuid.jpg。文件名一定不能用用户原始文件名,一方面有中文乱码风险,另一方面如果两个用户传了同名文件会互相覆盖。我会把文件名字全部改成 UUID + 原始扩展名的形式。跨域问题,也就是前端页面访问 /upload/** 资源时报跨域,需要在配置类里统一处理 CORS,允许前端地址的跨域请求。
3. 核心功能模块设计与代码落地
3.1 数据库表结构设计与字段约束
表结构是系统骨架,我直接把最核心的六张表列出来,字段设计可以说是我经过多个项目验证过的版本:
| 表名 | 核心字段 | 说明 |
|---|---|---|
| t_user | id, username, password, nickname, avatar, role, status, create_time | 用户表,role 区分 admin/user |
| t_category | id, category_name, sort, status | 分类表,sort 控制前端展示顺序 |
| t_novel | id, novel_name, author, category_id, intro, cover_url, novel_status, click_count, is_recommended | 小说表,点击量用于榜单排序 |
| t_chapter | id, novel_id, chapter_index, chapter_title, content, create_time | 章节表,content 用 longtext 存储 |
| t_bookshelf | id, user_id, novel_id, last_read_time | 书架表,唯一索引 user_id + novel_id |
| t_read_record | id, user_id, novel_id, chapter_id, update_time | 阅读记录,记住读到哪一章 |
关于章节表的 content 字段,我建议用 longtext 类型,一章节大约几千到两万字,单字段存储完全够用。不要拆成上下两篇还分表,那是互联网大厂海量数据下的优化策略,毕设场景没有那个数据量,反而徒增复杂度。为了提升查询性能,在 t_novel 的 novel_name 和 author 字段上建立普通索引,t_chapter 的 novel_id + chapter_index 上建立联合唯一索引,保证同一本小说的章节序号不重复。
密码存储我强烈建议加盐哈希,用 BCrypt 加密而不是 MD5。Spring Security 的 crypto 包里就提供了 BCryptPasswordEncoder,不需要引入完整的 Security 框架,单独引个 spring-security-crypto 依赖即可。很多模板项目的密码是明文存在的,答辩时这个问题极容易中招,老师问一句“你怎么保证密码安全”你能回 BCrypt,这是非常明显的加分项。
3.2 用户认证与角色权限:JWT 无状态认证 + 拦截器实现
用户认证这块,我没有引入 Spring Security,而是采用 JWT 加拦截器的方案。原因是:Spring Security 的过滤器链机制对新手来说像一个黑盒,出了问题定位困难;而 JWT + HandlerInterceptor 的思路非常直观,能在答辩时把认证流程讲清楚。
登录流程是这样的:用户提交用户名密码后,controller 调 service 校验用户名密码,成功则生成一个 JWT token,里面包含 userId 和 username,设置 7 天过期时间,返回给前端。前端存到 localStorage 里,之后每次请求在 HTTP Header 里加 Authorization: Bearer <token>。后端的拦截器拦截所有 /api/** 请求,先从 Header 取 token,解析失败或过期就返回 401 状态码,前端收到后跳转登录页。
用户权限的区分做两层:第一层是 URL 拦截,admin 接口路径以 /api/admin/** 开头,管理器拦截时检查 token 解析出的 role 字段,不是 admin 就拒绝;第二层是前端路由控制,菜单根据角色动态渲染。为了提升安全性,JWT 签名密钥要复杂一些,不能写死一个 "secret",我一般生成一个 32 位以上的随机字符串,存到配置文件里。另外 JWT 有个天然缺陷是退出登录后 token 在过期前仍然能用,我在项目里用 Redis 记录退出登录的 token 加入黑名单,虽然只是一个很小的优化,但足够在论文里作为改进点了。
3.3 小说管理:后台发布与封面上传的完整链路
管理端发布小说是典型的“表单 + 文件上传”场景。前端表单包含小说名称、作者、分类下拉框、简介文本域,以及一个封面上传组件。上传时先通过独立的 /api/upload/img 接口把图片文件传到后端,后端保存后返回图片 URL,接着再提交表单一起保存小说信息。
这里有一个所有新人都会踩的坑:文件上传接口接收 MultipartFile 类型,而 SpringBoot 默认的单次上传文件最大是 1MB,请求体最大 10MB。封面图稍大一点就会报 MaxUploadSizeExceededException。在配置文件中需要把限制调大:
yaml复制spring:
servlet:
multipart:
max-file-size: 10MB
max-request-size: 20MB
对于封面的合法性校验,我在后端做了三件事:检测文件扩展名是否为 jpg/png/webp,检测文件 Content-Type,以及用 MultipartFile.isEmpty() 判断是否为空。在开发和演示阶段,这些校验可能看不出价值,但写论文时这就是“服务端二次校验”的体现,能体现你对安全的重视。
3.4 阅读器模块:章节分页、上下章切换与阅读记录
阅读器是整个系统中用户感知最强的模块。章节内容数据量不小,一次全部加载会影响首屏打开速度,所以必须做分页。我的实现方案是:第一次进入阅读页时只加载当前章节的数据,上一章和下一章的标题信息通过接口一起返回。页面底部点击“上一章/下一章”时,根据目标章节 ID 发起新请求。为了避免用户来回点击造成的界面闪烁,前端在拿到下一章内容的瞬间不做整页刷新,而是局部更新内容区并把滚动位置调到顶部。
阅读记录的保存也是一个容易忽略的细节。用户读到某章节时,不应该每次翻页都往数据库写一条记录,否则数据库压力大还会产生大量无效的更新。我做了一个“离开页面时保存”的策略:前端在组件销毁时(路由切换、关闭页签)自动调用保存接口,更新 t_bookshelf 表和 t_read_record 表。这样既保证了“下次打开接着读”的体验,又避免了频繁写库的性能问题。
我的阅读记录表中没有存阅读到的具体百分比位置,只存了章节 ID。如果要做更细的“翻到第几页第几行”,需要前端把进度值传回来。大多数项目做到章节级别已经够了,数据库也就少一列多一个逻辑,但演示时能说“支持记住上次阅读章节”已经是一个完整闭环。
3.5 搜索功能:从简单的 LIKE 到加分的中文分词检索
搜索功能是所有毕设项目中展示技术深度最好的地方。最简单的做法是 SQL 的 LIKE 模糊查询,搜索小说名和作者:
sql复制SELECT * FROM t_novel
WHERE novel_name LIKE CONCAT('%', #{keyword}, '%')
OR author LIKE CONCAT('%', #{keyword}, '%')
LIKE 方案优点是代码简单,缺点是带不走前缀索引的性能隐患、不支持中文分词。你想搜“斗破苍”,数据库中存的是“斗破苍穹”,LIKE 能匹配到,但想搜“斗破穹苍”就搜不到,因为它是按完整子串匹配的。
想要加分,可以引入 HanLP 分词配合倒排索引机制。我的做法是:小说发布时,用 HanLP 的标准分词 API 对书名、作者、简介切词,把分词结果存到数据库一个 keyword 字段里;搜索时同样对关键字分词,再匹配。这种做法让你在论文里可以介绍“引入 HanLP 中文分词,实现基于倒排思路的轻量级搜索”。如果时间和精力充足,直接集成 Elasticsearch 也可以,但 ES 对毕设来说属于“重型武器”,数据量不够反而难以体现优势,而且答辩时老师可能会追问 ES 的原理,答不好反而扣分。
3.6 书架与阅读记录:用户行为数据的管理闭环
书架本质上是一个关联表,不需要独立的业务逻辑,但它的数据流非常能体现项目的完整度。用户点击“加入书架”时,后端先检查 t_bookshelf 表中是否已有该用户和该小说的关联记录,有则提示“已经在书架中”,没有则插入一条记录。书架上展示小说封面、书名、最近阅读的章节名和最后阅读时间,这个最近阅读信息从 t_read_record 表关联查询。
阅读记录和书架的联动是亮点:用户在阅读器里正常阅读时,读到的章节会更新;用户删除书架记录时,对应的阅读记录最好也一并清理,否则会留下脏数据。我在 service 层用一个带事务的 deleteBookshelfItem 方法同时处理这两张表的操作,给方法加上 @Transactional 注解。这里就引出了事务的一个关键点——在同一个类内部调用事务方法,事务会失效,这个问题我在第 5 章展开讲。
4. 前后端联调与部署上线实践
4.1 Vue 前端分离项目的工程划分与接口风格统一
后端开发完成后,前端我选择 Vue + Element UI 来实现。管理端用 Vue + Element Plus 的 admin 模板搭框架,这能节省大量布局时间。用户端我建议从零搭建一个简洁的书城页面。用户端需要的页面有:首页(轮播 Banner + 分类推荐 + 热门榜单)、小说列表页(分类筛选 + 搜索框)、小说详情页(封面 + 简介 + 目录)、阅读页、登录注册页、书架页、个人中心页。
前后端分离的联调,第一步要统一接口规范。我的返回结构固定是 { code: 200, message: "success", data: {} },code 为 200 代表成功,其他为失败。前端在 axios 的响应拦截器里统一处理:code 为 200 就返回 data,401 就清空本地 token 并跳转登录页,其他 code 就弹错误提示。这样业务代码里不用每个请求都写一遍错误处理,页面代码会干净很多。
跨域配置在开发环境用后端 CORS 解决,生产环境则用 Nginx 反向代理。我在后端写了一个全局 CORS 配置类,允许前端开发服务器地址的跨域请求。生产部署时,我习惯把构建好的前端 dist 文件夹也交给 Nginx 托管,Nginx 监听 80 端口,/api/ 路径转发到后端的 8080 端口,这样就规避了跨域问题,也更接近真实企业部署方式。
4.2 Docker 打包部署:从 Dockerfile 到 Docker Compose 编排
你只要写过一次 Docker 部署,就会明白它能避开多少环境问题。后端这里我给出一份可以在生产环境直接使用的 Dockerfile:
dockerfile复制FROM maven:3.8.6-openjdk-8 AS builder
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline -B
COPY src ./src
RUN mvn package -DskipTests
FROM openjdk:8-jre-alpine
WORKDIR /app
COPY --from=builder /app/target/novel-platform-0.0.1-SNAPSHOT.jar app.jar
ENV TZ=Asia/Shanghai
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这里我用了多阶段构建:第一阶段在 Maven 镜像里编译打包,第二阶段把打好的 jar 包放到精简的 JRE 镜像里运行。最终镜像体积会比直接把 jar 丢进完整 JDK 镜像小很多。启动时如果需要传入生产环境的数据库地址,用 -e JAVA_TOOL_OPTIONS="-Dspring.profiles.active=prod" 指定 profile,配置文件里用环境变量形式引用连接信息。
一次性要启动 MySQL、Redis、后端三个服务,我推荐直接写一个 docker-compose.yml:
yaml复制version: '3'
services:
mysql:
image: mysql:8.0
restart: always
environment:
MYSQL_ROOT_PASSWORD: root123456
MYSQL_DATABASE: novel_db
ports:
- "3306:3306"
volumes:
- ./mysql_data:/var/lib/mysql
redis:
image: redis:7.0
restart: always
ports:
- "6379:6379"
backend:
build: .
restart: always
depends_on:
- mysql
- redis
ports:
- "8080:8080"
这里有一个很隐蔽的坑:后端服务启动时 SpringBoot 就要连接 MySQL 和 Redis,但容器编排时 depends_on 只保证启动顺序,不保证 MySQL 已经初始化完成。实际部署时会出现后端先启动、数据库连接失败、进程退出的情况。我在后端启动脚本里做了一个健康检查重试逻辑,或者用 depends_on: condition: service_healthy 配合 MySQL 的 healthcheck,能稳定解决。
4.3 上传下载大文件:分片上传思路与断点续传简析
小说平台的封面图片不大,但如果你扩展了“管理员批量导入小说 TXT 文档”的功能,就会遇到大文件上传。浏览器默认的上传方式适合小文件,超过几百 MB 时一旦断网就要从头开始传。分片上传的思路是:前端用 File API 把文件切成固定大小的切片,比如每片 5MB,然后依次上传每个分片,后端接收后暂时存到临时目录,等所有分片都传完再合并成完整文件。
如果做成分片上传,需要引入两个额外的接口:查询“哪些分片已经上传”的接口、通知“所有分片上传完毕”的接口,这样断点续传就能做出来。在答辩项目中,实现分片上传是一个非常好的加分项,因为几乎所有在线文档、云盘产品都有这种需求,你可以把“断点续传”“并发控制”“服务端合并”这些关键词写进论文的功能亮点里。但注意别过度设计,如果上传需求确实只有封面图,那这个功能就是为了做亮点而做,不具备业务合理性。
5. 开发过程中的高频踩坑与排查实录
5.1 事务失效的场景与修正方法
项目里凡是涉及多张表更新的操作,我都加了事务,比如删除小说时同时删除其章节、发布章节时更新小说最新章节信息。但很多同学加了 @Transactional 注解后,发现异常发生时数据并没有回滚。我总结最常见的三个原因。
第一个是“同类内部调用”。A 类的 a 方法调用本类的 b 方法,b 方法标了 @Transactional,但事务不会生效。因为 Spring 的事务是通过代理对象实现的,this.b() 调用的是原始对象的方法,根本绕过了代理。解决方案是注入自己(@Autowired private A self)或用 AopContext.currentProxy() 获取代理对象。
第二个是“异常被吞掉”。事务方法内部用 try/catch 捕获了异常而没有抛出,Spring 不知道发生了异常,自然不回滚。我的处理习惯是事务方法里不捕获异常,由全局异常处理器 @RestControllerAdvice 统一捕获并且返回错误信息。如果非要捕获,捕获后必须 throw new RuntimeException(e)。
第三个是“数据库表引擎不是 InnoDB”。MySQL 的 MyISAM 引擎不支持事务,虽然现代 MySQL 默认就是 InnoDB,但如果你是通过老项目改造的,要检查一下建表语句。查询 SHOW TABLE STATUS WHERE Name = 't_novel',看到 Engine 列是 MyISAM 的话,用 ALTER TABLE t_novel ENGINE=InnoDB 迁移一下。
5.2 循环依赖:构造器注入的经典问题与替代方案
SpringBoot 2.6 版本之后,默认不再允许循环依赖了,这是很多同学升级版本后突然出现启动报错的原因。循环依赖指的是 A 类依赖 B 类、B 类依赖 A 类,两者相互注入。我在项目里写过 service 层互相调用的代码,A service 调 B service,B service 又调 A service,启动时直接报错 The dependencies of some of the beans in the application context form a cycle。
解决循环依赖的最好办法是重新梳理业务边界。拿我的项目来说,B service 需要 A service 的方法,但这个方法其实并不属于 A 的核心业务,那就把公共方法下沉到工具类或者新建一个 C service,让 A 和 B 都依赖 C,而不是互相依赖。如果确实需要相互调用,可以在其中一个注入上改用 @Lazy 延迟加载,让容器先创建代理,使用时再真实初始化。但 @Lazy 属于应急方案,代码里出现两个 @Lazy 就要警惕设计问题了。SpringBoot 2.6 默认禁用循环依赖实际上是提醒你要规范设计,而不是让你想方设法绕过。
5.3 MyBatis-Plus 分页失效、JSON 循环引用等细节问题
MyBatis-Plus 的分页插件需要手动注入 PaginationInnerInterceptor,而不是引入依赖后自动生效。这是使用量最大的一类报错:分页接口返回的总条数永远是 0,或者分页参数不生效。正确做法是写一个 MybatisPlusConfig 配置类,把 MybatisPlusInterceptor 注册成 Bean。我见过的代码里,80% 的分页不生效都是漏了这一截。
JSON 循环引用是另一个经典问题。当实体类中定义为“小说包含分类对象、分类对象又包含小说列表”时,直接返回给前端,Jackson 序列化时会抛出无限递归异常,或者返回一段 $ref 引用数据。我的习惯是多表关联查询时,不要用 ORM 的关联对象嵌套,而是用 DTO 接收查询结果,把需要的字段扁平化。这不仅解决了循环引用,也让接口的返回字段精简可控。如果实在想保留对象结构,可以在字段上标 @JsonIgnore 或 @JsonIgnoreProperties,但这会让实体类和接口逻辑耦合,不好维护。
5.4 SpringBoot 3.x 迁移时的差异点提醒
如果你最终决定采用 SpringBoot 3.x,有两个上手就要注意的变化。第一个是 javax 包名改为 jakarta:所有 javax.persistence、javax.servlet、javax.validation 的 import 都要换掉。第二个是 Spring Security 6 的配置方式变化很大,很多网上教程的写法直接报错。这两个点就可以解释热词里为什么有“springboot版本太高”和“springboot 3.0 找不到 aop”了——很多人升级后发现官方文档和现有资料对不上号,半天找不到解决方案。
我不建议毕设阶段冒险使用太新的版本,但不代表不能提。论文里可以写“本项目基于 SpringBoot 2.7,该版本具有成熟的生态和稳定性;同时项目代码结构符合向上迁移到 SpringBoot 3.x 的要求”,既展现了前瞻性,又回避了升级的坑。
5.5 单元测试基础:用 MockMvc 测试核心接口
单元测试在毕设中是容易被忽略却非常有价值的部分。我在项目里给核心接口加了几个冒烟测试,用了 SpringBootTest + MockMvc。比如测试登录接口是否能正确返回 token:
java复制@SpringBootTest
@AutoConfigureMockMvc
class AuthControllerTest {
@Autowired
private MockMvc mockMvc;
@Test
void testLogin() throws Exception {
mockMvc.perform(MockMvcRequestBuilders.post("/api/auth/login")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"admin\",\"password\":\"123456\"}"))
.andExpect(MockMvcResultMatchers.status().isOk())
.andExpect(MockMvcResultMatchers.jsonPath("$.code").value(200))
.andExpect(MockMvcResultMatchers.jsonPath("$.data.token").isNotEmpty());
}
}
测试不是为了追求覆盖率,而是为了在改动代码后快速确认核心链路没有坏。答辩时老师经常问“你的项目有测试吗”,有和没有的差别很大,有几个可运行的测试用例比嘴上说“我测过了”可信得多。
6. 答辩高频问题与项目亮点沉淀
6.1 SpringBoot 核心概念类问答速记
答辩时老师一定会问几个 SpringBoot 的基础概念,我把出现频率最高的几个整理成速记版:
- SpringBoot 是什么? 它是 Spring 框架的封装和增强,通过自动配置和起步依赖简化项目初始化,遵循“约定优于配置”的理念。
- 自动装配原理是什么? 启动类上的
@EnableAutoConfiguration会加载META-INF/spring.factories中的自动配置类,通过@ConditionalOnClass等条件判断是否需要生效。 - SpringBoot 和 Spring MVC 的区别? Spring MVC 是 Web 层的 MVC 框架,SpringBoot 是整合一切的开箱即用框架,它让 Spring MVC 的配置简化,两者不是替代关系。
- SpringBoot 怎么处理跨域? 可以在配置类实现 WebMvcConfigurer 的 addCorsMappings 方法,也可以使用
@CrossOrigin注解。 - SpringBoot 怎么内嵌 Tomcat? SpringBoot 应用通过 spring-boot-starter-web 依赖引入 Tomcat 作为内嵌容器,既能以独立 jar 方式运行,也能打包成 war 部署到外部容器。
回答问题的时候不要背概念,要结合项目讲。比如自动装配被问到,你就说“我在 pom 里引入 redis starter 后,RedisTemplate 直接被注入了,因为 RedisAutoConfiguration 通过条件注解检测到 redis 的依赖存在,就自动创建了相关 Bean”,这个回答比背定义真实得多。
6.2 如何把“普通小说系统”讲出高级感
答辩除了回答问题,还要主动讲出你的亮点。我给每个功能都准备了一句可以自然说出来的话:
- 缓存设计:我在热门小说榜单接口中引入 Redis 缓存,设置 10 分钟过期时间,极大缓解了数据库查询压力。我知道缓存穿透和缓存雪崩的问题,所以对空值做了缓存,对热点 key 做了过期时间加随机值。
- 文件存储:文件上传采用 UUID 重命名和磁盘路径映射,配置了跨域访问,保证了不同文件之间的隔离。
- 安全设计:用户密码使用 BCrypt 加密存储,JWT 设置合理过期时间,退出登录时加入 token 黑名单,接口层做了全局参数校验。
- 事务控制:凡是涉及多表写入的操作全部用事务管理,比如删除小说时连带删除章节,保证数据一致性。
- 搜索优化:引入 HanLP 分词,优化了模糊搜索的召回率,让用户可以按分词结果搜索到更精准的内容。
这些话术不是让你背稿子,而是给你一个方向。老师在追问题目细节时,你能准确说出设计理由和技术难点,就已经超过大多数只做 CRUD 的同学了。
6.3 后续扩展方向:给项目留一个“可持续演进”的接口
如果有余力,或者老师追问“你这个系统还有什么可以改进的地方”,你可以顺势给出三个方向的扩展思路。第一个是性能扩展:当用户量增大时,单体应用可以按功能模块拆分为微服务,比如用户服务、小说服务、支付服务独立部署。第二个是功能扩展:引入消息队列,比如 RocketMQ 或 RabbitMQ,在用户发表评论时通过异步通知活跃用户,在管理员发布新章节时推送更新通知。第三个是运营扩展:引入多维度排行榜(热度榜、推荐榜、月票榜),配合用户画像做个性化推荐。
这些扩展思路完全可以写进论文的“展望”部分,而且不急于立刻实现。关键是你要能解释清楚每个思路要解决什么问题、引入什么技术、会产生什么新的挑战。比如引入消息队列,你要能说出它解决了同步调用带来的响应时间变长和高峰期数据库压力问题,也要知道它带来的消息丢失、消息重复消费等问题。能说出这些,老师会觉得你真的思考过系统的演进路径,而不仅仅是把作业写完了。
说实话,每次带人做这种综合项目,我都会反复强调一个观点:毕设项目不是把你的 CRUD 写完交差,而是把这半年学到的知识集中在一个可以演示的系统里面。小说阅读平台之所以合适,是因为它的业务足够直观,老师能一眼看懂;它的技术点足够丰富,你有话可写、有料可讲;它又有天然的扩展空间,论文可以写出层次感。按照这套思路走下来,你会收获一个能上线演示的完整项目,更会在一次次解决报错和排查问题的过程中,把这些框架和工具真正变成自己的东西。
