简介:一份基于Spring Boot + MyBatis + MySQL + Thymeleaf 的同学录管理系统毕业设计源码包,面向计算机相关专业毕业生或需要完成课程设计的学生。项目覆盖了前后端完整实现,包含学生信息管理、班级管理、登录注册等典型功能模块,适合用来理解SSM整合与Thymeleaf模板引擎在实际业务中的落地方式。压缩包内共111个文件,以61个Java源码文件为主,搭配14个HTML页面、6个XML配置、5个JavaScript脚本、2个properties配置及SQL脚本等,整体结构清晰,便于直接导入开发工具后运行调试。资源大小仅1.34MB,轻量易获取,已有181人学习浏览。通过该资源可以快速获得一套可运行的毕业设计项目,包括数据库建表语句、前后端交互代码和页面样式,既能作为论文写作的支撑材料,也能在此基础上扩展功能或调整界面,减少从零搭建系统的时间成本。
1. 同学录系统为什么值得拆开看
同学录系统在毕业设计里看起来很“轻”,无非是登录注册、班级列表、学生详情加几条留言。但真把一个 Spring Boot + MyBatis + MySQL + Thymeleaf 的完整项目跑起来并讲清楚,你会发现它几乎覆盖了服务端渲染 Web 应用的所有高频考察点:关系建模、动态 SQL、会话保持、模板渲染、静态资源映射。这个压缩包的页面文件里同时出现了 index.html、login.html、classes-manage.html、student-detail.html 和 admin-index.html,说明它不只是学生端浏览,还带了班级管理和后台视图,业务面比一般 CRUD Demo 要宽。
适合读这篇文章的人有两类:一类是拿这个项目当模板、准备改造成自己毕设的在校生,需要知道每个目录和文件的作用,以及怎么改参数才能跑起来;另一类是工作后偶尔接私活或带新人的工程师,想快速判断 Spring Boot 2.x + Thymeleaf 这类老组合在现有环境下的兼容性边界。下面从选型逻辑讲起,再到数据库设计和每一层的落地代码,最后把最容易卡住的 MyBatis 与 Thymeleaf 细节单独拿出来说。
2. Spring Boot 与 MyBatis、Thymeleaf 的组合逻辑
2.1 为什么是 Thymeleaf 而不是 JSP 或前后端分离
JSP 在传统 SSM 项目里很常见,但 Spring Boot 官方对 JSP 的支持一直不算顺,需要额外配置 servlet 容器对 .jsp 的解析,打包成 jar 时还有一堆限制。Thymeleaf 是纯模板引擎,天然内嵌在 Spring Boot 的 autoconfigure 体系里,加一个 spring-boot-starter-thymeleaf 依赖就能用,不需要额外配置视图解析器。模板文件放在 src/main/resources/templates 下,静态资源放 static 下,结构非常规整。
前后端分离这种方案对毕设来说工程量更大:需要 Vue 或 React 工程、跨域处理、REST API 文档。同学录系统这种以页面跳转为主的场景,用户点开一个班级,服务端直接把带数据的 HTML 返回,体验上没问题,代码量也少得多。Thymeleaf 的一个额外优势是模板文件可以用浏览器直接打开看静态效果,虽然动态数据渲染不出来,但页面布局和 CSS 引用关系是可见的,这对调试前端样式很友好。
2.2 MyBatis 在 Spring Boot 中的定位
MyBatis 的定位是半自动 ORM:SQL 由你写,映射由框架做。对比 Hibernate,它的学习曲线平缓,SQL 可控性强,出了问题你能直接看到 SQL 语句和参数,不像 Hibernate 那样需要理解 HQL 与 SQL 的隐式转换。这个项目从 SSM 演进到 Spring Boot,核心包名和 Mapper 写法几乎可以平移,相当于把 Spring 和 SpringMVC 的 XML 配置换成自动配置,这对参考网上的 SSM 老教程很有帮助。
我一般会把 MyBatis 分成两层看:接口层定义方法签名,XML 层写 SQL。Spring Boot 启动类上标注 @MapperScan 指定 Mapper 接口所在包,框架会自动扫描并生成代理对象。XML 文件如果放在 src/main/resources/mapper 下,需要在 application.yml 里配置 mapper-locations 指过去,否则启动会报 Invalid bound statement 错误。这是新手最容易踩的第一个坑。
2.3 工程目录结构与 starter 依赖
核心依赖配置在 pom.xml 里,这里给出一个完整可用的最小集:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>mybatis-spring-boot-starter 需要显式写版本号,因为它不走 Spring Boot 的依赖管理。2.3.2 版本兼容 Spring Boot 2.5 到 2.7;如果你本地是 Spring Boot 3.x,需要换成 3.0 以上的 mybatis starter,同时注意 javax.servlet 到 jakarta.servlet 的包名迁移。mysql-connector-j 是 MySQL 8.x 驱动的新坐标,旧坐标 mysql-connector-java 也能用,但新项目建议直接用前者。
对应到源码包里的页面,templates 目录下至少有 login.html、register.html、index.html、classes-manage.html、classes-student-manage.html、classes-detail.html、student-detail.html、admin-index.html 和 admin-detail2.html。static 下的 common.css 负责全局样式。这样一个工程的职责边界就清晰了:Controller 控制页面跳转和数据填充,Service 处理业务,Mapper 操作数据库,Thymeleaf 负责呈现。
3. 核心表设计、Mapper 接口与动态 SQL
3.1 同学录的关系模型:三张表打底
同学录的核心业务对象有三个:班级、学生、留言。班级和学生是 1 对 N,一个学生有姓名、联系方式、入学年份、性格标签、留言板等属性。admin-detail2.html 这种页面说明系统里还存在管理员角色,所以还需要一张简单的用户表,用 role 字段区分普通用户和管理员。
| 表名 | 关键字段 | 说明 |
|---|---|---|
| class_info | id, class_name, grade, head_teacher, create_time | 班级基本信息,grade 表示届别 |
| student | id, class_id, name, phone, email, personality, photo_url | 同学信息,class_id 关联班级 |
| message | id, student_id, from_user, content, create_time | 同学留言,挂在某个同学名下 |
| user | id, username, password, role | 登录与权限,role 区分 admin 和 student |
建表 SQL 中需要特别注意外键的两种处理方式:物理外键约束会带来插入顺序问题,删除班级前必须先删学生和留言,业务代码里要处理级联;逻辑外键,也就是只建索引不建约束,灵活度高一些,适合毕业设计这种通过 Service 层保证数据一致性的场景。我一般推荐后者,因为 MyBatis 的批量插入和分页在无物理外键时更少踩坑。
CREATE TABLE class_info ( id INT AUTO_INCREMENT PRIMARY KEY, class_name VARCHAR(50) NOT NULL, grade VARCHAR(20), head_teacher VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, class_id INT NOT NULL, name VARCHAR(20) NOT NULL, phone VARCHAR(20), email VARCHAR(50), personality VARCHAR(255), photo_url VARCHAR(255), INDEX idx_class_id (class_id) );phone 用 VARCHAR 而不是 BIGINT,是因为手机号可能包含 +86 前缀或分隔符,数字类型会丢弃前导零。create_time 用 DATETIME 而不是 TIMESTAMP,DATETIME 的范围更宽且不受数据库时区影响,对同学录这种数据量小的系统足够用。
3.2 Mapper 接口与 XML 的动态 SQL 细节
Mapper 接口定义方法,XML 实现 SQL,这是 MyBatis 的标准用法。classes-student-manage.html 对应的查询逻辑是:进入一个班级详情页,展示该班所有学生,支持按姓名搜索。这个场景用动态 SQL 最合适。
@Mapper public interface StudentMapper { List<Student> selectStudentsByClassId(@Param("classId") Integer classId, @Param("name") String name); }<select id="selectStudentsByClassId" resultType="com.example.classbook.entity.Student"> SELECT id, class_id AS classId, name, phone, email, personality, photo_url FROM student WHERE class_id = #{classId} <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> ORDER BY id </select>这里至少有三个细节值得展开。
第一,resultType 映射时,如果实体类属性名和数据库列名不一致,比如 classId 与 class_id,可以用 AS 别名映射,也可以在 mybatis-config 里开启 mapUnderscoreToCamelCase 全局配置。前者直观,后者省事,但注意全局配置只对 resultType 自动映射生效,不改变 SQL。
第二,LIKE 查询用 CONCAT 拼接而不是直接写 '%#{name}%'。因为没有引号包裹,直接用占位符会被解析成参数占位,SQL 变成 WHERE name LIKE '%%{name}%',语法直接报错。用 CONCAT 是 MyBatis 里处理模糊查询的标准写法。
第三, 标签测试字符串时,name != ''对 null 值不会报错,但如果你用 OGNL 写name != null and name != '',注意空字符串判断在 MySQL 里对空格敏感的实际情况。搜索框输入一个空格,SQL 会查出所有名字包含空格的人,这往往不是预期行为,可以在 Service 层做 trim。
3.3 单个数字字符比较的坑与修复方式
搜索热词里有“mybatis 单个数字字符比较”,这个坑出现在 这类判断上。在 MyBatis 的 OGNL 表达式中,如果 grade 变量本身是 Character 类型,或者你的查询条件是传给一个数字字符串,直接写test="grade != '0'"会报 NumberFormatException 或将字符按数字比较。
常见做法是先把传入参数在 Service 层转成 String,再做判断;或者 XML 里写成:
<if test="grade != null and grade.toString() != '0'"> AND grade = #{grade} </if>原因在于 OGNL 对单字符字符串与 Character 的自动转换逻辑,'0' 被解析为 char,与 String 比较时触发强制类型转换。用 toString() 可以绕开这个解析分支。这个细节在静态代码审查时很难发现,只有真实跑分页或筛选才会暴露。
3.4 MyBatis 缓存级别与同学录场景的取舍
MyBatis 一级缓存是 SqlSession 级别的,同一个会话内两次相同的查询会命中缓存,不查数据库。二级缓存是 Mapper 级别的,跨会话共享,但需要注意脏读问题:如果你在一个事务里更新了某条学生记录,另一个会话的二级缓存可能还持有旧值。同学录系统以读为主,班级详情页会被反复打开,开启二级缓存能省不少查询:
<cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/>readOnly="true" 对同学录这种不要求修改缓存的场景性能更好,但如果有更新操作,必须保证刷新策略及时。我的建议是:班级表可以开二级缓存,student 表和 message 表尽量不开,因为留言和同学信息变动频繁,缓存失效控制不好会产生“明明改了数据页面还是老样子”的诡异问题。真遇到这种问题,第一反应不是查缓存配置,而是先检查当前 SQL 是否命中了 MyBatis 的缓存,再看 Thymeleaf 模板是否开启了页面级缓存。
4. Spring Boot 业务层搭建与 Thymeleaf 页面渲染
4.1 application.yml 里的关键配置项
Spring Boot 通过自动配置大幅简化了 SSM 工程里繁琐的 XML 配置,但数据源、视图缓存、SQL 日志这些还是要显式声明。下面是这个项目最可能的配置文件形态:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/classbook?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false suffix: .html mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.classbook.entity configuration: map-underscore-to-camel-case: truedriver-class-name 在 MySQL 8.x 下必须用 com.mysql.cj.jdbc.Driver,旧版 com.mysql.jdbc.Driver 虽然能跑但会打印弃用警告。url 里的 serverTimezone=Asia/Shanghai 是必填项,否则 JDBC 驱动会报 CST 时区无法识别的异常。mybatis.mapper-locations 指向 XML 文件所在路径,如果 Mapper 接口和 XML 同名且在同一包下,也可以省略,但分开放更清晰。
spring.thymeleaf.cache 在开发阶段必须设为 false。Thymeleaf 默认开启模板缓存,你改了 HTML 后刷新页面看不到效果,还以为是浏览器缓存,其实是服务端把模板编译结果缓存了。线上部署时再改回 true。
4.2 Controller 层的页面跳转与数据填充
同学录系统的 Controller 要处理两类请求:一类是页面跳转,返回模板名;另一类是表单提交,处理完重定向。看压缩包里的页面命名,登录注册和后台管理模块齐全,Controller 至少包含 LoginController、ClassController 和 StudentController。以班级管理为例:
@Controller @RequestMapping("/class") public class ClassController { @Autowired private ClassService classService; @PostMapping("/save") public String save(ClassInfo classInfo) { classService.saveOrUpdate(classInfo); return "redirect:/class/manage"; } @GetMapping("/{id}/students") public String students(@PathVariable Integer id, Model model) { model.addAttribute("classInfo", classService.getById(id)); model.addAttribute("students", studentService.listByClassId(id)); return "classes-student-manage"; } }@PostMapping("/save") 处理表单提交,saveOrUpdate 方法内部判断 id 是否为 null,为 null 则新增,否则更新。这种一个方法处理插入和更新的写法在毕设项目里很常见,虽然不符合严格的设计模式,但代码量少、好理解。redirect 关键字告诉 Spring MVC 发送 302 重定向,避免表单重复提交,这是 Thymeleaf 项目里非常推荐的做法。model.addAttribute 添加的对象在模板中直接用属性名访问,classes-student-manage.html 里用 th:each 遍历 student 列表时,就是遍历这里放入的 students。
4.3 Thymeleaf 模板语法在页面中的实际写法
register.html 和 login.html 涉及表单,classes-manage.html 涉及列表展示。Thymeleaf 的核心语法就是 th:text、th:each、th:if、th:href 这几个标签属性。写一个学生列表页的片段:
<tbody> <tr th:each="stu : ${students}"> <td th:text="${stu.name}">张三</td> <td th:text="${stu.phone}">13800000000</td> <td th:text="${stu.personality}">安静</td> <td> <a th:href="@{/student/detail/{id}(id=${stu.id})}">查看详情</a> </td> </tr> </tbody>th:each 类似于 JSP 的 forEach,stu 是循环变量,每次迭代从 students 列表中取一个 Student 对象。th:text 的作用是把标签体内容替换成表达式的值,浏览器直接打开 HTML 时看到的是占位文本张三,服务端渲染后变成真实数据。th:href 使用 @{} 语法拼接 URL,路径变量用 {id} 占位,括号中传值,生成 /student/detail/3 这样的地址。
登录状态的保持有两种选择。如果是纯服务端渲染,用 HttpSession 存储登录用户,controller 里加拦截器判断 session 是否为空,为空就重定向到 login.html。如果做了简单的 AJAX 请求,可以用 Cookie,但 Thymeleaf 模板引擎渲染的页面本来就以服务端跳转为主,直接用 Session 更匹配。admin-index.html 与 admin-detail2.html 是管理员的专属页面,拦截器配置时注意放行 /login、/register 和 /static/**,否则登录页的 CSS 都无法加载。
4.4 表单提交、参数绑定与页面回显
login.html 和 register.html 的提交逻辑,Spring Boot 的表单参数绑定做的很干脆。以注册为例:
@PostMapping("/register") public String register(User user, Model model) { if (userService.findByUsername(user.getUsername()) != null) { model.addAttribute("msg", "用户名已被注册"); return "register"; } userService.register(user); return "redirect:/login"; }前端表单 input 的 name 属性必须与 User 实体类的属性名完全一致,Spring MVC 才能自动完成参数绑定。校验失败时通过 model.addAttribute 返回提示信息,模板里用 th:if 判断 msg 是否存在再显示,避免每次刷新都弹出空白提示。
这里容易被忽略的是字符编码问题。表单提交遇到中文乱码,先检查页面 meta 标签和服务器端编码过滤器。Spring Boot 的 CharacterEncodingFilter 默认以 UTF-8 处理请求,但如果你的 MySQL 表是 latin1 或 gbk 编码,数据写进去后读出来就是乱码。建表时统一用 utf8mb4 能在源头上解决问题。
5. 项目启动、数据验证与容易翻车的细节
同学录系统跑不起来的场景,按发生频率排序:MySQL 连不上、MyBatis XML 路径配置错误、Thymeleaf 模板解析异常、端口占用。先给一个通用的验证路径,再单独说几个真正的知识点。
常见的坑可以从这几个方向排查。第一,MySQL 安装配置教程里强调的 root 密码和端口,本地连不上时先看 3306 端口是否监听,命令行执行mysql -uroot -p确认能进入。第二,Spring Boot 版本太高导致 mybatis-spring-boot-starter 不兼容,启动日志出现 ClassNotFoundException 就检查 starter 版本与 Boot 版本是否匹配。第三,thymeleaf 模板里写了错误的表达式,启动时不报错,访问页面才报 SpelEvaluationException 或 TemplateInputException,此时看控制台的具体行号和表达式片段。
MyBatis 控制台打印 SQL 是一个很实用的排错手段。在 application.yml 里追加:
logging: level: com.example.classbook.mapper: debug这样 MyBatis 会把每个 Mapper 方法最终执行 SQL 和传入参数打印出来。看日志时重点关注预编译 SQL 中 ? 的数量是否与参数个数一致。比如 selectById 方法的 SQL 打印为 WHERE id = ?,参数为 [3],说明绑定正常;如果打印出 WHERE id = 3 且没有 ? ,说明你用了 ${} 做了字符串拼接,这种情况下只要参数是用户输入,就有 SQL 注入风险,不建议这样做。
关于 MyBatis 动态 SQL 的另一个高频问题是批量操作。给班级批量导入学生时,常规循环逐条 insert 很慢且产生大量日志。使用 mybatis 进行批量写操作时我一般用 foreach 拼接批量插入:
<insert id="batchInsert"> INSERT INTO student (class_id, name, phone) VALUES <foreach collection="list" item="stu" separator=","> (#{stu.classId}, #{stu.name}, #{stu.phone}) </foreach> </insert>MySQL 对单条 INSERT 的 VALUES 个数没有硬性限制,几千条以内批量执行通常没问题,而且比单条插入快得多。注意 batchInsert 方法返回的是影响行数而不是自增主键,如果后续逻辑需要拿到新生成的学生 id,得改用 useGeneratedKeys 逐条插入或插入后再查一次。
最后说一个 Thymeleaf 时间格式化的细节。MySQL 的 DATETIME 取出后是 java.util.Date,直接在页面显示是 Tue Dec 03 10:00:00 CST 2024 这种英文格式。用 Thymeleaf 自带的日期格式化工具类可以解决:
<td th:text="${#temporals.format(message.createTime, 'yyyy-MM-dd HH:mm')}">2024-12-03 10:00</td>使用 #temporals 需要模板页引入 thymeleaf-extras-java8time 依赖,Spring Boot 的 thymeleaf starter 默认不包含它。如果不方便加依赖,另一种做法是在实体类中增加一个 String 类型的 createTimeStr 字段,查询时用 DATE_FORMAT(create_time, '%Y-%m-%d %H:%i') 生成,再直接输出。两种方式都在实际项目里见过,前者更优雅,后者改动更小,按你自己的依赖管理习惯取舍即可。
还有一点,admin-index.html 这类管理页面里的表格通常会显示班级创建时间,同学录里还会显示毕业年限。用 #temporals 处理时注意时区与 MySQL 的 serverTimezone 保持一致,否则页面显示的时间会比数据库早 8 小时。排查这类问题时,先把 yml 里的 serverTimezone=Asia/Shanghai 确认一遍,再看 Java 进程的系统时区是不是 UTC。
本文还有配套的精品资源,点击获取