基于JSP+Servlet+MySQL的学生信息管理系统开发实战

发布时间:2026/9/16 6:25:49
基于JSP+Servlet+MySQL的学生信息管理系统开发实战 简介基于 JSP/Servlet/MySQL 技术栈的学生信息管理系统完整项目包面向数据库课程设计、Java Web 初学与期末实训人群。系统覆盖用户登录注册、学生信息增删改查、成绩统计分析报表、数据可视化展示与权限分级管理模块能够帮助读者理解 Servlet 处理请求、JSP 页面渲染、MySQL 数据持久化及前后端交互的核心流程也可作为课设或毕设的改造基础。压缩包共 268 个文件大小约 10.25MB核心为 51 个 Java 源文件、52 个 JSP 页面及对应 class 文件另含 13 个依赖 jar 包、CSS/JS 前端资源、配置文件与数据库工作目录同时附带说明文档和附赠资源便于快速部署、运行与二次开发。已有 35 人学习下载适合需要完整可运行代码、数据库脚本和项目文档的开发者参考。1. JSPServletMySQL 做学生信息管理系统数据库课程设计为什么还选这套栈每年做数据库课程设计都有同学在技术栈上纠结半天。选 Spring Boot评分老师会追问JDBC 层在哪里选纯 JDBC 控制台程序又拿不出页面和交互。JSPServletMySQL 恰好卡在中间MVC 结构能讲清楚数据库设计和 SQL 能力是露在外面的部署也只需要 Tomcat 加一个 MySQL 实例。这个学生信息管理系统正好覆盖课程设计最常见的评分点——用户注册登录、学生信息增删改查、成绩统计分析报表、ECharts 数据可视化再加上权限分级管理。读懂这套代码相当于把 Servlet 生命周期、Session 机制、JDBC 连接和聚合查询全部过了一遍。2. MySQL 表结构与 JDBC 数据访问层把增删改查的评分点做扎实2.1 用户表、学生表、成绩表三张核心表的字段设计学生信息管理系统听起来功能多落到数据库就是三张表sys_user 管登录和权限student 管学生档案score 管成绩。很多课程设计会再加一张 course 表但考虑到成绩报表基本按课程名分组把 course_name 直接冗余在 score 表里更省事评分时也更容易解释字段含义。表名承担职责关键字段与其他表关系sys_user登录认证与权限分级username、password、salt、role无外键student学生档案维护student_no、name、class_name、status被 score 引用score成绩明细与统计来源student_id、course_name、score外键指向 student(id)CREATE TABLE sys_user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password CHAR(64) NOT NULL COMMENT SHA-256(salt明文), salt CHAR(16) NOT NULL COMMENT 16位随机盐, role TINYINT NOT NULL DEFAULT 2 COMMENT 0管理员 1教师 2学生, real_name VARCHAR(50) COMMENT 显示名, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; CREATE TABLE student ( id INT AUTO_INCREMENT PRIMARY KEY, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0 COMMENT 0未知 1男 2女, class_name VARCHAR(50) COMMENT 班级, major VARCHAR(50) COMMENT 专业, phone VARCHAR(20), email VARCHAR(100), enroll_date DATE COMMENT 入学日期, status TINYINT DEFAULT 1 COMMENT 1在读 0休学 2毕业, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id INT NOT NULL COMMENT 关联student.id, course_name VARCHAR(50) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) DEFAULT 0.0 COMMENT 学分, score DECIMAL(5,2) NOT NULL COMMENT 百分制成绩, exam_date DATE, FOREIGN KEY (student_id) REFERENCES student(id) ON DELETE CASCADE, UNIQUE KEY uk_stu_course (student_id, course_name) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT成绩表;几个容易被扣分的点字符集选 utf8mb4 而不是 utf8否则插入 emoji 或生僻姓氏会报 Incorrect string value学号和用户名必须加 UNIQUE这是数据完整性的评分点score 表加 (student_id, course_name) 联合唯一键防止同一个人同一门课录两次。外键 ON DELETE CASCADE 的意思是删学生档案时成绩一并删除避免孤儿数据这比在 Service 层手动删两条更能体现数据库设计意识。DECIMAL 类型专门用来存成绩和学分不要用 FLOAT。FLOAT 是近似值89.5 这种分数在浮点运算后会变成 89.49999做统计报表时聚合结果对不上评分老师用几条边界数据就能测出来。关于索引联合唯一键本身就是一个索引按学号精确查询走的是 student_no 的唯一索引但 name LIKE %张% 这种前置模糊查询用不上索引数据量上万后变慢是正常现象课程设计阶段不需要做全文索引。2.2 Druid 连接池与 db.properties三个必调参数JDBC 直连的写法在课程设计里也能跑但每次请求都 new 一个 ConnectionMySQL 默认的 wait_timeout 一超时页面就开始报 Communications link failure。常见做法是用 Druid 连接池把连接复用起来配置放在 src 根目录的 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/student_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8 jdbc.usernameroot jdbc.password123456 jdbc.initialSize5 jdbc.maxActive20 jdbc.maxWait60000 jdbc.validationQuerySELECT 1三个必调参数maxActive 控制最大连接数课程设计并发量小10 到 20 足够开太大反而占 MySQL 的连接数maxWait 是拿不到连接时的等待毫秒数默认 -1 表示无限等页面会卡死设成 60000 至少能快速报错serverTimezoneAsia/Shanghai 必加MySQL 8.x 的驱动不写时区直接启动报错这条在 mysql 安装配置教程里经常被单独拿出来说。characterEncodingutf8 配合 JSP 页面编码一起用能省掉一半中文乱码问题。public class DruidUtil { private static DataSource dataSource; static { try (InputStream in DruidUtil.class.getClassLoader().getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); dataSource DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }druid-1.2.x 的 jar 包要放到 WEB-INF/lib 下如果用 Maven 就引入 druid 和 mysql-connector-j 两个依赖。注意 mysql-connector-java 老版本驱动类是 com.mysql.jdbc.Driver8.x 改成 com.mysql.cj.jdbc.Driver配错的症状是启动时 ClassNotFoundException。2.3 BaseDAO 抽取PreparedStatement 封装与防注入Servlet 里直接写 JDBC 代码也能交差但数据库课程设计一般会被要求体现分层。常见做法是抽一个 BaseDAO把连接、预编译、结果集映射封装起来子类只写 SQL 和参数。public abstract class BaseDAO { protected T ListT queryList(String sql, RowMapperT mapper, Object... args) { try (Connection conn DruidUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } try (ResultSet rs ps.executeQuery()) { ListT list new ArrayList(); while (rs.next()) { list.add(mapper.map(rs)); } return list; } } catch (SQLException e) { throw new RuntimeException(SQL执行失败: sql, e); } } protected int update(String sql, Object... args) { try (Connection conn DruidUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i args.length; i) { ps.setObject(i 1, args[i]); } return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException(更新失败: sql, e); } } }这段代码有几个细节值得写进设计说明书try-with-resources 保证 Connection、PreparedStatement、ResultSet 自动关闭不写 finally 也不漏连接全程用 PreparedStatement 的 ? 占位符拼参数而不是字符串拼接 SQL这是防 SQL 注入的第一道防线答辩时经常被单独问。RowMapper 是函数式接口子类用 Lambda 把每行 ResultSet 变成 Student 对象。学生模块的 DAO 至少要有分页查询、按主键查、插入、更新、删除五个方法public class StudentDAO extends BaseDAO { public ListStudent findByPage(String keyword, int offset, int limit) { String sql SELECT * FROM student WHERE name LIKE ? OR student_no LIKE ? ORDER BY student_no LIMIT ?, ?; String like % keyword %; return queryList(sql, rs - new Student(rs), like, like, offset, limit); } public int count(String keyword) { String sql SELECT COUNT(*) FROM student WHERE name LIKE ? OR student_no LIKE ?; // 返回总条数分页组件靠它算总页数 } public Student findById(int id) { /* SELECT * FROM student WHERE id ? */ } public int insert(Student s) { /* INSERT ... */ } public int update(Student s) { /* UPDATE ... WHERE id ? */ } public int deleteById(int id) { /* DELETE FROM student WHERE id ? */ } }分页 SQL 用 LIMIT offset, limitoffset 是 (page-1)*pageSize前端每传一次页码就重查一次课程设计不需要做游标分页。keyword 里只匹配 name 和 student_no不要给所有列都加 LIKE否则查不出结果时用户会怀疑是 bug。3. 登录注册与权限分级管理盐值哈希、Session 与过滤器拦截3.1 注册模块密码加盐哈希不能明文入库注册功能最容易被忽略的是密码存储方式。直接把密码明文存进 sys_user数据库一旦被导出来所有账号裸奔这在课程设计答辩里是硬伤。常见做法是 SHA-256 加盐每个用户生成独立的随机盐把盐拼在密码后面一起做哈希库里只存盐和哈希结果。public class PasswordUtil { public static String genSalt() { return UUID.randomUUID().toString().replace(-, ).substring(0, 16); } public static String hash(String salt, String rawPassword) { String input salt rawPassword; try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] bytes md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : bytes) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } } }验证登录时用库里查出来的 salt 重新算一遍哈希和 password 字段比对。哈希结果定长 64 位十六进制字符所以建表时 password 用 CHAR(64)salt 用 CHAR(16)字段长度和算法输出严格对应这比统一 VARCHAR 更能体现设计意识。注册 Servlet 里还要做两件事一是先用 SELECT 查 username 是否已存在存在就回注册页提示二是两次输入密码的一致性校验前端做一遍、后端也要做一遍因为前端校验可以被绕过。后端校验不通过直接转发回 register.jsp 并携带错误消息不要跳转到成功页。3.2 登录流程BaseServlet 抽取与 Session 登录态登录接口按 MVC 惯例放在 UserServlet 里通过 action 参数分发WebServlet(/user) public class UserServlet extends HttpServlet { Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (login.equals(action)) { doLogin(req, resp); } else if (register.equals(action)) { doRegister(req, resp); } else { resp.sendError(404); } } }doLogin 的核心逻辑从请求里取 username 和 password调 UserDAO 按用户名查用户查不到或哈希不匹配都统一提示用户名或密码错误不要分别提示避免攻击者用注册接口做用户名枚举。登录成功后执行 req.getSession().setAttribute(loginUser, user)Session 默认超时是 30 分钟可以在 web.xml 里调整session-config session-timeout60/session-timeout /session-config登录成功之后按角色分流管理员进 /admin/index.jsp教师进 /teacher/index.jsp学生进 /student/index.jsp。注意要用 resp.sendRedirect 而不是 forward否则刷新页面会重复提交登录请求。3.3 权限分级管理AuthFilter 按 URL 前缀拦截角色权限分级管理在 JSPServlet 里的标准做法是 URL 前缀分区加 Filter 拦截。页面和 Servlet 按角色分目录管理员访问 /admin/ 下的资源教师只能访问 /teacher/学生只能访问 /student/公共登录注册页不设限制。角色可访问路径操作范围管理员 role0全部路径用户管理、学生增删改查、成绩报表、数据可视化大屏教师 role1/teacher/、/public/学生查询、成绩录入、统计报表学生 role2/student/、/public/个人信息、个人成绩查询WebFilter(/*) public class AuthFilter implements Filter { private static final ListString PUBLIC_URLS Arrays.asList( /login.jsp, /register.jsp, /user, /css, /js, /images); Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI().substring(req.getContextPath().length()); if (isPublic(uri)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(false); User user session null ? null : (User) session.getAttribute(loginUser); if (user null) { resp.sendRedirect(req.getContextPath() /login.jsp?errtimeout); return; } if (uri.startsWith(/admin/) user.getRole() ! 0) { resp.sendError(403); return; } if (uri.startsWith(/teacher/) user.getRole() 1) { resp.sendError(403); return; } chain.doFilter(request, response); } }三个要点第一req.getSession(false) 不会新建 Session未登录用户拿到的 session 是 null不会给每个请求都种 Session第二公开路径列表里必须有 /css 和 /js否则登录页的样式表全被拦截表现为页面能打开但没样式第三拦截到未登录请求要 redirect 到登录页并带 err 参数登录页根据 err 显示会话超时或无权限提示。权限部分写到这个程度已经超出能登录的基本要求答辩时可以讲清楚过滤器链的执行顺序。4. 学生增删改查与成绩统计分析报表动作分发与 GROUP BY 聚合4.1 增删改查的动作分发与分页边界处理学生信息模块是业务主体用一个 StudentServlet 加 action 参数统一分发比每个功能建一个 Servlet 更符合课程设计的代码量WebServlet(/student/manage) public class StudentServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); switch (action) { case list - listWithPage(req, resp); case edit - showEditPage(req, resp); case delete - delete(req, resp); default - resp.sendError(404); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); switch (action) { case insert - insert(req, resp); case update - update(req, resp); default - resp.sendError(404); } } }action业务动作请求方式list分页条件查询列表GETedit按主键查单条去编辑页GETdelete按主键删除GETinsert新增学生POSTupdate修改学生POST分页列表是增删改查里最容易出错的环节。页面要显示三样东西当前页数据、总条数、总页数。总条数用 COUNT(*) 单独查一次不要用 List.size() 代替因为 LIMIT 之后拿到的只是当前页的数据。private void listWithPage(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { int page 1; int pageSize 10; try { page Integer.parseInt(req.getParameter(page)); pageSize Integer.parseInt(req.getParameter(pageSize)); } catch (NumberFormatException ignored) { } String keyword req.getParameter(keyword); if (keyword null) keyword ; StudentDAO dao new StudentDAO(); int total dao.count(keyword); int totalPages Math.max(1, (int) Math.ceil(total * 1.0 / pageSize)); page Math.min(page, totalPages); ListStudent list dao.findByPage(keyword, (page - 1) * pageSize, pageSize); req.setAttribute(studentList, list); req.setAttribute(page, page); req.setAttribute(totalPages, totalPages); req.setAttribute(keyword, keyword); req.getRequestDispatcher(/admin/student_list.jsp).forward(req, resp); }这里有个细节page 不能只做解析还要做边界收敛。用户手动把 URL 改成 ?page99要么返回空列表要么 MySQL 报 LIMIT 溢出正确做法是 page 超过总页数时收敛到总页数小于 1 时回退到 1。类似的防御逻辑写在 Servlet 里而不是页面里因为分页控件只是前端约束。插入和更新的参数校验集中写在一个方法里校验学号非空、姓名非空、学号长度不超过字段定义。学号重复时 MySQL 会抛重复键异常要在 Servlet 里捕获 SQLIntegrityConstraintViolationException转换成学号已存在的友好提示不能把堆栈打到页面上。4.2 成绩统计分析报表GROUP BY 聚合与分数段行转列成绩统计分析报表是整门课程设计的加分点评分点在于 SQL 聚合能力。报表页需要三类数据各课程平均分和及格率、最高最低分、分数段分布。一份 SQL 可以同时出前三项SELECT course_name, COUNT(*) AS total, ROUND(AVG(score), 2) AS avg_score, MAX(score) AS max_score, MIN(score) AS min_score, ROUND(SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) / COUNT(*) * 100, 2) AS pass_rate FROM score GROUP BY course_name ORDER BY avg_score DESC;逐个说AVG 是平均值ROUND 保留两位小数防止出现 82.333333SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) 是统计及格人数的标准写法除以 COUNT() 再乘 100 就是及格率。COUNT() 统计的是分组内的行数也就是选课人数如果缺考的学生在成绩表里没有记录及格率不会被拉低这条要在设计文档里写明约定。分数段分布用多段 CASE WHEN 做横向展开也叫行转列SELECT course_name, SUM(CASE WHEN score 90 THEN 1 ELSE 0 END) AS level_great, SUM(CASE WHEN score 80 AND score 90 THEN 1 ELSE 0 END) AS level_good, SUM(CASE WHEN score 70 AND score 80 THEN 1 ELSE 0 END) AS level_mid, SUM(CASE WHEN score 60 AND score 70 THEN 1 ELSE 0 END) AS level_pass, SUM(CASE WHEN score 60 THEN 1 ELSE 0 END) AS level_fail FROM score GROUP BY course_name;这种写法把分数段从行转换成列前端图表直接拿五个字段当五个系列不需要在 Java 里二次加工。很多同学习惯先在 Java 里把成绩全查出来再循环计数数据量小也能跑但 SQL 里能完成的事不要挪到应用层——统计在哪里做的是数据库课程设计答辩最常问的问题。如果觉得 SQL 太长也可以把这段逻辑封装成 MySQL 存储过程但课程设计建议保留在 DAO 层方便现场演示和讲解。4.3 报表数据转 JSONReportServlet 与 Gson 序列化图表要的数据格式和 JSP 要的不一样常见做法是单独写一个 ReportServlet专门输出 JSON 给 AJAX 接口WebServlet(/report/data) public class ReportDataServlet extends HttpServlet { protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws IOException { String type req.getParameter(type); ReportDAO dao new ReportDAO(); Object data; if (course.equals(type)) { data dao.courseSummary(); } else if (level.equals(type)) { data dao.scoreLevelDist(); } else { resp.sendError(400); return; } resp.setContentType(application/json;charsetUTF-8); resp.getWriter().write(new Gson().toJson(data)); } }resp.setContentType(application/json;charsetUTF-8) 这行不能省。不加 charset 时浏览器默认按 ISO-8859-1 解析JSON 里的中文课程名会变成乱码而且这种乱码在 DevTools 里看响应是正常的只有渲染到图表上才暴露。Gson 序列化 ListMapString, Object 是最省事的方案返回结构直接对应 ECharts 的 xAxis 和 series 数据格式。5. ECharts 数据可视化落地报表装配、排错与答辩验证清单5.1 ECharts 本地引入与图表装配数据可视化展示通常指成绩统计的柱状图、饼图和折线图。ECharts 5.x 的 echarts.min.js 下载后放到 webapp/js 目录下JSP 里用相对路径引用避免硬编码端口script src${pageContext.request.contextPath}/js/echarts.min.js/script script src${pageContext.request.contextPath}/js/jquery.min.js/script页面里放一个 div 当图表容器必须显式设置宽高ECharts 初始化时容器宽高为 0 会渲染成空白。AJAX 请求上一章的 /report/data 接口装配双轴图$.get(contextPath /report/data?typecourse, function (data) { var chart echarts.init(document.getElementById(courseChart)); chart.setOption({ tooltip: { trigger: axis }, legend: { data: [平均分, 及格率] }, xAxis: { type: category, data: data.map(function (d) { return d.course_name; }) }, yAxis: [ { type: value, name: 平均分, max: 100 }, { type: value, name: 及格率, axisLabel: { formatter: {value}% } } ], series: [ { name: 平均分, type: bar, data: data.map(function (d) { return d.avg_score; }) }, { name: 及格率, type: line, yAxisIndex: 1, data: data.map(function (d) { return d.pass_rate; }) } ] }); });注意及格率在 SQL 里算出来是 0 到 100 的数值不是 0 到 1 的小数这里直接映射到右侧 y 轴。课程设计不需要做成实时刷新的大屏柱状图加饼图放在报表页同一行CSS Grid 两栏布局足够导出图片用图表右上角自带的保存按钮即可。5.2 三个必查排错点第一JSP 编译后的 class 文件位置。Tomcat 会把 JSP 翻译成 Java 再编译成 class位置在 Tomcat 安装目录的 work/Catalina/localhost/项目名/org/apache/jsp 下。改了 JSP 但页面没变化先看这里对应 .class 文件的时间戳没更新说明 Tomcat 没重新编译直接删掉 work 目录下该项目文件夹重启 Tomcat 强制重新翻译。第二MySQL 连接报 The server time zone value。MySQL 8.x 默认时区和驱动要求不一致解决方法是连接 URL 带 serverTimezoneAsia/Shanghai。如果带了还报错检查 mysql-connector 版本旧驱动对 JDBC 4.2 时区处理有兼容问题升级到 mysql-connector-j 8.0.33 以上。第三中文乱码。乱码源头有三个JSP 头部缺 % page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %、Servlet 输出 JSON 没写 charsetUTF-8、MySQL 表或连接字符集不是 utf8mb4。按页面、响应、数据库三层顺序排查大多数问题出在第一层。5.3 答辩前的验证清单场景操作预期结果未登录访问直接访问 /admin/student_list.jsp跳转登录页学生访问管理页用 role2 账号访问 /admin/返回 403学号重复插入已存在学号提示学号已存在成绩聚合造 5 条成绩验证及格率与手算结果一致SQL 注入尝试搜索框输入 OR 11返回空结果或不报错中文搜索按姓名搜张结果正确且无乱码权限边界测试值得单独强调很多系统只在页面菜单层隐藏了入口直接输入 URL 依然能访问。评分老师通常会点开权限分级管理这个功能点验证学生账号能不能访问教师接口所以过滤器层拦截是必须的不能只靠前端藏按钮。答辩前用不同角色各登录一次把表格里的场景手动过一遍发现的问题记在纸上当场改比对着文档空想要有效得多。本文还有配套的精品资源点击获取