Java Web学生信息管理系统实战:从数据库设计到Servlet分页与权限拦截
发布时间:2026/9/29 1:45:30来源:尧图网络
简介这份资源是面向计算机专业学生与Java Web初学者的一套学生信息管理系统完整项目源码对应课程设计、实训作业或毕业设计等场景帮助读者理解如何用Java技术栈搭建一个可运行的信息管理应用。压缩包为rar格式整体约1.21MB包内以Java源文件、JSP页面及数据库脚本等类型为主分别承担后端业务逻辑、前端数据展示与数据表结构定义等职责。目前已有3006人学习下载具备一定的参考热度。系统围绕学生信息的增删改查展开涵盖登录验证、Session身份识别、Servlet请求处理、JDBC数据库连接以及PreparedStatement预编译防注入等关键环节数据库表设计涉及学号、姓名、性别、出生日期、专业等字段并兼顾主键、索引与密码加密存储等安全考量。读者可据此梳理Java Web项目的分层结构与开发流程对照源码理解Servlet、JSP与JDBC的协作方式为课程实践与技能提升提供可复用的参考方案。1. 从一张成绩单说起Java Web 学生信息管理系统到底在管什么教务老师每学期末最怕什么不是考试安排是学生成绩、学籍异动、班级花名册散落在七八个 Excel 里改一处要同步三处查一个学生的历年成绩得翻五个文件。基于 Java Web 的学生信息管理系统本质上就是把这堆表格收进一个浏览器能打开的页面里让辅导员、教务、任课老师各看各的数据各干各的活。它不新鲜但每年毕业设计和课程设计里出现频率极高原因也简单业务边界清楚、技术栈成熟、能跑通增删改查就能交差。可真正动手写的时候很多人卡在“表怎么设计”“权限怎么分”“分页查询为什么慢”这些具体问题上。这篇笔记就按一个能上线的学生信息管理系统来拆从数据库表结构到 Servlet 跳转再到部署排错把能抄的代码和能踩的坑都摆出来。2. 技术选型与数据库表设计别急着写 Servlet先把实体关系理清2.1 为什么是 JSP Servlet JDBC而不是一上来就 Spring Boot打开招聘软件搜 Java WebSpring Boot 铺天盖地但学生信息管理系统这个题目有个现实约束很多学校的实验环境只装了 Tomcat 和 JDK连 Maven 仓库都连不上。我一般会建议先用 JSP Servlet JDBC 把业务跑通原因有三。第一Servlet 是 Java Web 的根请求怎么进来、响应怎么出去、Session 怎么维持这些在 Spring 里被封装成注解出了问题只能靠猜而在原生 Servlet 里每一步都看得见。第二学生信息管理系统的核心操作是表单提交和列表展示JSP 的 EL 表达式和 JSTL 标签足够应付不需要前后端分离那一套。第三调试成本低改完 JSP 直接刷新浏览器不用等热部署。当然这不是说 Spring Boot 不能用。如果你的环境允许用 Spring Boot MyBatis Thymeleaf 会更省事尤其是事务管理和连接池这块。但本文的代码示例以原生 Servlet 为主因为它的报错信息最直白适合用来理解“请求从浏览器到数据库再回来”的完整链路。选型确定后第一件事不是建工程是画 ER 图。学生信息管理系统里最核心的实体就四个学生、班级、课程、成绩。它们之间的关系是一个班级有多个学生一个学生选多门课每门课对应一条成绩记录。很多新手上来就建一张大表把学生姓名、班级名、课程名、分数全塞进去结果改一个班级名要更新几十行这就是典型的没做范式化。2.2 学生课程成绩信息实体表设计五张表撑起整个系统下面这套表结构是我在多个课程设计里反复用过的字段不多但覆盖了增删改查和统计需求。注意student_no和course_no都加了唯一索引因为学号和课程号是业务主键不能重复。-- 班级表一个班级对应多个学生 CREATE TABLE class ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL COMMENT 班级名称如计算机2101, grade_year INT NOT NULL COMMENT 入学年份, counselor VARCHAR(20) COMMENT 辅导员姓名 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学生表学号唯一外键关联班级 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, name VARCHAR(20) NOT NULL, gender CHAR(1) DEFAULT 男, birth_date DATE, class_id INT, phone VARCHAR(15), FOREIGN KEY (class_id) REFERENCES class(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表课程号唯一 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, course_no VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, course_name VARCHAR(50) NOT NULL, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, teacher VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表学生和课程的多对多关系分数保留一位小数 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(4,1) COMMENT 0-100分, exam_date DATE, UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表登录用角色区分管理员和普通教师 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT 存MD5值不存明文, role VARCHAR(10) DEFAULT teacher COMMENT admin或teacher, real_name VARCHAR(20) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;建完表后往sys_user里插一条管理员记录密码用 MD5 加密。下面这段 Java 代码用来生成 MD5 值跑一次拿到结果直接写进 SQL 就行。import java.security.MessageDigest; public class MD5Util { public static String md5(String input) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(input.getBytes(UTF-8)); StringBuilder sb new StringBuilder(); for (byte b : digest) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException(MD5计算失败, e); } } public static void main(String[] args) { // 把 admin123 转成 MD5插入数据库时用这个值 System.out.println(md5(admin123)); } }参数说明MessageDigest.getInstance(MD5)指定算法digest返回 16 字节数组%02x保证每个字节转成两位十六进制。注意 MD5 现在不安全但学生系统里用来避免明文存储足够了真要上生产得换 BCrypt。2.3 连接池配置为什么你的系统跑十分钟就卡死很多课程设计里直接用DriverManager.getConnection()拿连接用完close()。这在单用户测试时没问题但一旦多人同时访问每个请求都新建 TCP 连接数据库很快耗尽连接数表现就是页面转圈然后报Too many connections。正确做法是用连接池Tomcat 自带 DBCP配置在META-INF/context.xml里。Context Resource namejdbc/studentDB authContainer typejavax.sql.DataSource maxTotal20 maxIdle10 maxWaitMillis10000 usernameroot passwordyourpassword driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/student_db?useSSLfalseamp;serverTimezoneAsia/Shanghaiamp;characterEncodingutf8/ /Context然后在web.xml里引用这个资源Servlet 里通过InitialContext查找。maxTotal20表示最多 20 个活跃连接maxWaitMillis10000表示拿不到连接时等 10 秒就抛异常避免请求无限挂起。这个配置在 50 人同时在线的小系统里够用了如果班级规模大把maxTotal调到 50同时观察 MySQL 的max_connections参数别超。3. 用 Servlet JSP 跑通学生列表分页从请求到渲染的完整链路3.1 分页查询的 SQL 写法与 Servlet 参数接收学生列表是使用频率最高的页面一个年级几百人不分页的话页面加载慢数据库也吃力。分页的核心是LIMIT offset, size但 offset 不能写死要根据当前页码算。下面这个StudentListServlet接收page和keyword两个参数keyword用来按姓名或学号模糊搜索。WebServlet(/student/list) public class StudentListServlet extends HttpServlet { private static final int PAGE_SIZE 10; protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 接收页码默认第一页 String pageStr request.getParameter(page); int page (pageStr null || pageStr.isEmpty()) ? 1 : Integer.parseInt(pageStr); // 2. 接收搜索关键词用于姓名或学号模糊匹配 String keyword request.getParameter(keyword); if (keyword null) keyword ; Connection conn null; PreparedStatement ps null; ResultSet rs null; ListStudent students new ArrayList(); int totalCount 0; try { conn DBUtil.getConnection(); // 3. 先查总数用于计算总页数 String countSql SELECT COUNT(*) FROM student s LEFT JOIN class c ON s.class_id c.id WHERE s.name LIKE ? OR s.student_no LIKE ?; ps conn.prepareStatement(countSql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); rs ps.executeQuery(); if (rs.next()) totalCount rs.getInt(1); rs.close(); ps.close(); // 4. 再查当前页数据关联班级名 String listSql SELECT s.id, s.student_no, s.name, s.gender, c.class_name FROM student s LEFT JOIN class c ON s.class_id c.id WHERE s.name LIKE ? OR s.student_no LIKE ? ORDER BY s.student_no LIMIT ?, ?; ps conn.prepareStatement(listSql); ps.setString(1, % keyword %); ps.setString(2, % keyword %); ps.setInt(3, (page - 1) * PAGE_SIZE); ps.setInt(4, PAGE_SIZE); rs ps.executeQuery(); while (rs.next()) { Student stu new Student(); stu.setId(rs.getInt(id)); stu.setStudentNo(rs.getString(student_no)); stu.setName(rs.getString(name)); stu.setGender(rs.getString(gender)); stu.setClassName(rs.getString(class_name)); students.add(stu); } // 5. 计算总页数向上取整 int totalPages (totalCount PAGE_SIZE - 1) / PAGE_SIZE; request.setAttribute(students, students); request.setAttribute(currentPage, page); request.setAttribute(totalPages, totalPages); request.setAttribute(keyword, keyword); request.getRequestDispatcher(/WEB-INF/views/student_list.jsp).forward(request, response); } catch (SQLException e) { throw new ServletException(查询学生列表失败, e); } finally { DBUtil.close(conn, ps, rs); } } }逻辑说明先查COUNT(*)拿到总记录数再查当前页数据两次查询用同一个连接。(page - 1) * PAGE_SIZE是偏移量第一页 offset 为 0第二页为 10以此类推。totalPages用(totalCount PAGE_SIZE - 1) / PAGE_SIZE计算避免浮点数除法。参数keyword前后拼%实现模糊匹配注意这里用了PreparedStatement能防 SQL 注入。3.2 JSP 页面渲染与分页条实现Servlet 把数据放到 request 域后转发到 JSPJSP 里用 JSTL 遍历。分页条要显示上一页、下一页和页码当前页高亮。下面这段 JSP 放在/WEB-INF/views/student_list.jsp注意WEB-INF下的页面不能直接通过浏览器访问必须走 Servlet 转发这是为了安全。% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表/title style table { border-collapse: collapse; width: 80%; margin: 20px auto; } th, td { border: 1px solid #ccc; padding: 8px; text-align: center; } .pagination { text-align: center; margin: 20px; } .pagination a, .pagination span { margin: 0 5px; padding: 4px 10px; border: 1px solid #ddd; } .pagination .current { background: #4a90d9; color: #fff; } /style /head body form action${pageContext.request.contextPath}/student/list methodget input typetext namekeyword value${keyword} placeholder输入姓名或学号/ input typesubmit value搜索/ /form table tr th学号/thth姓名/thth性别/thth班级/thth操作/th /tr c:forEach items${students} varstu tr td${stu.studentNo}/td td${stu.name}/td td${stu.gender}/td td${stu.className}/td td a href${pageContext.request.contextPath}/student/edit?id${stu.id}编辑/a a href${pageContext.request.contextPath}/student/delete?id${stu.id} onclickreturn confirm(确认删除)删除/a /td /tr /c:forEach /table div classpagination c:if test${currentPage 1} a href?page${currentPage - 1}keyword${keyword}上一页/a /c:if c:forEach begin1 end${totalPages} varp c:choose c:when test${p currentPage} span classcurrent${p}/span /c:when c:otherwise a href?page${p}keyword${keyword}${p}/a /c:otherwise /c:choose /c:forEach c:if test${currentPage totalPages} a href?page${currentPage 1}keyword${keyword}下一页/a /c:if /div /body /html参数说明${pageContext.request.contextPath}拿到应用上下文路径避免部署到不同目录时链接失效。分页链接里带上keyword这样翻页时搜索条件不丢失。onclickreturn confirm(...)是删除前的二次确认防止误操作。3.3 中文乱码的三种场景与对应解法中文乱码是 Java Web 的经典翻车点分三种情况。第一种是表单 POST 提交请求体编码不对表现为插入数据库后变成问号。解法是在 Servlet 里第一行加request.setCharacterEncoding(UTF-8)或者用 Filter 统一处理。第二种是 GET 请求 URL 参数乱码Tomcat 8 以后默认 URI 编码是 UTF-8但老版本需要在server.xml的 Connector 上加URIEncodingUTF-8。第三种是响应输出乱码页面显示问号解法是response.setContentType(text/html;charsetUTF-8)。我一般会写一个CharacterEncodingFilter在web.xml里配成/*一劳永逸。WebFilter(/*) public class CharacterEncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }注意这个 Filter 要放在其他 Filter 之前否则编码设置会被覆盖。如果用了 Spring 的CharacterEncodingFilter确保forceEncoding设为true。4. 登录鉴权与权限拦截别让普通老师看到管理员菜单4.1 Session 存用户信息与登录 Servlet 实现学生信息管理系统里管理员能增删改所有数据普通老师只能查成绩和录入自己课的成绩。这个区分靠 Session 里的role字段。登录成功后把user对象放进session后续每个受保护的 Servlet 先检查session.getAttribute(user)是否为空为空就跳登录页。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password MD5Util.md5(request.getParameter(password)); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement( SELECT id, username, role, real_name FROM sys_user WHERE username? AND password?)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { User user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRole(rs.getString(role)); user.setRealName(rs.getString(real_name)); request.getSession().setAttribute(user, user); response.sendRedirect(request.getContextPath() /student/list); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(/login.jsp).forward(request, response); } } } catch (SQLException e) { throw new ServletException(登录查询失败, e); } } }参数说明密码在前端传过来是明文这里先 MD5 再比对数据库里的 MD5 值。request.getSession()默认创建新 Session如果之前有旧 Session 会被复用。登录成功后重定向到学生列表避免刷新页面重复提交表单。4.2 用 Filter 做统一权限拦截每个 Servlet 里都写一遍 Session 检查太啰嗦用 Filter 拦截/student/*、/score/*等路径统一判断。下面这个AuthFilter还做了角色判断删除操作只允许admin。WebFilter(urlPatterns {/student/*, /score/*, /class/*}) public class AuthFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); // 1. 未登录跳转登录页 if (session null || session.getAttribute(user) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } User user (User) session.getAttribute(user); String uri request.getRequestURI(); // 2. 删除操作仅管理员可执行 if (uri.contains(/delete) !admin.equals(user.getRole())) { response.sendError(403, 无权限执行删除操作); return; } chain.doFilter(req, resp); } }注意request.getSession(false)表示不新建 Session只取已有的。如果写成true或空参每个请求都会创建 Session浪费内存。sendError(403)会返回 Tomcat 的错误页如果想自定义可以在web.xml里配error-page。4.3 密码存储与 SQL 注入的两个硬性要求密码不能明文存这是底线。MD5 虽然弱但比明文强至少数据库被拖库时不会直接暴露。更好的做法是 BCrypt加盐哈希但需要引入jbcrypt依赖。如果环境不允许MD5 加固定盐值也能凑合比如md5(password student_sys_2024)。SQL 注入方面所有带参数的查询必须用PreparedStatement不要用字符串拼接。下面这两种写法第一种是安全的第二种一搜 OR 11就绕过登录。// 安全PreparedStatement 预编译 ps conn.prepareStatement(SELECT * FROM sys_user WHERE username? AND password?); ps.setString(1, username); ps.setString(2, password); // 危险字符串拼接别用 String sql SELECT * FROM sys_user WHERE username username AND password password ;PreparedStatement会把参数当成数据而不是 SQL 语法即使用户输入 OR 11也会被当成普通字符串匹配查不到记录。5. 避坑与排查部署到 Tomcat 后最容易翻车的五个地方5.1 现象页面报 404但路径明明是对的原因JSP 放在WEB-INF下浏览器直接访问/WEB-INF/views/student_list.jsp会被 Tomcat 拒绝返回 404。这是 Servlet 规范的安全限制WEB-INF下的资源只能通过 Servlet 转发访问。解决所有页面跳转走 Servlet用request.getRequestDispatcher(/WEB-INF/views/xxx.jsp).forward(...)。如果确实需要直接访问把 JSP 移到WebContent根目录下但这样会暴露源码逻辑不推荐。5.2 现象数据库连接报No suitable driver found原因MySQL 驱动 jar 没放到WEB-INF/lib下或者driverClassName写错。MySQL 8 的驱动类是com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver。解决把mysql-connector-java-8.0.xx.jar复制到WEB-INF/lib检查context.xml里的driverClassName和url。URL 里要加serverTimezoneAsia/Shanghai否则 MySQL 8 会报时区错误。5.3 现象分页查询第二页数据重复或丢失原因ORDER BY的字段有重复值比如按class_id排序同一个班的学生顺序不稳定MySQL 每次返回的顺序可能不同导致分页错乱。解决ORDER BY后面加一个唯一字段比如ORDER BY s.student_no或ORDER BY s.id。学号和 id 都是唯一的能保证排序稳定。5.4 现象删除学生时报外键约束错误原因score表里有该学生的成绩记录直接删student表会触发外键约束报Cannot delete or update a parent row。解决先删score里对应的记录再删student。或者在建表时把外键设为ON DELETE CASCADE但这样会连带删除成绩要确认业务上是否允许。我一般会在删除前弹窗提示“该学生有成绩记录确认一并删除”确认后在一个事务里先删成绩再删学生。5.5 现象Tomcat 启动慢日志里刷SEVERE: Unable to process Jar entry原因WEB-INF/lib下放了重复或冲突的 jar比如同时有servlet-api.jar和 Tomcat 自带的servlet-api.jar类加载器扫描时冲突。解决servlet-api.jar和jsp-api.jar不要放进WEB-INF/libTomcat 已经提供了。只放 MySQL 驱动、JSTL、连接池等业务依赖。用 Maven 的话把这两个依赖的scope设为provided。6. 进阶技巧用一条 SQL 查出班级成绩排名与及格率系统跑通后教务最常问的是“哪个班平均分最高”“这门课及格率多少”。这些统计不需要在 Java 里循环算一条 SQL 就能出结果。下面这条查询按班级统计每门课的平均分、最高分、最低分和及格率直接返回给 JSP 渲染成报表。SELECT c.class_name AS 班级, co.course_name AS 课程, COUNT(*) AS 人数, ROUND(AVG(sc.score), 1) AS 平均分, MAX(sc.score) AS 最高分, MIN(sc.score) AS 最低分, CONCAT(ROUND(SUM(CASE WHEN sc.score 60 THEN 1 ELSE 0 END) * 100.0 / COUNT(*), 1), %) AS 及格率 FROM score sc JOIN student s ON sc.student_id s.id JOIN class c ON s.class_id c.id JOIN course co ON sc.course_id co.id GROUP BY c.class_name, co.course_name ORDER BY c.class_name, 平均分 DESC;参数说明CASE WHEN sc.score 60 THEN 1 ELSE 0 END把及格记录标记为 1SUM后除以总人数得到及格率。ROUND(..., 1)保留一位小数。GROUP BY同时按班级和课程分组ORDER BY先按班级再按平均分降序方便一眼看出哪个班哪门课考得好。如果要在 JSP 里展示Servlet 里把ResultSet转成ListMapString, ObjectJSP 用c:forEach遍历即可。注意Map的 key 是列别名中文别名在 JDBC 里可能乱码建议用英文别名再在 JSP 里映射成中文表头。我自己的习惯是统计类查询一律不写进 Java 循环能交给 SQL 就交给 SQL。数据库的聚合函数比 Java 快一个数量级而且代码短维护起来不容易出错。唯一要注意的是GROUP BY的字段和SELECT里的非聚合字段必须一致MySQL 8 默认开启了ONLY_FULL_GROUP_BY不一致会直接报错这其实是好事能帮你发现逻辑漏洞。最后说一个我踩过的坑统计查询不要和分页查询共用同一个Connection因为ResultSet没关闭前同一个连接上再执行查询会报Streaming result set错误。要么用两个连接要么先把统计结果读进内存再关ResultSet。这个错误在本地测试时不容易出现一上压力测试就暴露血泪经验。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网