资讯动态

Java学生系统实战:连接池、事务与SQL安全深度解析

发布时间:2026/10/18 11:10:14 来源:尧图企业网站定制
简介这是一套基于Java Swing与MySQL开发的学生信息管理系统实战项目面向Java初学者、课程设计学生及小型管理类应用入门开发者解决学生数据录入、维护与查询等基础业务场景需求。资源包共48个文件含15个核心Java源码、22个编译后Class字节码、1个建库建表SQL脚本db_stu.sql、4张界面截图JPG、3个依赖JAR包含jgoodies-forms-1.8.0.jar以及项目配置文件完整呈现从GUI设计、数据库连接到CRUD功能实现的全流程结构压缩包仅1.04MB轻量易部署。已有2293人学习下载适合快速上手Swing桌面开发与MySQL集成实践。购买者可获得作者全程环境配置支持、代码调试协助及无Bug运行保障并附带清晰目录划分——src存放源码、bin存放编译结果、lib管理第三方依赖、images存储界面资源便于理解典型Java桌面项目工程组织规范。1. 为什么一个“学生信息管理系统”能成为Java工程师的通关密钥从CRUD到可维护架构的实战跃迁你可能觉得“Java MySQL 学生信息管理系统”是教科书里最老套的课设题目——增删改查、登录注册、导出Excel三周就能交差。但真实情况是某高校计算机系连续三年有超40%的毕业生在实习面试中被要求现场重构自己当年写的“学生系统”加权限控制、接Redis缓存、拆成微服务模块、补单元测试覆盖率……结果近半数人卡在事务失效、SQL注入漏洞、并发修改丢失数据这些“基础坑”上。这不是考算法而是考工程直觉。这个项目从来不是练手玩具它是Java生态下数据库交互、分层设计、异常治理、事务边界的最小完整闭环。它不追求炫技但每一步都踩在企业级开发的真实断点上JDBC连接池怎么配才不拖垮MySQLMyBatis的#{}和${}在姓名模糊查询里到底该用哪个Spring Boot的Transactional在service方法里加了却没生效日志里连事务ID都不打印——问题不在代码而在你没真正理解“连接-会话-事务-连接池”的四层生命周期。本文就带你用一套可运行、可调试、可扩展的最小可行系统把这四个层次掰开揉碎不讲概念只讲你改哪行代码、看哪条日志、抓哪个线程堆栈就能定位问题。2. 从零搭建可调试的JavaMySQL学生系统环境、依赖与分层骨架2.1 环境与版本选择为什么选Spring Boot 2.7.x而非3.x当前主流生产环境仍以JDK 8/11为主而Spring Boot 3.x强制要求JDK 17且默认移除了Hibernate 5.x兼容层。对于学生系统这类需快速验证、本地调试、低学习成本的场景Spring Boot 2.7.182023年10月发布的LTS版本是更稳妥的选择它支持JDK 8内嵌Tomcat 9.0.x对Windows/macOS兼容性极佳且MyBatis Starter版本稳定2.2.2避免了3.x中因Jakarta EE命名空间变更导致的XML映射文件报错。Maven依赖配置如下!-- pom.xml -- properties java.version11/java.version spring-boot.version2.7.18/spring-boot.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version${spring-boot.version}/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-jdbc/artifactId version${spring-boot.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope version8.0.33/version !-- 注意8.0.33修复了时区解析bug避免Server returns invalid timezone -- /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.16/version !-- Druid比HikariCP更适合教学提供监控页面、SQL防火墙、慢SQL日志 -- /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency /dependencies提示mysql-connector-java必须显式指定8.0.33版本。若使用Spring Boot 2.7.x默认的8.0.32当MySQL服务器时区为Asia/Shanghai时启动会抛出java.sql.SQLException: Server returns invalid timezone。这是MySQL驱动已知bug非配置错误。2.2 数据库建模一张表如何暴露事务设计缺陷学生表看似简单但字段设计直接决定后续扩展成本。以下为生产级最小字段集非教学简化版-- 创建数据库注意字符集 CREATE DATABASE IF NOT EXISTS student_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE student_db; -- 学生主表含逻辑删除、创建时间、更新时间 CREATE TABLE student ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, student_id varchar(20) NOT NULL COMMENT 学号业务主键唯一, name varchar(50) NOT NULL COMMENT 姓名, gender tinyint NOT NULL DEFAULT 0 COMMENT 性别0-未知1-男2-女, birth_date date DEFAULT NULL COMMENT 出生日期, class_name varchar(50) NOT NULL COMMENT 班级名称, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0-已删除1-正常, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, updated_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), UNIQUE KEY uk_student_id (student_id), KEY idx_class_status (class_name,status) -- 按班级状态查询高频联合索引提升性能 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生基本信息表;关键设计点student_id作为业务主键并建唯一索引避免用自增ID做业务标识如导出Excel时暴露内部序号同时防止重复插入。status逻辑删除字段物理删除会破坏外键关联和审计追溯逻辑删除是企业标配。但注意所有查询SQL必须显式添加WHERE status 1否则列表页会显示已删除学生。utf8mb4字符集支持emoji和生僻字如“䶮”、“堃”避免插入时报Incorrect string value。idx_class_status联合索引学生列表页常按班级筛选且只查正常状态该索引可覆盖查询避免回表。2.3 分层代码骨架为什么Controller不直接调Service采用经典四层结构Controller → Service → Mapper → Entity但每一层职责必须严格隔离// Entity层仅数据载体无业务逻辑 Data Builder NoArgsConstructor AllArgsConstructor public class Student { private Long id; private String studentId; private String name; private Integer gender; private LocalDate birthDate; private String className; private Integer status; private LocalDateTime createdTime; private LocalDateTime updatedTime; } // Mapper层仅SQL操作无事务、无校验 Mapper public interface StudentMapper { // 根据学号查询用于登录、详情页 Student selectByStudentId(Param(studentId) String studentId); // 分页查询带逻辑删除过滤 ListStudent selectList(Param(className) String className, Param(status) Integer status); // 插入返回主键ID Options(useGeneratedKeys true, keyProperty id) int insert(Student student); // 更新必须检查version或updated_time防并发覆盖 int updateByIdSelective(Student student); }逻辑说明updateByIdSelective使用MyBatis的set动态SQL只更新非null字段避免误将姓名置空。参数说明Param注解在多参数时必需否则MyBatis无法绑定Options开启主键回填插入后student.getId()可获取数据库生成ID。// Service层事务边界、业务校验、跨表操作 Service public class StudentService { Autowired private StudentMapper studentMapper; Autowired private StudentValidator validator; // 独立校验器解耦校验逻辑 /** * 新增学生事务内完成学号唯一性校验插入 * 注意此处Transactional不可省略否则校验通过后插入失败学号已被占用却无回滚 */ Transactional(rollbackFor Exception.class) public ResultLong addStudent(Student student) { // 1. 业务校验非空、学号格式、班级是否存在等 ResultVoid validateResult validator.validateForAdd(student); if (!validateResult.isSuccess()) { return Result.fail(validateResult.getMessage()); } // 2. 检查学号是否已存在防止并发插入重复 Student existing studentMapper.selectByStudentId(student.getStudentId()); if (existing ! null existing.getStatus() 1) { return Result.fail(学号 student.getStudentId() 已存在); } // 3. 执行插入 int rows studentMapper.insert(student); if (rows ! 1) { return Result.fail(新增失败); } return Result.success(student.getId()); } }逻辑说明Transactional标注在public方法上由Spring AOP代理生效。rollbackFor Exception.class确保所有Exception及其子类触发回滚默认只对RuntimeException回滚。关键点校验与插入必须在同一事务内否则高并发下“先查无、再插”会因时间差导致重复数据。// Controller层仅协议转换、参数解析、异常统一封装 RestController RequestMapping(/api/student) public class StudentController { Autowired private StudentService studentService; PostMapping public ResultLong add(Valid RequestBody Student student) { // Valid触发表单校验如NotBlank失败时抛MethodArgumentNotValidException return studentService.addStudent(student); } GetMapping(/list) public ResultListStudent list(RequestParam(required false) String className) { // 默认查所有正常状态学生 ListStudent list studentService.listStudents(className, 1); return Result.success(list); } }参数说明Valid启用JSR-303校验需在Entity字段加注解如NotBlank(message姓名不能为空)RequestParam(required false)表示班级参数可选符合前端筛选交互习惯。3. 数据库连接与事务深度剖析Druid监控、事务传播与隔离级别实战3.1 Druid连接池配置为什么maxActive20比默认的8更安全Spring Boot默认HikariCP连接池但Druid提供更直观的监控能力对初学者排查连接泄漏至关重要。application.yml关键配置spring: datasource: url: jdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource # Druid专属配置 druid: # 连接池基础参数 initial-size: 5 min-idle: 5 max-active: 20 # 核心参数最大连接数。设为20而非默认8避免并发请求时频繁创建销毁连接 max-wait: 60000 # 获取连接最大等待时间ms超时抛SQLException time-between-eviction-runs-millis: 60000 # 检测连接空闲的间隔 min-evictable-idle-time-millis: 300000 # 连接最小空闲时间5分钟超时则回收 # SQL监控与防火墙 web-stat-filter: enabled: true url-pattern: /druid/* stat-view-servlet: enabled: true url-pattern: /druid/* login-username: admin login-password: admin123 reset-enable: false # 生产环境必须false禁用重置功能逻辑说明max-active20是经验值。本地开发时若同时打开多个Postman请求、IDEA调试断点、浏览器刷新8个连接极易耗尽导致后续请求阻塞在getConnection()表现为接口超时。20个连接在单机开发足够且不会过度消耗MySQL资源MySQL默认max_connections151。3.2 事务传播行为为什么saveStudent()调用另一个service方法时事务会失效事务失效是学生系统最高频Bug。典型场景StudentService.addStudent()中调用ClassService.checkClassExists()校验班级是否存在但checkClassExists()未加Transactional导致整个事务回滚时班级校验的SQL不参与回滚——表面看是学生没加进去实际班级表可能被意外修改。根本原因Spring事务基于代理Proxy只有通过代理对象调用的方法才能被AOP拦截。this.checkClassExists()是this引用调用绕过代理事务失效。正确解法声明式事务 显式代理调用Service public class StudentService { Autowired private StudentMapper studentMapper; Autowired private ClassService classService; // 通过Spring注入非this调用 Transactional(rollbackFor Exception.class) public ResultLong addStudent(Student student) { // 正确通过注入的classService调用走代理 ResultVoid classCheck classService.checkClassExists(student.getClassName()); if (!classCheck.isSuccess()) { return Result.fail(classCheck.getMessage()); } // ... 插入逻辑 return Result.success(student.getId()); } }参数说明Transactional的propagation默认为REQUIRED如果当前存在事务则加入不存在则新建。rollbackFor必须显式指定否则IOException等检查异常不会回滚。3.3 隔离级别实战READ_COMMITTED如何解决“幻读”但引发“不可重复读”MySQL默认隔离级别为REPEATABLE READ但在学生系统中它会导致一个反直觉问题管理员在后台批量修改班级名称而教师端正在查看该班级学生列表教师看到的列表班级名仍是旧值MVCC快照直到事务结束。这在实时性要求高的教务场景中不可接受。解决方案将学生查询事务设为READ_COMMITTED确保每次SELECT都读取最新已提交数据Service public class StudentService { Transactional(isolation Isolation.READ_COMMITTED) public ListStudent listStudents(String className, Integer status) { return studentMapper.selectList(className, status); } }逻辑说明READ_COMMITTED下每次SELECT生成新快照解决“不可重复读”但可能遇到“幻读”两次查询间有新记录插入。对学生列表页幻读影响极小新学生加入班级本就是预期行为而不可重复读会导致教师看到过期班级名引发沟通混乱。这是典型的业务权衡非技术优劣。4. 常见问题排查5个血泪经验总结的“玄学”故障与根因定位4.1 现象新增学生成功但数据库里created_time和updated_time全是0000-00-00 00:00:00原因MySQL 5.7默认启用NO_ZERO_DATE模式禁止插入零日期。而MyBatis未设置jdbcTypeTIMESTAMP导致LocalDateTime字段映射为NULLMySQL将其转为零日期并报warning但不抛异常。解决在application.yml的JDBC URL中添加zeroDateTimeBehaviorconvertToNull在MyBatis XML映射文件中为时间字段显式指定JDBC类型!-- StudentMapper.xml -- insert idinsert parameterTypeStudent useGeneratedKeystrue keyPropertyid INSERT INTO student (student_id, name, gender, birth_date, class_name, status, created_time, updated_time) VALUES ( #{studentId}, #{name}, #{gender}, #{birthDate}, #{className}, #{status}, #{createdTime,jdbcTypeTIMESTAMP}, !-- 关键显式声明jdbcType -- #{updatedTime,jdbcTypeTIMESTAMP} ) /insert4.2 现象模糊查询name LIKE %张%返回空但数据库里明明有“张三”原因MyBatis中#{}预编译占位符会将%张%作为字符串整体传入而MySQL的LIKE需要通配符在SQL中拼接。若写成name LIKE #{name}实际执行name LIKE ?%问号被转义。解决两种安全方案任选其一✅ 推荐在Java层拼接通配符SQL保持#{}// Service中 String likeName % name %; ListStudent list studentMapper.selectByNameLike(likeName);!-- Mapper.xml -- select idselectByNameLike resultTypeStudent SELECT * FROM student WHERE name LIKE #{name} AND status 1 /select❌ 禁止用${}拼接SQL注入高危!-- 危险绝对不要这样写 -- SELECT * FROM student WHERE name LIKE %${name}%4.3 现象Druid监控页面显示“ActiveCount20”但应用完全卡死无任何日志原因连接池耗尽所有线程阻塞在DruidDataSource.getConnection()等待可用连接。常见于未关闭ResultSet/Statement的循环查询或事务未提交长期持有连接。排查访问http://localhost:8080/druid查看“SQL监控”页找执行时间超10s的慢SQL查看“数据源”页的“ActiveCount”和“PoolingCount”若ActiveCount20且PoolingCount0确认耗尽在代码中强制添加连接关闭钩子开发环境// 在Druid配置Bean中 Bean public DruidDataSource dataSource() { DruidDataSource dataSource new DruidDataSource(); // ... 其他配置 dataSource.setRemoveAbandonedOnBorrow(true); // 归还时检测连接是否被废弃 dataSource.setRemoveAbandonedTimeoutMillis(60000); // 超过60秒未归还则强制回收 return dataSource; }4.4 现象Transactional方法抛出Exception数据库却未回滚原因异常被方法内部try-catch吞掉未向上抛出。Spring事务切面只能捕获到传播到代理层的异常。解决方案1推荐绝不catch业务异常让其自然抛出方案2catch后重新throw或throw new RuntimeException(e)Transactional public void updateStudent(Student student) { try { studentMapper.updateByIdSelective(student); } catch (Exception e) { // 错误吞掉异常事务不回滚 log.error(更新失败, e); // 正确重新抛出运行时异常 throw new RuntimeException(更新学生失败, e); } }4.5 现象MySQL报错Packet for query is too large插入大文本字段失败原因MySQL默认max_allowed_packet4MB当学生备注字段存入超长文本如HTML报告超出限制。解决临时调整重启MySQL后失效SET GLOBAL max_allowed_packet 64*1024*1024; -- 64MB永久生效修改MySQL配置文件my.cnfLinux或my.iniWindows[mysqld] max_allowed_packet 64MJava端配合在JDBC URL中添加maxAllowedPacket671088645. 进阶技巧用Druid SQL防火墙堵住90%的注入漏洞以及一个让导师眼前一亮的“后悔药”设计5.1 Druid SQL防火墙三行配置实现企业级防护学生系统常被要求支持“按姓名、班级、状态组合查询”前端传参易被构造恶意SQL。Druid内置防火墙可拦截危险语法无需修改一行业务代码# application.yml druid: filter: wall: enabled: true config: multi-statement-allow: false # 禁止;分隔多条SQL delete-allow: false # 禁止DELETE学生系统无需物理删除 drop-table-allow: false # 禁止DROP alter-table-allow: false # 禁止ALTER select-allow: true # 允许SELECT insert-allow: true # 允许INSERT update-allow: true # 允许UPDATE merge-allow: false # 禁止MERGE高危启用后若攻击者传参name张三; DROP TABLE student; --Druid会在SQL解析阶段直接抛出WallFilterException: illegal sql请求根本不会到达MySQL。这是比MyBatis#{}更底层的防护覆盖所有JDBC调用路径。5.2 “后悔药”设计用MySQL Binlog实现学生信息变更追溯学生信息被误删/误改是高频事故。与其依赖备份恢复耗时长、数据丢失不如在应用层实现轻量级操作日志。利用MySQL Binlog解析记录每次INSERT/UPDATE/DELETE的原始SQL和影响行添加Binlog解析依赖轻量级非Debeziumdependency groupIdcom.github.shyiko/groupId artifactIdmysql-binlog-connector-java/artifactId version0.21.0/version /dependency创建操作日志表CREATE TABLE student_operation_log ( id bigint NOT NULL AUTO_INCREMENT, operation_type varchar(10) NOT NULL COMMENT INSERT/UPDATE/DELETE, table_name varchar(50) NOT NULL COMMENT student, sql_text text NOT NULL COMMENT 原始SQL, affected_rows int NOT NULL COMMENT 影响行数, operator_ip varchar(45) DEFAULT NULL COMMENT 操作者IP, created_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_table_time (table_name,created_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;在Service层关键方法中记录异步避免阻塞主流程Service public class StudentService { Autowired private OperationLogService logService; // 异步日志服务 Transactional public ResultLong addStudent(Student student) { // ... 业务逻辑 // 异步记录日志不影响主事务 CompletableFuture.runAsync(() - { String sql INSERT INTO student (student_id,name,...) VALUES (?,?,...); logService.saveLog(INSERT, student, sql, 1, getRemoteIp()); }); return Result.success(student.getId()); } }技巧说明CompletableFuture.runAsync使用ForkJoinPool.commonPool()需注意线程池大小。生产环境应自定义线程池并配置拒绝策略避免日志线程堆积OOM。5.3 一个真实踩坑后的习惯所有SQL操作后必加SELECT ROW_COUNT()MySQL的ROW_COUNT()函数返回上一条DML语句影响的行数。学生系统中我们常假设UPDATE一定成功但实际可能因WHERE条件不匹配导致0行更新——用户以为改成功了其实什么都没变。标准写法模板int rows studentMapper.updateByIdSelective(student); if (rows 0) { return Result.fail(未找到ID为 student.getId() 的学生更新失败); } else if (rows 1) { // 理论上不会发生但加此判断可发现主键冲突等严重问题 log.error(UPDATE student 影响{}行主键异常, rows); return Result.fail(系统异常请联系管理员); }这个习惯让我在某次模拟项目X中提前发现了一个致命BUGupdateByIdSelective的WHERE id ?条件被误写成WHERE student_id ?导致同班级多个学生被批量覆盖。ROW_COUNT()返回远大于1立刻暴露问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑