基于JSP与Servlet的学生信息管理系统:分层架构与请求链路实战
简介:这是一份面向Java Web初学者与课程实践者的学生信息管理系统完整项目源码,以JSP与Servlet为核心技术栈,配合MVC分层思路实现学生与班级信息的增删改查、登录验证等功能,适合用于课程设计、毕业设计参考或Java Web入门练手。压缩包共576个文件,约2.75MB,包含156个html、132个css、105个png、79个js等前端页面与静态资源,20个java源文件及对应class文件承载Servlet与DAO业务逻辑,另有jsp页面、sql初始化脚本、jar依赖与项目配置文件,结构完整可直接导入容器运行。已有999人学习下载。项目覆盖JSP动态页面、Servlet请求处理、JDBC数据库操作、HTTP请求响应流程、会话管理与Tomcat部署等知识点,并附数据库建表脚本,便于读者对照源码理解MVC各层职责、梳理请求流转路径,是快速上手Java Web开发流程的实践案例。
1. 从一份“能跑但不敢交”的课设说起
每年到了课程设计交付季,总有一批学生信息管理系统的项目卡在同一个地方:功能列表写得满满当当,增删改查都能点,但一问到“请求是怎么从浏览器走到数据库的”“会话是怎么保住的”“并发改同一条记录会不会互相覆盖”,就答不上来。这套基于 JSP 与 Servlet 的学生信息管理系统,解决的正是这个问题——它不追求花哨的前端框架,而是把 Java Web 最核心的请求响应链路、会话管理、分层结构和数据库访问讲透,让读者交得出一份结构完整、经得起追问的方案。适合正在做 Java Web 课程设计、毕业设计选题偏管理系统方向的学生,也适合需要一份可参考的 Servlet 分层落地模板的初级开发者。下面按需求、设计、实现、避坑、验收的顺序展开。
2. 需求与选型:为什么这套系统坚持用 JSP + Servlet 打底
2.1 先把角色和用例钉死,再谈技术
学生信息管理系统的需求看起来简单,真正落到设计上,第一件要做的事是把角色和用例钉死,而不是急着建表。通常的做法是划分三类角色:管理员、教师、学生。管理员负责账号与班级的基础数据维护,教师负责成绩录入与查询,学生只能查看自己的信息与成绩。用例层面,核心是学生档案的增删改查、班级与院系的关联维护、成绩的录入与统计、登录鉴权与权限隔离。
这里有个容易被忽略的点:很多课设把“权限”做成登录后所有页面都能访问,只是菜单不同。这在答辩时是硬伤。合格的负责人会把权限校验下沉到 Servlet 层,每个受保护的请求都过一次会话与角色判断,而不是靠前端隐藏按钮。需求阶段就要明确哪些 URL 属于哪个角色可访问,形成一张权限矩阵,后面写过滤器时直接照着矩阵实现。
需求文档里还要写清非功能约束:单机部署、MySQL 存储、支持几十人并发访问、页面响应在两秒内。这些约束决定了后面的技术选型不能太重,也不需要引入分布式组件。
2.2 技术选型:Servlet 做控制,JSP 做视图,JDBC 做持久
选型上,控制层用 Servlet,视图层用 JSP 配合 JSTL 标签,持久层用原生 JDBC 加手写 DAO,数据库用 MySQL。这套组合的理由很实在:Servlet 是 Java Web 的规范底座,请求映射、生命周期、会话管理都是标准能力,答辩时能讲清楚service方法里发生了什么;JSP 虽然老,但配合 JSTL 的c:forEach、c:if能把列表渲染讲明白,且不需要额外构建工具;JDBC 手写 SQL 让读者真正理解连接、语句、结果集三件套,而不是被 ORM 屏蔽掉细节。
如果换成 Spring Boot 加 MyBatis,功能一样能实现,但课设的考察点往往在“你是否理解请求链路”,框架会把这条链路藏起来。用 Servlet 打底,等于把黑匣子拆开给你看。当然,代价是要自己处理连接释放、字符编码、SQL 注入防护这些琐事,这恰恰是后面避坑章节要重点讲的。
2.3 工程结构:按层分包,别按页面分包
工程目录按层划分,而不是按页面划分。常见的错误是建一堆student_add.jsp、student_list.jsp平铺在根目录,Servlet 也堆在一个包里。正确的做法是按职责分包:
src/ com.school.sims controller/ StudentServlet, ScoreServlet, LoginServlet service/ StudentService, ScoreService dao/ StudentDao, ScoreDao, BaseDao entity/ Student, Score, User filter/ EncodingFilter, AuthFilter util/ DBUtil, PageUtil WebContent/ WEB-INF/ views/ student_list.jsp, student_edit.jsp web.xml static/ css, jsWEB-INF/views下的 JSP 不能被浏览器直接访问,必须经 Servlet 转发,这样视图层就被保护起来了。BaseDao放公共的连接获取与关闭逻辑,子类 DAO 只写各自的 SQL。这个结构在答辩时一眼就能看出分层意识,也是后面所有实现的地基。
3. 总体设计:四层结构与请求链路的职责划分
3.1 分层职责与一次请求的完整走向
系统分四层:视图层、控制层、业务层、持久层。一次“查询学生列表”的请求走向是这样的:浏览器发起GET /student?action=list,EncodingFilter先统一设置请求与响应编码,AuthFilter校验会话中是否有登录用户且角色允许,通过后进入StudentServlet的doGet,根据action参数分发到list方法,方法调用StudentService.queryByPage,Service 做参数校验与分页计算后调用StudentDao.findPage,DAO 拼 SQL、取连接、执行、封装成List<Student>返回,Servlet 把结果放进request属性,forward到student_list.jsp,JSP 用 JSTL 遍历渲染成表格。
这条链路里每一层只做自己的事:Servlet 不写 SQL,DAO 不碰HttpServletRequest,Service 不关心页面长什么样。职责一旦串味,后面改一个字段要动三四个文件,维护成本立刻上来。我一般会在设计文档里画一张分层职责表,把每层的输入、输出、禁止事项写清楚,评审时对着表逐条过。
| 层 | 输入 | 输出 | 禁止事项 |
|---|---|---|---|
| 视图层 JSP | request/session 属性 | HTML | 写 Java 业务逻辑、连数据库 |
| 控制层 Servlet | HTTP 请求参数 | 转发或重定向 | 拼 SQL、做复杂计算 |
| 业务层 Service | 实体或参数 | 实体或结果集 | 直接操作 request |
| 持久层 DAO | 查询条件 | 实体列表 | 处理页面跳转 |
3.2 数据库设计:三张主表加两张关联表
数据库设计围绕学生、班级、成绩、用户、课程展开。学生表存学号、姓名、性别、班级外键等;班级表存班级名与院系;成绩表存学生外键、课程外键、分数;用户表存登录名、密码哈希、角色。学生与班级是多对一,学生与课程通过成绩表形成多对多。建表时主键用自增整型,学号加唯一索引,外键加索引避免联表查询全表扫描。
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL, gender TINYINT DEFAULT 0, class_id INT, INDEX idx_class (class_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,2), UNIQUE KEY uk_stu_course (student_id, course_id), INDEX idx_student (student_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;uk_stu_course这个唯一键很关键,它从数据库层面防止同一学生同一课程被录入两条成绩,比在代码里查一遍再插入可靠得多。字符集统一用utf8mb4,避免姓名里出现生僻字或特殊符号时乱码。分数用DECIMAL而不是FLOAT,避免浮点误差导致统计对不上。
3.3 会话与权限模型:过滤器兜底,矩阵驱动
登录成功后把用户对象放进HttpSession,键名统一为loginUser。权限校验不写在每个 Servlet 里,而是由AuthFilter统一拦截。过滤器里维护一张“URL 前缀到允许角色”的映射,比如/student允许管理员和教师,/score允许教师和管理员,/profile允许学生。请求进来先取会话,没有登录用户就重定向到登录页,有用户但角色不匹配就返回 403 页面。
会话超时在web.xml里配置,通常设 30 分钟。这里有个细节:过滤器放行静态资源和登录页,否则登录页自己都被拦掉,形成死循环。权限矩阵要写进设计文档,答辩时能直接指着矩阵说“这条规则对应过滤器里哪一行”,比空谈“做了权限控制”有说服力得多。
4. 核心模块实现:从登录鉴权到分页查询的落地代码
4.1 登录鉴权:密码哈希与会话写入
登录模块的 Servlet 接收用户名密码,调用 Service 校验,校验通过后把用户写入会话并重定向到首页。密码不能明文存库,注册或初始化时用 SHA-256 加盐哈希。校验时对输入密码做同样处理再比对。
// LoginServlet.doPost String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userService.login(username, password); if (user == null) { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("/WEB-INF/views/login.jsp").forward(request, response); return; } request.getSession().setAttribute("loginUser", user); response.sendRedirect(request.getContextPath() + "/student?action=list");userService.login内部先按用户名查用户,查不到直接返回 null,查到后对输入密码加盐哈希再与库中值比对。注意这里不要分别提示“用户不存在”和“密码错误”,统一提示能减少账号枚举风险。会话写入用setAttribute,重定向用sendRedirect而不是转发,避免刷新页面重复提交登录表单。这套写法在课设里足够,答辩时能讲清“为什么用重定向”就是加分项。
4.2 学生信息 CRUD:Servlet 分发与 DAO 参数化
学生模块用一个 Servlet 处理多个动作,通过action参数分发。新增和修改共用一个表单页,靠是否有id区分。DAO 层全部使用PreparedStatement参数化,杜绝字符串拼接 SQL。
// StudentDao.findPage public List<Student> findPage(String keyword, int offset, int limit) throws SQLException { String sql = "SELECT s.*, c.class_name FROM student s " + "LEFT JOIN clazz c ON s.class_id = c.id " + "WHERE s.name LIKE ? OR s.sno LIKE ? " + "ORDER BY s.id DESC LIMIT ?, ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String like = "%" + keyword + "%"; ps.setString(1, like); ps.setString(2, like); ps.setInt(3, offset); ps.setInt(4, limit); try (ResultSet rs = ps.executeQuery()) { List<Student> list = new ArrayList<>(); while (rs.next()) { list.add(mapRow(rs)); } return list; } } }try-with-resources保证连接、语句、结果集按逆序自动关闭,这是 JDBC 里最容易翻车的地方,后面避坑章节会展开。LIKE的通配符拼在 Java 侧而不是 SQL 里,参数仍然走占位符,注入风险被挡住。LEFT JOIN让没有班级的学生也能查出来,class_name为空时页面显示“未分班”。分页的offset和limit由 Service 根据页码算好传入,DAO 不关心页码语义。
4.3 分页查询与列表渲染:PageUtil 与 JSTL 遍历
分页逻辑抽成PageUtil,持有当前页、每页条数、总记录数,算出总页数和偏移量。Service 先查总数再查当前页数据,两个结果一起返回给 Servlet。
public class PageUtil<T> { private int pageNo = 1; private int pageSize = 10; private int total; private List<T> rows; public int getOffset() { return (pageNo - 1) * pageSize; } public int getTotalPages() { return (total + pageSize - 1) / pageSize; } // getter/setter 省略 }JSP 侧用 JSTL 遍历page.rows,底部渲染页码链接,链接带上keyword和pageNo参数,保证翻页时搜索条件不丢。这里常见的坑是页码越界:用户手动改 URL 里的pageNo为负数或超过总页数,Service 里要做钳制,pageNo小于 1 取 1,大于总页数取总页数。列表页还要处理空结果,显示“暂无数据”而不是一张空表格,这些细节在验收演示时很显眼。
4.4 成绩录入与统计:事务与聚合查询
成绩录入涉及“先判断是否已存在,再插入或更新”,两步操作要放在一个事务里,否则并发下可能插出重复记录。Service 层手动管理事务:关闭自动提交,执行完提交,异常回滚。
public void saveScore(int studentId, int courseId, BigDecimal score) throws SQLException { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); scoreDao.upsert(conn, studentId, courseId, score); conn.commit(); } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }upsert用INSERT ... ON DUPLICATE KEY UPDATE依赖前面建的唯一键,一条语句搞定插入或更新,比先查后写少一次往返。统计查询用AVG、MAX、MIN加GROUP BY course_id算出每门课的平均分与最高最低分,结果封装成统计对象传给 JSP 渲染。事务这块答辩常被问“如果提交前断电怎么办”,答案是 InnoDB 的 redo 日志保证已提交的落盘、未提交的回滚,讲到这里基本就稳了。
5. 避坑与排查:这套系统最容易翻车的五个点
5.1 中文乱码:现象是列表姓名变问号
现象:新增学生后列表里姓名显示为???,或者搜索中文关键词查不到数据。原因通常是三处编码不一致:JSP 页面pageEncoding、请求体编码、数据库连接串的characterEncoding。解决是把它们统一成 UTF-8:JSP 顶部声明<%@ page contentType="text/html;charset=UTF-8" %>,EncodingFilter里对request和response都设 UTF-8,JDBC URL 加characterEncoding=utf8,建库建表用utf8mb4。GET 请求的参数编码还依赖容器配置,必要时在连接器上显式指定。
5.2 连接泄漏:现象是跑一会儿就报连接超时
现象:系统用一段时间后所有数据库操作卡住,日志里出现连接池耗尽或Too many connections。原因多半是 DAO 里Connection、PreparedStatement、ResultSet没有在异常路径上关闭,或者只关了连接没关语句。解决是统一用try-with-resources,把资源声明在 try 的括号里,异常时也能自动关闭。排查时可以在DBUtil里加计数,每次获取加一、关闭减一,跑一轮压测看计数是否归零,归不了零就说明有泄漏点。
5.3 SQL 注入:现象是搜索框输入引号就报错
现象:搜索框输入'导致 SQL 语法错误,或者输入' OR '1'='1能查出全部数据。原因是用了字符串拼接 SQL。解决是所有用户输入一律走PreparedStatement占位符,LIKE的通配符在 Java 侧拼好再setString。排序字段这种不能参数化的地方,用白名单校验,只允许预设的几个列名,其余一律拒绝。这条在答辩时几乎是必问项,能当场演示注入被挡住,比口头保证有用。
5.4 会话失效与重复提交:现象是刷新页面重复新增
现象:新增表单提交后按 F5 刷新,同一条学生记录被插入两次;或者登录后过一会儿操作被踢回登录页。前者是转发导致的表单重复提交,解决是新增成功后用sendRedirect重定向到列表页,形成 PRG 模式。后者是会话超时,属于正常行为,但要在过滤器里区分“未登录”和“会话过期”,给用户友好提示而不是直接 500。涉及金额或成绩的写操作,还可以在会话里放一次性令牌,提交后校验并作废。
5.5 分页与排序错乱:现象是翻页后条件丢失
现象:搜索“张”之后翻到第二页,结果又变回全部学生;或者按学号排序后翻页顺序乱掉。原因是分页链接没带搜索参数,或者 SQL 的ORDER BY字段不唯一导致同值行顺序不稳定。解决是分页链接统一由标签拼接,把当前查询条件作为参数带上;排序时在ORDER BY末尾追加主键id作为兜底,保证顺序确定。这类问题在演示时不一定暴露,但数据量一上来就明显,属于典型的血泪经验。
6. 验收与答辩:怎么证明这套系统真的能交付
验收分三步走。第一步是功能清单逐项过:登录鉴权、学生增删改查、分页搜索、成绩录入与统计、权限隔离,每项准备一条正常路径和一条异常路径,比如删除不存在的学生要给出提示而不是白屏。第二步是数据一致性验证:并发提交同一学生的同一课程成绩,确认唯一键生效只留一条;手动改 URL 越权访问,确认被过滤器拦下返回 403。第三步是代码走查:随机抽一个 DAO 方法,看是否参数化、是否try-with-resources、Service 是否有事务边界。
答辩时我一般建议准备一张“请求链路图”和一张“权限矩阵表”,被问到“这个请求怎么走的”就顺着图讲一遍,被问到“权限怎么做的”就指着矩阵说对应过滤器规则。演示环节控制在五分钟内,先跑主流程再演示一个异常场景,比如输入注入字符串被挡住,比堆功能列表更能体现理解深度。常见追问还有“会话存在哪”“事务怎么保证”“分页总数怎么查”,提前把这三问的答案写进讲稿。
| 验收项 | 通过标准 | 验证方式 |
|---|---|---|
| 登录鉴权 | 未登录访问受保护页跳登录 | 清 Cookie 后直接访问列表 URL |
| 权限隔离 | 学生访问成绩录入返回 403 | 用学生账号改 URL 访问 |
| 参数化查询 | 注入字符串不改变结果集 | 搜索框输入' OR '1'='1 |
| 事务一致性 | 重复录入只留一条成绩 | 并发提交同一学生课程 |
| 资源释放 | 压测后连接计数归零 | DBUtil 计数日志 |
从那以后我每次带这类课设,都会在交付前强制走一遍“清会话、改 URL、输引号、刷页面”这四步,四步都过才敢让学生去答辩。这套动作花不了十分钟,却能挡掉大部分现场翻车。希望帮到你。
本文还有配套的精品资源,点击获取