这个项目是我帮学生做的一个课程设计,名字叫《逃跑吧少年》介绍系统。说白了就是一个游戏官网式的信息展示平台,把角色图鉴、地图玩法、攻略资讯这些内容做成一个能看能管的完整站点。技术栈选了SpringBoot+MyBatis+MySQL,SpringBoot负责把整个Web应用的骨架搭起来,MyBatis负责和数据库打交道,前端用Thymeleaf做服务端渲染,一套典型的单体应用结构。这篇文章会从源码结构、代码讲解、部署文档三条线把整个项目过一遍,适合正在做Java课设或毕设的同学,也适合想搞懂SpringBoot完整开发流程的初学者。
1. 项目简介与技术选型:从零认识这个系统
1.1 逃跑吧少年介绍系统的功能画像
先把这个系统到底要做到什么程度说清楚。"逃跑吧少年"是一款非对称对抗类的休闲手游,玩家分逃生者和追捕者两个阵营。我们做的介绍系统,定位是游戏内容展示与信息管理,主要拆成前台和后台两块。
前台面向普通访客,必须包含几个核心模块:首页轮播图,用来展示游戏主视觉图;角色图鉴,把逃生者、追捕者的角色属性、技能说明做成卡片列表;地图玩法,对游戏内几张地图做图文介绍;攻略资讯,编辑可以发布游戏攻略和活动公告。这些模块的共性是"只读展示"加上"列表-详情"的信息流模式,非常适合用服务端渲染来做。
后台面向运营人员,功能就是登录验证、角色信息的增删改查、攻略文章发布、轮播图管理。后台管理是这类系统的核心价值所在,因为纯静态页面没法维护内容,必须有一个内容管理系统来支撑数据的持续更新。整条业务链路就是:管理员在后台录入数据,数据落到MySQL,前台页面通过后端接口读取数据并渲染到页面上。
数据库层面我设计了四五张表,核心的包括角色表、资讯表、管理员表,角色表存姓名、阵营、定位、技能描述、图片路径这些字段,资讯表存标题、正文、封面图、发布时间、状态。字段设计尽量简单,避免冗余,因为课程设计的时间有限,复杂外键和联表查询能省则省。
1.2 为什么选SpringBoot做这类系统
技术选型没有高大上的理由,就是"够用、好上手、生态成熟"。SpringBoot最大的价值在于自动配置,以前的Spring项目要写一堆XML配置,SpringBoot把这些全部封装好了,引入web依赖就能直接跑一个内嵌Tomcat。这对快速交付一个课程设计来说太重要了,时间应该花在业务代码上,而不是跟配置死磕。
MyBatis的选择则是出于对SQL控制的偏好。JPA虽然写起来更省事,但在多表关联和复杂查询时反而容易失控。MyBatis把SQL明明白白写在XML里,出问题时一眼就能看出来哪里错了。对一个包含列表搜索、条件过滤、排序分页场景的系统来说,手写SQL的效率并不低,而且可读性强。索引优化和慢查询定位也更直观。
前端为什么用Thymeleaf而不是Vue,这里多说一句。如果做前后端分离,意味着要搭建Vue工程、处理后端跨域、还要考虑打包后的静态资源部署,对单人课程设计来说链条太长。Thymeleaf的好处是SpringBoot默认支持,HTML页面里直接写th:each、th:text这些标签就能循环渲染数据,部署时和jar包一起打包带走,省掉一套前端工程。想改成前后端分离也容易,项目结构里Controller层返回JSON和返回页面的写法天然兼容,后续要扩展随时可以拆。
2. 源码结构硬核拆解:一个请求从页面到数据库的完整旅程
2.1 项目目录与基础结构
源码结构这一块,先看整体目录,再逐层拆开讲。
src/main/java/com/example/escapedboy ├── controller │ ├── CharacterController.java │ ├── NewsController.java │ └── GuideController.java ├── service │ ├── CharacterService.java │ └── impl │ └── CharacterServiceImpl.java ├── mapper │ ├── CharacterMapper.java │ └── NewsMapper.java ├── entity │ ├── Character.java │ └── News.java ├── common │ ├── Result.java │ └── GlobalExceptionHandler.java └── EscapedBoyApplication.java src/main/resources ├── application.yml ├── mapper │ ├── CharacterMapper.xml │ └── NewsMapper.xml ├── static │ ├── css │ ├── js │ └── images └── templates ├── index.html ├── character │ └── list.html ├── news │ ├── list.html │ └── detail.html └── admin └── login.html这个结构是标准的Controller-Service-Mapper三层架构。Controller负责接HTTP请求,Service处理业务逻辑,Mapper操作数据库。很多初学者觉得分层是多余的,直接把SQL写Controller里不就行了。实际经历过就明白,分层最大的优势是让改动可控。比如后面要给角色查询加缓存,只需要改Service层,Controller和Mapper都不动,这才是三层架构存在的意义。
实体类的核心写法也要注意,数据库字段名是下划线风格,Java属性是驼峰风格,MyBatis的mapUnderscoreToCamelCase配置打开后就能自动映射。日期字段用LocalDateTime,配合注解格式化输出,比老的Date类型好用得多。
2.2 Controller层:入口设计
Controller是请求进系统的第一道门。我习惯把页面跳转和数据接口分开写,返回页面用Controller,返回JSON数据用RestController,这样逻辑清晰。
角色图鉴列表控制器的写法大概是这样的:
@Controller @RequestMapping("/character") public class CharacterController { @Autowired private CharacterService characterService; @GetMapping("/list") public String list(Model model) { List<Character> characters = characterService.listAll(); model.addAttribute("characters", characters); return "character/list"; } @GetMapping("/detail/{id}") public String detail(@PathVariable("id") Integer id, Model model) { Character character = characterService.getById(id); model.addAttribute("character", character); return "character/detail"; } @GetMapping("/api/list") @ResponseBody public Result apiList() { return Result.success(characterService.listAll()); } }注意路径的设计原则:/list和/detail/{id}是页面跳转,返回视图名给Thymeleaf;/api/list是数据接口,返回JSON给后续可能要做的移动端或管理系统用。这样的设计让一个Controller同时兼顾页面渲染和API输出,后面的部署和二次开发都会顺手很多。
Model传参是Thymeleaf渲染的基础,model.addAttribute("characters", characters)就相当于把数据放到请求作用域里,HTML里可以用${characters}直接取。这里有个初学者经常踩的坑:返回视图名时千万别加@RestController,否则返回的字符串会被当作JSON正文输出,页面直接白屏显示一行路径名。
2.3 Service层与事务边界
Service层是业务逻辑的核心,接口加实现类的写法是我一贯坚持的做法。接口定义方法签名,实现类写具体逻辑,好处是后续做单元测试时可以轻松mock实现类,做扩展时也能在不改动调用方的前提下替换实现。
角色查询的Service实现大概这样:
@Service public class CharacterServiceImpl implements CharacterService { @Autowired private CharacterMapper characterMapper; @Override public List<Character> listAll() { return characterMapper.selectAll(); } @Override @Transactional public boolean addCharacter(Character character) { return characterMapper.insert(character) > 0; } }事务注解需要重点提一下。@Transactional不是随便加的,只有涉及写操作的方法才考虑加。比如新增角色时要同时更新角色表和操作日志表,两步必须同时成功或同时回滚,这时事务才有意义。如果只是查询方法,加上事务反而白白增加开销。事务边界放在Service层是最合适的,Controller层不用管事务,Mapper层的操作也不应该单独标事务,因为一个业务方法可能调用多个Mapper方法,只在Service层统一控制才能保证原子性。
Service层还有一个容易忽视的点就是参数校验。数据从前端传过来,不能直接放心用,空值、超长、类型不对都要拦在Controller或Service入口。规范的做法是Controller做基础的不为空、格式校验,Service做业务规则校验。比如新增角色时检查角色名是否重复,这种校验依赖数据库查询,必须放在Service层。
2.4 Mapper层与SQL管理
Mapper层负责数据库操作,接口定义方法,XML写SQL。接口用@Mapper标注或者启动类加@MapperScan都可以,我习惯在启动类加@MapperScan一次扫完,省得每个接口都写注解。
一个常见的角色查询Mapper接口:
public interface CharacterMapper { List<Character> selectAll(); Character selectById(@Param("id") Integer id); int insert(Character character); }对应的XML:
<select id="selectAll" resultType="com.example.escapedboy.entity.Character"> select * from t_character where status = 1 order by sort_order asc </select> <select id="selectById" resultType="com.example.escapedboy.entity.Character"> select * from t_character where id = #{id} </select>#{}和${}的区别要说清楚,这是个高频面试点也是安全点。#{}是预编译占位符,最终会生成?参数,MyBatis自动做类型转换和防注入处理;${}是字符串拼接,直接替换进SQL,容易产生SQL注入漏洞。写动态排序字段时确实可能用到${},但要确保字段名称是自己程序拼接的,绝不能直接把用户输入传进去。
XML文件的位置也值得留意。我在resources目录下建了mapper文件夹,application.yml里配置mybatis.mapper-locations: classpath:mapper/*.xml,这样MyBatis启动时会自动加载这些SQL映射文件。如果XML文件的namespace写错或者id不匹配,启动时会直接报BindingException,属于典型的低级错误。
2.5 前端页面的组织方式
前端用Thymeleaf模板引擎,页面放在templates目录,静态资源放在static目录。templates下的HTML文件不能直接浏览器访问,必须通过Controller路由跳转;static下的css、js、图片可以静态访问。这个目录约定很重要,新手最容易把两者混在一起。
首页的一个循环片段:
<div th:each="item : ${characters}" class="card"> <img th:src="${item.imageUrl}" class="card-img-top" alt="角色图片"/> <div class="card-body"> <h5 class="card-title" th:text="${item.name}">角色名</h5> <p class="card-text" th:text="${item.position}">角色定位</p> <a th:href="@{/character/detail/{id}(id=${item.id})}" class="btn btn-primary">查看详情</a> </div> </div>th:each类似循环遍历,th:text输出文本,@{/path}是URL表达式,会自动拼接上下文路径。这种服务端渲染页面的好处是搜索引擎可以直接抓取内容,无需额外做SEO处理。对介绍系统来说,内容能被搜索引擎收录本身就是一种推广。
如果后面想要前端列表搜索或者轮播切换效果,页面里引入Vue或jQuery也是可以的。Thymeleaf服务端渲染和前端交互并不冲突,静态资源里放好js文件,模板底部引入就行。
3. 部署文档全流程:从环境准备到线上运行
3.1 环境准备清单
部署这套系统,环境很常规,整理出来就是下面这张清单:
| 组件 | 版本建议 | 说明 |
|---|---|---|
| JDK | 1.8 或 11 | SpringBoot 2.x 搭配 JDK8 最稳 |
| Maven | 3.6 或以上 | 负责项目依赖管理和打包 |
| MySQL | 5.7 或 8.0 | 存储业务数据,8.0注意驱动版本 |
| 服务器 | CentOS 7+ / Ubuntu 20.04+ | 生产环境建议Linux,Windows也能跑 |
| Navicat | 推荐付费版,用免费的也够 | 数据库可视化管理工具 |
JDK版本这个坑我要专门提醒。现在很多新教程直接教SpringBoot 3.x + JDK17,但是3.x版本把javax包迁移到了jakarta包,很多旧代码习惯全要改,对新手不友好。我的建议是个人开发老老实实用SpringBoot 2.7.x + JDK8,资料最多、兼容性最好、遇到问题搜一下到处都是解决方案。
MySQL 8.0的用户注意,数据库连接驱动需要是com.mysql.cj.jdbc.Driver,老版本驱动是com.mysql.jdbc.Driver,配置反了会报ClassNotFoundException。8.0还要在JDBC连接串里带上serverTimezone=Asia/Shanghai,否则时区错误会导致日期处理出问题,这也是一个典型的新手报错点。
3.2 application.yml核心配置
配置文件是整个系统能否跑起来的关键,我把核心配置拆开逐项说明。
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/escapedboy?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root123456 thymeleaf: cache: false prefix: classpath:/templates/ suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.escapedboy.entity configuration: map-underscore-to-camel-case: true logging: level: com.example.escapedboy.mapper: debugserver.port决定应用监听端口,一台服务器上跑多个服务时记得改端口。数据库URL这一类配置要注意:useUnicode=true和characterEncoding=utf8是中文不乱码的必备参数,serverTimezone是MySQL 8.0的必填项。用户名密码用正确的就行,生产环境千万别把密码硬编码在配置文件里,可以用环境变量或配置中心管理,课程设计阶段用明文勾选默认密码也不算大问题。
Thymeleaf的cache: false对开发阶段调试特别重要。模板文件修改后马上生效,不用重启服务。生产环境建议改回true,模板会被缓存,减少磁盘IO,提升访问速度。
MyBatis配置中map-underscore-to-camel-case设为true后,数据库的user_name字段就能自动映射到Java的userName属性,不用在XML里写一大堆resultMap,代码量能少很多。logging.level设为debug可以打印SQL语句,排查问题时视图更清晰。
3.3 Maven构建与打包发布
构建打包是部署前的最后一步,操作本身很简单,但里面的细节不少人踩过坑。
在IDEA右侧的Maven面板中,先执行clean清理target目录,再执行package进行打包。命令行方式等效写法是:
mvn clean package -DskipTests-DskipTests跳过单元测试,避免因测试环境数据库没配好导致打包失败。如果只是不想执行测试但想编译测试代码,可以用-Dmaven.test.skip=true,效果更彻底。
打包完成后,target目录下会生成两个jar文件:一个是以-SNAPSHOT.jar结尾的完整可执行包,另一个是.jar.original,这个原始包是SpringBoot重新打包之前的文件,没有依赖,直接删掉或忽略即可。要确认打出来的是可执行包,可以用java -jar试运行一下。
打包前还要检查Maven的镜像源,国内使用阿里云镜像可以让依赖下载速度提升一个量级。在Maven的settings.xml里配置mirror节点,指向https://maven.aliyun.com/repository/public,亲测有效。依赖下载速度对开发体验影响很大,没有配镜像的人等一次全量依赖下载,泡杯咖啡回来可能还没好。
3.4 服务器部署细节
服务器部署我推荐直接用jar包运行,不需要额外装Tomcat。SpringBoot内置了Tomcat,java -jar就能直接跑起来,这是相比传统war包部署的巨大优势。
前台运行一条命令:
java -jar escapedboy-0.0.1-SNAPSHOT.jar但这种方式Ctrl+C退出服务就停了。生产环境要后台运行,用nohup命令:
nohup java -jar escapedboy-0.0.1-SNAPSHOT.jar --spring.profiles.active=prod > app.log 2>&1 &nohup让进程忽略终端挂断信号,> app.log 2>&1把标准输出和错误日志都写进app.log文件,末尾的&表示后台运行。查看日志用tail -f app.log,排查启动失败和运行时异常看这里就够了。
内存有限的话可以限制JVM参数:
nohup java -Xms128m -Xmx256m -jar escapedboy-0.0.1-SNAPSHOT.jar > app.log 2>&1 &-Xms是初始堆内存,-Xmx是最大堆内存。个人项目256m以内基本够用,别给太小否则会频繁GC导致卡顿。
服务器还需要开放防火墙端口。CentOS上用firewalld,命令是firewall-cmd --zone=public --add-port=8080/tcp --permanent加firewall-cmd --reload。等这些步骤都完成,浏览器访问http://服务器IP:8080就能看到首页了。数据库的3306端口不建议对公网开放,只允许本机访问,安全别忽略。
4. 高频踩坑实录:这些问题我全都替你趟过
4.1 端口冲突与启动失败
最常见的启动失败报错是Port 8080 was already in use,症状很直接,应用还没能起步就被操作系统拒绝了。解决方式就是先把占用端口的进程找出来杀掉。
Linux下排查:
lsof -i:8080 kill -9 进程PIDWindows下排查:
netstat -ano | findstr 8080 taskkill /PID 占用端口的PID /F还有一种情况是端口并没有被别的进程占用,但你之前启动过同一个应用没杀干净。SpringBoot启动时检测到端口被占用会启动失败,先去查后台java进程,ps -ef | grep java,把可疑进程处理掉再启动。
如果是自己写的项目里配置了server.servlet.context-path,比如设成了/escapedboy,那访问路径就不再是根路径了,URL变成了/escapedboy/。这个不算故障,但很多人部署后访问不了就是被这个绊倒的,开发时记得确认context-path到底配没配。
4.2 数据库连接与编码问题
数据库相关的报错就那几类,先说最常见的Access denied for user 'root'@'localhost',本质是用户名或密码错误。密码里如果有特殊字符,比如@、#、$,在YAML里要加双引号或者单引号包裹,否则解析会出错。YAML解析有这种情况,密码配置包上引号最保险。
Communications link failure这个报错常见于MySQL 8.0。原因通常是URL连接串没加serverTimezone参数,或者MySQL服务本身没有启动。还有一种可能:数据库账号只授权了localhost访问,而项目配置的URL用了192.168.x.x这种局域网IP,这时要改用'user'@'%'的授权方式。
中文乱码问题在MySQL里非常典型。数据库连接串加characterEncoding=utf8是第一步,建表时指定utf8mb4编码是第二步,网页端的<meta charset="utf-8">是第三步,三重保障缺一不可。特别是MySQL 8.0默认字符集虽然是utf8mb4,但旧库从5.7迁移过来的话可能还是latin1,中文读写直接就乱成一团。
数据库驱动版本不匹配也会导致连接失败,MySQL 5.7用com.mysql.jdbc.Driver没问题,但换成MySQL 8.0后必须升级驱动并改成com.mysql.cj.jdbc.Driver,同时Maven依赖里的mysql-connector版本要同步升到8.x。版本不匹配的表现极其迷惑,有时报ClassNotFound,有时报SSL连接错误,排查时需要先确认驱动版本。
这里顺手整理一个速查表,以后出了问题可以直接对号入座。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| Access denied | 用户名密码错误、权限未授权 | 核对密码,授权账号运行 |
| Communications link failure | MySQL未启动、serverTimezone缺失 | 启动MySQL,补时区参数 |
| 中文乱码 | 连接串无编码参数、库表非utf8 | 连接串加utf8,改库表字符集 |
| ClassNotFound(驱动) | MySQL版本与驱动不匹配 | 升级驱动,改用cj驱动 |
| 连接超时 | 网络不通、端口未开放 | 检查服务器安全组和防火墙 |
4.3 SpringBoot版本过高带来的兼容性问题
很多人在搜索资料的时候会遇到一大波新教程,SpringBoot 3.x + JDK17的组合看着很新鲜,但照搬之后项目反而起不来。这里想清楚一个问题:提纲和样例代码大多是基于SpringBoot 2.x写的,SpringBoot 3把javax包整体替换成了jakarta,Controller、 Entity里的注解全都要把javax.*改成jakarta.*,这套迁移逻辑对新手来说得不偿失。
如果你的项目是基于SpringBoot 3.x开发的,而网上找到的生成模板都是2.x,最简单的处理方式是在IDEA创建项目时直接选择SpringBoot 2.7.x版本,JDK保持1.8,这样不用处理任何包名的兼容问题。旧版本项目模板支持还是很多的,选择的时候留意Group的依赖版本即可。
另一个版本问题是依赖之间的冲突。比如引入一个第三方库时,这个库版本较旧,依赖的Spring版本和你项目版本不兼容。表现就是启动时出现大量NoSuchMethodError或BeanCreationException。这类问题很难一次性想到是哪里的版本问题,解决思路是用Maven Helper插件查看依赖树,找到冲突的包,在pom里对指定依赖做exclusion排除,或者升级第三方库版本。排查版本问题的利器就是Maven Helper的Show Dependencies图形化依赖树功能。
4.4 静态资源与前后端配置细节
后端接口都能正常调了,页面却一片空白或者样式全部丢失,这种问题的频率极高。最常见原因就是模板路径和静态资源路径对应不上。
Thymeleaf页面里引用css的方式:
<link th:href="@{/css/style.css}" rel="stylesheet">不加th:而直接写href="/css/style.css"在有context-path的情况下就会失效,因为资源实际路径是/projectname/static/css/style.css。统一用th:href的URL表达式,就能自动拼接context-path,省去很多麻烦。
还有一种情况是项目继承了WebMvcConfigurationSupport或者自己加了@EnableWebMvc,导致SpringBoot默认的静态资源映射被覆盖,所有css和js全部404。这种情况的解决办法是保留SpringBoot的自动配置,不要轻易自定义addResourceHandlers,或者在自己定义的同时手动把classpath:/static/加进去。
前后端分离的坑也不少见。Vue项目打包后把dist里的文件放到static目录,能解决静态展示问题,但Vue Router如果用了history模式,刷新页面会出现404。原因是没有服务器端转发,需要在SpringBoot里写一个控制器,把所有非静态资源路径转发到index.html,或者改用hash模式让URL带#来避开路径转发。这个细节我在帮朋友调项目时碰到过一次,第一次排查确实得花点时间。
说回我这套项目,因为是Thymeleaf服务端渲染,天然避开了前后端分离的路由问题,部署和运行都简单了不少。如果你的需求复杂到必须上Vue,那建议把前端工程单独部署到Nginx,API通过Nginx反向代理转发给SpringBoot,分工明确,后续各自构建和部署都不会互相影响。
5. 部署文档与代码讲解的配套经验
做了这么多遍项目,我还摸索出一套"项目交付三件套"的整理方法:源码目录一份完整注释版本、一份部署文档、一份代码讲解文档。很多人觉得代码写完了项目就交差了,实际上对课设、毕设、甚至企业内部分享来说,配套文档的价值不低于代码本身。
部署文档写的核心要点是把环境变量、配置文件、启动命令、验证方式四件事写清楚。最怕的是文档里写着"配置好环境即可"这种话,每一步具体命令是什么、运行后会看到什么LOG、端口是否正常都能写进去才叫合格文档。我在每个项目的部署文档里固定加一个"验证清单"小节,把"访问首页能看到轮播图""后台能登录""新增角色后前台能看到"这种关键路径列出来,任何人按这个清单走一遍,就知道部署是否真正成功。
代码讲解文档的写法也有一套方法论。按"页面请求-Controller-Service-Mapper-数据库"这条链路来组织,比按类逐个讲要清晰得多。我的讲解文档固定包含三块:核心业务流程图(用文字列出)、关键代码片段加注释说明、每个模块的改进扩展点。读者按这条路理解项目逻辑,比自己翻一遍源码效率高很多。
还有一个细节是源码注释和文档注释的风格统一。变量命名用通俗的英文单词,关键业务逻辑加注释,Controller的接口方法上标注请求方式和功能说明。不要写那种"i就是循环变量"的废话注释,要写清楚"这里的状态过滤决定前台是否展示该角色"这类有业务含义的说明。注释的目的是让别人快速理解意图,不是告诉别人你写了什么代码。
另外,Git版本管理习惯建议一开始就养成。每做一个模块提交一次到Git仓库,commit message写清楚这轮改动做了什么。万一改坏了代码,回滚到上一个commit就能救回来,不至于整个项目报废。课程设计中老师最反感看到的时间就是从"最终版v1"到"最终版v8"这样命名的文件,Git仓库才是正经做项目该用的工具。
最后分享一个我做了多轮这类项目沉淀下来的体会:这类SpringBoot介绍系统的技术难度并不高,核心价值在于把"从零到部署"的整条链路走通。对我个人来说,真正在上,面的收获不是某个技术点,而是学会了排查问题的思路——遇到异常先看日志,日志找关键字,定位到具体类和方法,再结合上下文判断原因。这套思路比记住任何框架API都值钱。如果正在读这篇文章的你打算把这个项目作为练手的起点,我建议你在跑通基础功能之后,挑一个模块自己改造一下,比如把角色列表改成Elasticsearch搜索,或者加上Redis缓存热点数据。折腾的过程才是真正的学习过程。