简介:这是一套面向计算机专业本科生的毕业设计级学生信息管理系统参考实现,适用于课程设计、实训项目及Java Web入门学习。系统基于Java EE技术栈构建,完整覆盖学生、班级、院系、课程与成绩等核心教务管理模块,并附有清晰的系统架构图,便于理解MVC分层设计与数据库建模逻辑。压缩包共44个文件,含22个Java源码文件(实现业务逻辑与Servlet控制)、9个XML配置文件(Spring与MyBatis整合配置)、2个SQL脚本(结构导出与含数据备份)、1张架构JPG图,以及IDEA工程元数据文件,整体9.54MB,结构规范,开箱即用。已有7344人学习下载,读者可直接导入IntelliJ IDEA,配合JDK8与MySQL快速运行调试,获取从需求分析、数据库设计、前后端交互到部署验证的全流程实践范例。
1. 学生信息管理系统简单版(包含架构图):不是写个 CRUD 就叫系统,而是用一张图说清「数据怎么流、代码怎么分、改 bug 时往哪查」
你手头刚接了个教学实训任务:两周内交付一个“学生信息管理系统简单版”,要求带架构图。别急着建 Spring Boot 项目、敲 Controller——先问自己三个问题:
- 如果教务老师导出 Excel 失败,你是翻
StudentController还是看数据库连接池配置? - 新增“班级平均分统计”功能时,该在 Service 层加逻辑,还是直接写 SQL 视图?
- 面试官指着你画的架构图问:“DAO 层如果换成 MyBatis-Plus,哪些模块要动?哪些能不动?” 你能三秒答出来吗?
这个“简单版”真正的门槛不在代码行数,而在用最小技术栈把三层职责划清楚、让每个模块有明确输入输出、且架构图能反向推导出代码结构。它不追求高并发、不堆微服务、不套 OAuth2,但必须经得起“删掉框架重写”的拷问——比如把 MyBatis 换成 JDBC 原生调用,只改 DAO 层,其他层零改动。本文就带你从零画出这张图、跑通本地 MySQL、生成可验证的 SQL 表结构,并把所有坑踩在部署前。适合 Java 初学者练手、高校实训验收、面试前快速复盘系统设计逻辑。
2. 用三层架构图锚定开发边界:为什么不用 Spring Boot 自动装配,而坚持手画包结构?
2.1 架构图不是装饰画,是开发说明书:从“学生增删改查”倒推模块划分
很多同学一上来就spring init,结果写到一半发现:
- Controller 里混了校验逻辑和数据库操作;
- Service 方法既调 DAO 又拼 SQL 字符串;
- Entity 类里塞了
@Data和@Table注解,但字段名和数据库列名不一致,报错时根本不知道是映射错了还是 SQL 写错了。
真正有效的架构图,必须从用户动作出发,反向定义每一层的契约。以“添加学生”为例:
- 表现层(Presentation Layer):只做三件事——接收 HTTP 请求参数、调用 Service 方法、返回 JSON 响应。绝不处理业务规则、绝不拼 SQL、绝不操作数据库连接。
- 业务逻辑层(Service Layer):只做两件事——校验学号是否重复(查库)、组装 Student 对象、调用 DAO 保存。绝不写
INSERT INTO ...,绝不关心 MySQL 还是 Oracle。 - 数据访问层(DAO Layer):只做一件事——执行 SQL、映射结果、抛出统一异常。不包含任何 if/else 业务判断,不调用其他 Service。
提示:这张图不是画给老板看的 PPT,而是你写代码时的“导航仪”。每新建一个类,先问:它属于哪一层?它的方法签名是否符合该层契约?比如
StudentService.addStudent()的参数必须是Student实体类,返回值必须是boolean或ResultVO,绝不能是Connection或ResultSet。
2.2 手画架构图:用 PlantUML 写出可执行的视觉文档(附可复制代码)
别用 Visio 或 PowerPoint 画——它们无法和代码同步更新。我们用 PlantUML,语法即代码,改完架构图,直接生成 PNG 插入 README:
@startuml package "学生信息管理系统" { [表现层] as presentation [业务逻辑层] as service [数据访问层] as dao [数据库] as db presentation --> service : 调用业务方法 service --> dao : 调用数据操作 dao --> db : 执行 SQL } @enduml把这个文本存为architecture.pu,用 VS Code 安装 PlantUML 插件,右键 → “Preview Current Diagram”,立刻生成清晰架构图。关键点:
- 箭头方向 = 依赖方向:表现层依赖业务层,但业务层不依赖表现层(否则 Controller 里调用
new StudentController()就成了循环依赖); - 包名即代码路径:
presentation对应com.example.student.controller,service对应com.example.student.service,命名必须严格匹配,否则 IDE 导包会报红; - 数据库图标不写 IP 和端口:架构图描述抽象关系,具体连接信息放
application.properties,图里只写“MySQL”。
2.3 包结构落地:为什么entity放在model下,而dto必须独立建包?
实际项目中,包结构决定代码可维护性。按架构图展开,标准目录如下:
src/main/java/com/example/student/ ├── controller/ // 表现层:只含 @RestController ├── service/ // 业务层:接口 + 实现类(@Service) ├── dao/ // 数据访问层:接口 + XML 或注解 SQL ├── model/ // 实体类:与数据库表一一对应(@Table, @Id) ├── dto/ // 数据传输对象:Controller 入参/出参(无业务逻辑) └── config/ // 配置类:DataSource、MyBatis 配置重点说明两个易错点:
model.Studentvsdto.StudentAddDTO:model.Student字段必须和数据库列名完全一致(如student_id),而dto.StudentAddDTO字段名用驼峰(studentId),由 Spring MVC 自动转换。如果把 DTO 和 Entity 混在一个类里,新增字段时就会污染数据库映射;dao.StudentMapper.java必须是接口:MyBatis 要求 DAO 层是接口,XML 文件或@Select注解写在实现侧。切记不要在 DAO 接口里写public void save(Student s),而要写int insert(Student student)—— 返回值是影响行数,这是 MyBatis 的契约。
3. 用 MyBatis-Plus 自动生成建表 SQL:告别手写 DDL,但必须亲手校验字段类型
3.1 从 Java 实体类生成建表语句:为什么@TableId(type = IdType.AUTO)比@TableId(type = IdType.INPUT)更安全?
MyBatis-Plus 的AutoGenerator工具能根据@TableName和@TableField注解生成 SQL,但默认生成的语句往往不符合生产规范。比如:
// model/Student.java @TableName("t_student") public class Student { @TableId(type = IdType.AUTO) // 关键!设为 AUTO,生成 SQL 时自动加 AUTO_INCREMENT private Long id; @TableField("student_name") // 显式指定数据库列名,避免驼峰转下划线出错 private String name; @TableField("grade") // 不加注解时,默认按字段名转下划线,但 "grade" → "grade",没问题 private Integer grade; }运行代码生成器后,得到的建表 SQL 是:
CREATE TABLE `t_student` ( `id` bigint NOT NULL AUTO_INCREMENT, `student_name` varchar(50) DEFAULT NULL, `grade` int DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;注意三个细节:
AUTO_INCREMENT是 MySQL 主键自增的关键字,如果type = IdType.INPUT,生成的 SQL 里就没有这行,插入时必须手动传 ID,极易出错;varchar(50)是 MyBatis-Plus 默认长度,但真实场景中姓名可能超 50 字(如少数民族长名),必须手动在@TableField加length = 100;DEFAULT CHARSET=utf8mb4是必须项,否则存 emoji 会乱码,生成器不会自动加,得在配置里显式指定。
3.2 手动校验并优化建表 SQL:为什么NOT NULL和COMMENT不能靠生成器?
生成的 SQL 往往缺少业务约束。比如学号student_no必须非空且唯一,但实体类里只写了:
@TableField("student_no") private String studentNo;生成的 SQL 是student_no varchar(20) DEFAULT NULL,这显然不对。必须人工补全:
ALTER TABLE `t_student` MODIFY COLUMN `student_no` varchar(20) NOT NULL COMMENT '学号,唯一标识', ADD UNIQUE KEY `uk_student_no` (`student_no`);更关键的是COMMENT:
COMMENT是给 DBA 看的,也是生成文档的基础(如 Swagger 读取字段注释);- MyBatis-Plus 不生成
COMMENT,必须在@TableField加value = "student_no"并配合@ApiModelProperty(如果集成 Swagger); - 所有
NOT NULL字段,必须在 Java 实体类加@TableField(fill = FieldFill.INSERT)或手动校验,否则前端不传值,MySQL 直接报错Field 'xxx' doesn't have a default value。
3.3 在 MySQL 中执行建表脚本:mysql -u root -p < init.sql之后必须做的三件事
执行完 SQL 后,别急着写 Java 代码。先登录 MySQL 做三件事:
mysql -u root -p # 输入密码后执行: SHOW CREATE TABLE t_student; # 确认字段类型、主键、索引是否正确 DESCRIBE t_student; # 查看字段是否为 NOT NULL、是否有默认值 SELECT COUNT(*) FROM t_student; # 确认表为空,避免历史脏数据干扰测试常见翻车点:
SHOW CREATE TABLE发现ENGINE=MyISAM:MyISAM 不支持事务,必须改成ENGINE=InnoDB,在建表语句末尾加ENGINE=InnoDB;DESCRIBE显示student_name是NULL:说明@TableField没加nullable = false,或建表时没写NOT NULL;SELECT COUNT返回非零:说明之前建过同名表,必须DROP TABLE t_student;再重跑脚本,否则 MyBatis-Plus 的insert可能因主键冲突失败。
4. 避坑:MyBatis-Plus + MySQL 开发中最常踩的 5 个坑(现象→原因→解决)
4.1 现象:studentMapper.selectById(1L)返回 null,但SELECT * FROM t_student WHERE id = 1在 Navicat 里能查到
原因:实体类Student的id字段类型是Long,但数据库id列是INT(不是BIGINT)。MySQL 驱动将INT映射为Integer,而 MyBatis 尝试赋值给Long时失败,静默返回 null。
解决:
- 数据库改类型:
ALTER TABLE t_student MODIFY COLUMN id BIGINT NOT NULL AUTO_INCREMENT; - 或 Java 实体类改类型:
private Integer id;(不推荐,ID 超过 21 亿时会溢出)
4.2 现象:studentMapper.insert(student)报错Field 'create_time' doesn't have a default value
原因:实体类有createTime字段,但建表时没设DEFAULT CURRENT_TIMESTAMP,且@TableField没配自动填充。
解决:
- 建表时加默认值:
create_time datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' - Java 实体类加注解:
@TableField(fill = FieldFill.INSERT) private LocalDateTime createTime; - 配置自动填充策略(
MyMetaObjectHandler类):@Override public void insertFill(MetaObject metaObject) { this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now()); }
4.3 现象:studentMapper.selectList(null)查不到数据,日志显示Preparing: SELECT * FROM t_student WHERE deleted = 0
原因:MyBatis-Plus 默认开启逻辑删除,deleted字段不存在于表中,但拦截器强制加了WHERE deleted = 0。
解决:
- 方案一(推荐):在
application.yml关闭逻辑删除:mybatis-plus: global-config: db-config: logic-delete-field: ~ # 清空逻辑删除字段名 - 方案二:建表时加
deleted tinyint(1) DEFAULT 0 COMMENT '逻辑删除:0-未删,1-已删',并加@TableLogic注解。
4.4 现象:studentMapper.updateById(student)更新后grade字段变成NULL,其他字段正常
原因:student对象的grade字段值为null,MyBatis-Plus 默认使用UPDATE t_student SET name=?, grade=? WHERE id=?,把NULL写入数据库。
解决:
- 使用
LambdaUpdateWrapper只更新非空字段:LambdaUpdateWrapper<Student> wrapper = new LambdaUpdateWrapper<>(); wrapper.eq(Student::getId, student.getId()); if (student.getName() != null) wrapper.set(Student::getName, student.getName()); if (student.getGrade() != null) wrapper.set(Student::getGrade, student.getGrade()); studentMapper.update(null, wrapper); - 或全局配置
update-strategy为not_null(mybatis-plus.configuration.call-setters-on-nulls=false)。
4.5 现象:启动报错java.lang.NoClassDefFoundError: com/mysql/cj/jdbc/Driver
原因:mysql-connector-java版本与 JDK 不兼容。JDK 8 用8.0.33,JDK 17 用8.1.0,但 Maven 里写了5.1.49(老版本)。
解决:
- 删除旧依赖,添加新版:
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- JDK 8 --> <!-- <version>8.1.0</version> --> <!-- JDK 17 --> </dependency> - 关键:检查
pom.xml是否有scope=test,把它删掉——测试范围的依赖不会被打包进 jar。
5. 用application.properties精确控制连接池:HikariCP 的 4 个必调参数和 1 个隐藏陷阱
5.1 为什么不用 Druid?HikariCP 的connection-timeout是救命参数
Druid 在监控上更友好,但 HikariCP 的性能和稳定性在中小项目中更可靠。application.properties的核心配置如下:
# 数据库连接 spring.datasource.url=jdbc:mysql://localhost:3306/student_db?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai spring.datasource.username=root spring.datasource.password=123456 # HikariCP 连接池(必须显式指定,否则 Spring Boot 2.4+ 默认用 Hikari) spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.hikari.maximum-pool-size=20 spring.datasource.hikari.minimum-idle=5 spring.datasource.hikari.connection-timeout=30000 spring.datasource.hikari.idle-timeout=600000 spring.datasource.hikari.max-lifetime=1800000重点解释四个必调参数:
maximum-pool-size=20:单机 MySQL 最大连接数默认 151,设为 20 是安全值,超过可能触发 MySQL 的max_connections限制;connection-timeout=30000(30 秒):当所有连接被占用时,新请求等待的最大时间。设太小(如 1000)会导致频繁报Connection is not available,设太大(如 300000)会让前端卡死 5 分钟;idle-timeout=600000(10 分钟):空闲连接存活时间。必须小于 MySQL 的wait_timeout(默认 28800 秒=8 小时),否则连接被 MySQL 主动断开,Hikari 不知道,下次用时报Connection reset;max-lifetime=1800000(30 分钟):连接最大生命周期。必须小于 MySQL 的interactive_timeout,且建议比idle-timeout大,避免连接被双重回收。
5.2 隐藏陷阱:serverTimezone=Asia/Shanghai缺失导致datetime字段全为0000-00-00 00:00:00
MySQL 8.0+ 默认时区是SYSTEM,但 Java 读取datetime时会按 JVM 时区解析。如果 JVM 是 UTC,而数据库存的是2024-05-20 14:30:00,Java 会当成 UTC 时间转成本地时间,变成2024-05-20 22:30:00(东八区)。
解决:URL 中必须加serverTimezone=Asia/Shanghai,且确保 MySQL 服务端时区正确:
-- 登录 MySQL 执行 SELECT @@global.time_zone, @@session.time_zone; -- 如果返回 SYSTEM,执行: SET GLOBAL time_zone = '+08:00';5.3 验证连接池是否生效:用 Actuator 端点看实时连接数
加 Actuator 依赖,暴露健康检查端点:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency>application.properties加:
management.endpoints.web.exposure.include=health,metrics,threaddump management.endpoint.health.show-details=always启动后访问http://localhost:8080/actuator/health,返回 JSON 中有:
"diskSpace":{"total":51234567890,"free":12345678901,"threshold":10485760}, "db":{"status":"UP","database":"MySQL","hello":"1"}再访问http://localhost:8080/actuator/metrics/hikaricp.connections.active,看到"value":2,说明连接池已工作。如果active始终为 0,说明连接根本没建立成功,要回查url和username/password。
6. 架构图验证法:用「逆向生成」检验你的设计是否真能落地
6.1 从代码反推架构图:用 IntelliJ 的 Dependency Structure Matrix 功能
IDEA 自带的依赖分析工具,能验证你画的架构图是否真实存在。操作路径:Analyze → Analyze Dependencies → Show Dependency Structure Matrix
选择com.example.student包,勾选Include test sources,点击 OK。你会看到一个矩阵:
- 行是包名(
controller,service,dao),列是其他包; - 单元格颜色表示依赖强度(绿色=强依赖,灰色=弱依赖);
- 如果
controller行里dao列有绿色块,说明 Controller 直接调用了 DAO —— 架构图画错了,必须删掉; - 如果
service行里controller列有块,说明 Service 反向依赖表现层 —— 违反分层原则,需重构。
这个矩阵比手画图更残酷,它不撒谎。我带过的学生里,80% 第一次跑出来都发现controller → dao的红线——因为写了@Autowired StudentMapper mapper在 Controller 里。记住:架构图不是目标,而是约束。每次加依赖前,先看矩阵会不会变红。
6.2 用单元测试验证分层契约:一个@Test方法覆盖三层协作
写一个测试,不启动 Spring 容器,纯验证分层逻辑:
@SpringBootTest(classes = {StudentService.class, StudentMapper.class}) class StudentServiceTest { @Autowired private StudentService studentService; @Autowired private StudentMapper studentMapper; @Test void should_insert_student_and_return_id() { // 准备数据 Student student = new Student(); student.setName("张三"); student.setGrade(85); // 执行业务逻辑(不走 Controller,直调 Service) boolean result = studentService.addStudent(student); // 验证结果 assertThat(result).isTrue(); assertThat(student.getId()).isNotNull(); // ID 已被 MyBatis-Plus 自动填充 // 验证 DAO 层是否真写了数据库 Student saved = studentMapper.selectById(student.getId()); assertThat(saved.getName()).isEqualTo("张三"); assertThat(saved.getGrade()).isEqualTo(85); } }这个测试的价值在于:
- 它绕过了 HTTP 层,证明 Service 和 DAO 能独立工作;
assertThat(student.getId()).isNotNull()验证了IdType.AUTO生效;studentMapper.selectById(...)直接调用 DAO,确认 DAO 接口和 XML/注解 SQL 匹配。
如果这个测试失败,问题一定在 Service 或 DAO 层,而不是网络或前端——这就是分层的价值:故障域隔离。
6.3 给架构图加「演化注释」:标注未来可替换点(这才是工程师思维)
一张静态架构图没有生命力。我在自己的架构图底部加了一行小字:
可替换点:DAO 层 → 替换为 JPA/Hibernate;Service 层 → 提取为 Dubbo 接口;表现层 → 改造成 Vue 前端调用 REST API。
这不是画饼,而是标记技术债的边界。比如:
DAO 层标注【可替换】:意味着只要保持StudentMapper接口不变,内部换成 JPA 的StudentRepository,Service 层代码一行不用改;表现层标注【可替换】:说明StudentController的@PostMapping方法签名(参数/返回值)就是 API 协议,前端无论用 React 还是小程序,都按这个契约调用;- 绝不写“未来接入 Redis 缓存”这种虚话——要写就写清楚:
在 StudentService.getById() 方法上加 @Cacheable(key="#id"),缓存名 student:#{id}。
最后说句血泪经验:我见过太多“简单版”项目,因为没画清架构图,后期加个导出 Excel 功能,硬生生把 Service 层写成了 Excel 工具类集合,DAO 层开始调 Apache POI,最后谁都不敢动。画图不是浪费时间,是给未来的自己买后悔药。这张图越简单,越要让它像手术刀一样精准——切开系统,看清每一层的肌肉和血管。希望帮到你。
本文还有配套的精品资源,点击获取