
简介这是一份面向Java Web初学者的课程设计资源基于JSPServletMysql实现一套简单的学生信息管理系统学生可自行注册登记、维护个人信息教师登录后可对学生信息进行增删改查与统一管理功能覆盖常见课设要求适用于高校Java Web课程设计、毕业设计前的入门练手。项目集成Layui与jQuery搭建交互界面代码分层清晰运行环境为JDK7Tomcat8MySQL并支持IntelliJ IDEA直接导入数据库脚本及jar包配套齐全。资源包共2个文件包含1个ZIP源码压缩包和1个HTM说明文档总大小仅2.13MB其中ZIP内附完整项目代码、lib目录下的所需jar包数据库文件则通过文内百度网盘链接另行提供整体结构紧凑、无冗余内容。目前已有6435人学习下载对于需要快速搭建小型Web管理系统的同学而言这份可直接运行、便于二次开发的源码值得参考。1. 能在答辩现场讲明白的JSPServlet学生管理系统到底该怎么搭期末周在某个高校实验室A同学攥着一份课程设计任务书发呆要求做一个基于Java Web的学生信息管理系统技术栈限定JSPServletMySQL。他没有急着写代码而是先问了自己一个问题——这东西就算做出来了答辩的时候老师问“你用了什么设计思想”“为什么用连接池”“分页是怎么做的”我能接得住吗带着这个疑问他把这个“简单学生信息管理系统”拆成了需求、设计、实现、验收四段来推进最终交出了一份既能跑通、又能讲清楚的设计方案。这篇博文就是把这套完整方案整理出来给正在做JSPServlet课程设计、毕业设计的同学做参考。无论你是零基础起步还是已经能跑通增删改查但总感觉哪里“不成体系”这篇都能帮你把项目从“能跑”提升到“能答辩”。方案默认读者会一点Java语法、会装IDEA和MySQL不会的地方我也会把操作细节写到命令和配置级别。2. 技术选型与总体架构设计为什么2023年还要用JSPServlet以及三层结构怎么落地2.1 技术栈选型这个“简单”系统该用什么框架技术上很容易头脑发热明明学过Spring Boot为什么作业非要限定JSPServlet先别急着抱怨这门课的教学目标其实是让你理解Web最底层的请求-响应模型。JSP负责视图渲染Servlet负责控制逻辑JDBC负责数据库交互——这正是Java Web最原始的MVC形态。你把它做扎实了后面学Spring MVC时会有一种“原来框架帮我干的就是这些事”的顿悟感。因此这套方案的技术栈定为JDK版本JDK 8最稳妥也能兼容后续的JSP语法Web容器Tomcat 9Java EE规范里的Servlet 4.0JSP 2.3版本数据库MySQL 5.7InnoDB引擎字符集utf8mb4前端JSP JSTL EL表达式少量CSS不引入前端框架连接管理C3P0连接池避免每次请求都用DriverManager创建新连接实际动手前先做一件事在IDEA里建一个Maven项目。别用Dynamic Web Project因为手动拷jar包、手动配web.xml的体验堪称“锻炼耐心”Maven能自动拉取依赖以后你们会遇到的前人踩坑帖基本也都基于Maven结构。2.2 三层架构划分Servlet不在这个层面里是谁的责任很多同学做的管理系统会把JDBC代码直接写在Servlet里页面一多就变成一片混乱。这个项目里我采用了经典的三层架构加控制层总共四个包com.example.entity // 实体类Student, Course, Score, User com.example.dao // 数据访问层StudentDao, UserDao, ScoreDao com.example.service // 业务层StudentService, UserService com.example.servlet // 控制层各个Servlet BaseServlet复用 com.example.util // 工具类DBUtil, PageBean三层架构的意义是分离职责但题目要求了“JSPServlet”所以控制层的Servlet单独一个包JSP放在webapp目录下。请求流程是浏览器发起HTTP请求Servlet接收并解析调用Service层处理业务逻辑Service层调用DAO层做数据库操作最后Servlet把结果setAttribute后转发或重定向到JSPJSP再用EL表达式和JSTL把数据渲染出来。我把这张图直接画给过A同学看JSP页面 → Servlet控制层 → Service业务层 → DAO数据访问层 → MySQL数据库 ↑ | | | └──────────┴──────────────┴───────────────┘ 最终结果返回到JSP渲染这是一种单方向的依赖关系每一层的修改原则上不影响上层。Service层存在的意义在这次的课程设计中可能体现得不够充分因为逻辑简单但它为后续扩展保留了位置这也是答辩时可以主动讲的“设计冗余”。2.3 数据库设计与建表SQL四张表和一个外键约束数据库设计是答辩时最容易考察“你有没有真正理解”的一环。这次系统的业务场景是一个学生信息管理者要维护学生、课程和成绩同时必须有一个用户表支撑登录功能。所以设计四张表student学生表学号是自然主键自带业务含义无需自增IDcourse课程表课程编号为主键来自教学计划的唯一编码score成绩表用自增ID作为逻辑主键(student_id, course_id)做唯一索引防止同一学生同一课程重复录入user用户表存储登录账号密码密码用MD5加固定盐存储杜绝明文建表SQL如下直接复制到Navicat或命令行执行即可CREATE DATABASE IF NOT EXISTS student_ms DEFAULT CHARSET utf8mb4; USE student_ms; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT 男 COMMENT 性别, class_name VARCHAR(50) COMMENT 班级, phone VARCHAR(11) COMMENT 手机号, email VARCHAR(100) COMMENT 邮箱, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB COMMENT 学生表; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程编号, course_name VARCHAR(100) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, teacher VARCHAR(50) COMMENT 授课教师 ) ENGINEInnoDB COMMENT 课程表; CREATE TABLE score ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, score DECIMAL(5,2) COMMENT 成绩, UNIQUE KEY uk_stu_course (student_id, course_id), CONSTRAINT fk_score_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_score_course FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINEInnoDB COMMENT 成绩表; CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL COMMENT MD5加盐后密文, real_name VARCHAR(50) COMMENT 操作员姓名 ) ENGINEInnoDB COMMENT 登录用户表;设计说明为什么学号不用自增ID因为教务系统里的学号本身就是稳定的自然键用它做关联查询可以少一次JOIN。成绩表里的UNIQUE合并索引是很妙的一笔它能防止一条肮脏的数据破坏统计结果外键虽然有时影响性能但在这种小规模管理系统中它换来的数据完整性对验收很有帮助。此外密码别用明文MD5加盐虽然不算强加密但足以表明你具备“不能存明文”的安全意识这在答辩中是个加分项。2.4 包结构与关键工具类DBUtil和统一Servlet的写法在写具体功能之前先把地基打好。第一块地基是数据库连接工具类使用C3P0连接池。连接池为什么会是这套系统的关键优化点因为传统的DriverManager写法是一次请求、一次连接、一次关闭高并发下数据库会很快被拖垮而连接池能让连接重复使用。C3P0的配置写在src/main/resources/c3p0-config.xml中?xml version1.0 encodingUTF-8? c3p0-config default-config property namedriverClasscom.mysql.jdbc.Driver/property property namejdbcUrljdbc:mysql://localhost:3306/student_ms?useSSLfalseamp;characterEncodingutf8/property property nameuserroot/property property namepassword123456/property property nameinitialPoolSize5/property property nameminPoolSize5/property property namemaxPoolSize20/property property namemaxIdleTime60/property /default-config /c3p0-config配套的DBUtil工具类package com.example.util; import com.mchange.v2.c3p0.ComboPooledDataSource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; public class DBUtil { // 私有静态连接池进程内复用不会每次new private static final DataSource dataSource new ComboPooledDataSource(); public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }这里说明两个重点参数initialPoolSize是系统启动时预先创建的连接数设为5意味着第一个请求进来不用等待建立连接maxPoolSize是池子上限超过就排队等待。生产价值的体现集中在释放连接的地方——你的DAO层用完连接后必须在finally块里调用close()Connection对象调close()在连接池模式下实际上是把连接归还给池子不是真正关闭。这一点踩坑率极高稍后避坑章节会展开讲。第二块地基是BaseServlet的设计。原始Servlet开发里一个功能一个Servlet类动不动建十几个类。更优雅的做法是一个模块一个Servlet前端用action参数区分操作。定义一个BaseServlet继承HttpServlet重写service方法根据参数反射调用对应方法package com.example.servlet; import javax.servlet.ServletException; import javax.servlet.http.HttpServlet; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.lang.reflect.Method; public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); if (action null || action.trim().isEmpty()) { resp.sendError(400, 缺少action参数); return; } try { Method method this.getClass().getMethod(action, HttpServletRequest.class, HttpServletResponse.class); String result (String) method.invoke(this, req, resp); if (result ! null) { if (result.startsWith(redirect:)) { resp.sendRedirect(req.getContextPath() result.substring(9)); } else { req.getRequestDispatcher(result).forward(req, resp); } } } catch (NoSuchMethodException e) { resp.sendError(404, 未知的action: action); } catch (Exception e) { throw new ServletException(调用方法异常: action, e); } } }这个设计带来的好处在后面写StudentServlet时就能体会——删改查都集中在同一个类里URL统一为student?actionlist这种形式既整洁又方便权限控制Filter统一拦截。你完全可以把这个BaseServlet的说明写进设计文档作为“可扩展性”论点的论据。3. 核心功能模块实现登录拦截、学生信息CRUD、分页查询与模糊搜索的完整落地3.1 登录模块与拦截器Session会话和Filter过滤器怎么配合登录是所有管理系统的入口也是最检验细节的部分。登录流程由LoginServlet和UserDao共同完成步骤是用户提交JSP表单中的username与passwordServlet从请求中取出参数对密码做MD5加盐哈希处理调用UserDao的findByUsernameAndPassword方法查询数据库查到则setAttribute存入Session重定向到主页面查不到则转发回登录页并显示错误核心代码如下WebServlet(/login) public class LoginServlet extends BaseServlet { private UserDao userDao new UserDao(); public String doLogin(HttpServletRequest req, HttpServletResponse resp) throws Exception { String username req.getParameter(username); String password req.getParameter(password); if (username null || password null || username.trim().isEmpty() || password.trim().isEmpty()) { req.setAttribute(errorMsg, 用户名和密码不能为空); return /login.jsp; } String salted MD5Util.md5(password SALT); User user userDao.findByUsernameAndPassword(username, salted); if (user null) { req.setAttribute(errorMsg, 用户名或密码错误); return /login.jsp; } req.getSession().setAttribute(loginUser, user); return redirect:/index.jsp; } public String logout(HttpServletRequest req, HttpServletResponse resp) throws Exception { req.getSession().invalidate(); return redirect:/login.jsp; } }注意一个细节UserDao.findByUsernameAndPassword里用的SQL要写成SELECT id, username, password, real_name FROM user WHERE username ? AND password ?为什么用PreparedStatement而不直接拼接字符串最直接的理由是SQL注入防护。比如用户在用户名字段输入 OR 11字符串拼接直接改变SQL逻辑而PreparedStatement会把整个输入当作一个字符串参数对待。验证方法很简单你可以在登录页试一次用户名填 OR 11密码随便填拼接SQL的系统会直接瘫痪而PreparedStatement的方案会提示“用户名或密码错误”。接下来是Filter。登录后会访问所有页面如果只靠每个页面检查Session代码会重复倒数。一个登录拦截过滤器解决所有问题LoginFilter检查请求路径下的Session状态WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String path req.getRequestURI(); if (path.endsWith(/login) || path.endsWith(/login.jsp) || path.contains(static)) { chain.doFilter(request, response); return; } if (req.getSession().getAttribute(loginUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }这里有一个隐藏细节Filter配置用/*而JSP里有访问CSS和图片的需求一旦拦截了静态资源页面会瞬间“裸奔”。解决方式是放行含有static关键字的路径所以我在webapp目录里建了一个static文件夹放CSS和JS文件。代码里我写的path.contains(static)逻辑在真实项目中更严谨的做法是专门给静态目录起独立前缀。另外重定向时要带req.getContextPath()这是容器部署路径的前缀不加的话假设你部署在/sms下重定向到/login.jsp会直接404这是血泪经验。3.2 学生信息管理CRUD操作从界面到数据库的完整链路学生模块是管理系统的老大哥功能包括学生列表、新增学生、修改学生、删除学生、条件查询。由于使用了BaseServletStudentServlet只需要专注于业务方法WebServlet(/student) public class StudentServlet extends BaseServlet { private StudentService studentService new StudentService(); // 列表 分页 模糊查询 public String list(HttpServletRequest req, HttpServletResponse resp) throws Exception { String pageNumStr req.getParameter(pageNum); String keyword req.getParameter(keyword); if (keyword null) keyword ; int pageNum 1; try { pageNum Integer.parseInt(pageNumStr); } catch (NumberFormatException e) { /* 保持默认值 */ } PageBeanStudent pageBean studentService.queryPage(pageNum, keyword); req.setAttribute(pageBean, pageBean); req.setAttribute(keyword, keyword); return /student/list.jsp; } public String toAdd(HttpServletRequest req, HttpServletResponse resp) { return /student/add.jsp; } public String add(HttpServletRequest req, HttpServletResponse resp) throws Exception { Student student assembleStudent(req); if (studentService.addStudent(student)) { return redirect:/student?actionlist; } req.setAttribute(errorMsg, 学号已存在或参数不合法); return /student/add.jsp; } private Student assembleStudent(HttpServletRequest req) { Student s new Student(); s.setStudentId(req.getParameter(studentId)); s.setName(req.getParameter(name)); s.setGender(req.getParameter(gender)); s.setClassName(req.getParameter(className)); s.setPhone(req.getParameter(phone)); s.setEmail(req.getParameter(email)); return s; } }纯粹的数据表单向Servlet提交的“新增”有玄学学生的学号来自用户输入而不是自增如果数据库里已经有这个学号插入会抛主键冲突异常。最稳妥是在Service层先通过findById做一次预查询存在就返回false前端提示“重复”。这个预查询放在DAO层的实现也很简单Override public boolean addStudent(Student student) throws SQLException { String checkSql SELECT COUNT(*) FROM student WHERE student_id ?; String insertSql INSERT INTO student (student_id, name, gender, class_name, phone, email) VALUES (?,?,?,?,?,?); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps conn.prepareStatement(checkSql)) { ps.setString(1, student.getStudentId()); ResultSet rs ps.executeQuery(); if (rs.next() rs.getInt(1) 0) { return false; } } try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setString(1, student.getStudentId()); ps.setString(2, student.getName()); ps.setString(3, student.getGender()); ps.setString(4, student.getClassName()); ps.setString(5, student.getPhone()); ps.setString(6, student.getEmail()); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { e.printStackTrace(); // 这里应该回滚 return false; } }注意conn.setAutoCommit(false)这一步它把检查学号是否重复和插入学生这两步操作放进了一个事务保证要么都执行要么都不执行。防止并发请求下多个重复学号在同一瞬间都被插入这种黑匣子问题在课设答辩时讲出来老师会觉得你对“事务隔离性”有概念。界面上JSP列表页核心代码是这样一个表格配合EL表达式和JSTL标签遍历table classtable table-bordered thead tr th学号/thth姓名/thth性别/thth班级/th th手机/thth邮箱/thth操作/th /tr /thead tbody c:forEach items${pageBean.list} varstu tr td${stu.studentId}/td td${stu.name}/td td${stu.gender}/td td${stu.className}/td td${stu.phone}/td td${stu.email}/td td a hrefstudent?actiontoEditstudentId${stu.studentId}编辑/a button classbtn-del>document.querySelectorAll(.btn-del).forEach(btn { btn.addEventListener(click, function () { if (confirm(确定删除该学生吗删除后不可恢复)) { fetch(student?actiondeletestudentId this.dataset.id, { method: POST }) .then(() location.reload()); } }); });3.3 分页查询与模糊搜索PageBean的设计和SQL的LIMIT参数列表数据随着测试数据变多会无限膨胀所以分页是每个管理系统的标配。这套系统的分页绝对值不能漏不然一次把几百行全部渲染出来JSP页面会明显卡顿。我的设计是PageBean通用分页类package com.example.util; import java.util.List; public class PageBeanT { private int pageNum; // 当前页码 private int pageSize 10; // 每页条数 private int totalCount; // 总记录数 private int totalPages; // 总页数 private ListT list; // 当前页数据 private int startRow; // 起始行偏移量SQL限制用 // 构造时自动计算startRow和totalPages }在DAO层的查询方法里模糊查询和分页是紧耦合的。很多同学死记硬背LIMIT offset, pageSize但总淘神在offset该从0开始还是从1开始。正确逻辑是SELECT * FROM student WHERE name LIKE ? OR student_id LIKE ? OR class_name LIKE ? ORDER BY create_time DESC LIMIT ?, ?第一条?传keyword包装成的%关键词%值可以在Java里拼好String likePattern % keyword %;第二条?传startRow值为(pageNum-1) * pageSize第三条传pageSize。因为SQL中LIMIT的偏移量从0开始第一页的startRow就是0第二页就是10由此推导出(pageNum-1)*pageSize。查询总条数与查询当前页数据的SQL可以拆成两次独立查询在计算总页数时加一点防御逻辑totalPages (totalCount pageSize - 1) / pageSize; if (totalPages 1) totalPages 1;这个公式比totalCount / pageSize的向上取整写法更直接totalCount0时也能兜底。在做demo阶段你也许会想“不装分页了”但冲刺答辩时老师大概率会问“数据量上来了怎么办”到那时再补分页重构成本远超现在。3.4 成绩与课程模块多表关联查询时JDBC怎么封装ResultSet成绩模块的业务是某个学生选修了某门课程获得一个分数。页面上的成绩列表往往是多表查询的结果显示学生姓名、课程名称而不是裸ID。这时DAO层的SQL就变成JOINpublic ListScoreVO findScorePage(int offset, int pageSize, String keyword) throws SQLException { String sql SELECT s.student_id, s.name AS student_name, c.course_id, c.course_name, sc.score FROM score sc JOIN student s ON sc.student_id s.student_id JOIN course c ON sc.course_id c.course_id WHERE s.name LIKE ? OR c.course_name LIKE ? ORDER BY sc.id DESC LIMIT ?, ?; ListScoreVO list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { String keywordPattern % keyword %; ps.setString(1, keywordPattern); ps.setString(2, keywordPattern); ps.setInt(3, offset); ps.setInt(4, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { ScoreVO vo new ScoreVO(); vo.setStudentId(rs.getString(student_id)); vo.setStudentName(rs.getString(student_name)); vo.setCourseId(rs.getString(course_id)); vo.setCourseName(rs.getString(course_name)); vo.setScore(rs.getDouble(score)); list.add(vo); } } } return list; }这里取别名是必须规范的点。student.name和c.course_name如果不取别名在ResultSet编码返回值时你拿到的是同一列名取值只会取到第一个匹配列成绩列表就会“学生名和课程名永远一样”这个翻车案例非常适合拿到避坑章节。规范的写法是先给列起别名再以别名getString(student_name)取值。成绩录入的页面核心是三个下拉框。学生和课程的选项从数据库动态加载这样录入的人没法选到不存在的选项数据的脏率大幅降低select namestudentId c:forEach items${studentList} varstu option value${stu.studentId}${stu.studentId} - ${stu.name}/option /c:forEach /select3.5 项目工程化数据库连接配置、日志与SQL防注入的规范建议除了功能代码工程化的细节分为三个方面第一所有SQL参数一律使用PreparedStatement。只要看到字符串拼接SQL的地方就是需要审查的漏洞点。你可以做一个简单的代码搜索把Statement挑出来全部改掉同时把这个防止SQL注入的考量写进设计文档的“安全性”段落。第二配置外置。数据库密码不要写在DAO类里而是放到c3p0-config.xml中将来部署到别的环境运维只需要修改配置而不动Java源码。这是一个“你懂实践”的信号答辩中可以提一句。第三统一异常处理。Servlet中不应当catch所有异常然后吞掉至少把printStackTrace()和日志框架结合。仅做课程设计的话拿System.err输出也能跑但更好的一个习惯是集中跳转到一个错误页面而不是让默认的堆栈信息直接裸露在浏览器里观感很差。4. 高频翻车点排查JSPServlet项目最容易踩的五个坑及修复方法4.1 页面中文乱码设置编码的地方有一处疏忽全文动态显示都会出问题现象页面上的中文、写入MySQL的中文显示为“???”或者JSP页面头部正常但Servlet写入的数据乱码。原因链路中至少需要两级编码一致。第一级JSP页面本身要声明UTF-8第二级Servlet读取参数和写回响应要UTF-8第三级数据库表和JDBC连接URL要UTF-8。很多同学只设置了pageEncoding却漏了request.setCharacterEncoding和数据库connector的characterEncodingutf8。解决在代码里用一个全局的CharacterEncodingFilter在web.xml中配置为第一个Filter保证每个请求在进入业务逻辑之前就被设置为UTF-8WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding(UTF-8); resp.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8); chain.doFilter(req, resp); } }4.2 同一用户对同一课程录了两次成绩统计报表直接翻车现象成绩表里出现两行student_id和course_id完全相同的数据总分和平均分都算不对像是闹了鬼。原因录入界面的学生、课程两个下拉框全部可选用户手一滑同一学生选了两次同一门课数据库没有业务约束拦下重复插入。解决建表阶段就该在score表上加联合唯一约束UNIQUE KEY uk_stu_course(student_id, course_id)。如果表已建好可以后补执行ALTER TABLE score ADD UNIQUE KEY uk_stu_course(student_id, course_id);4.3 部署到Tomcat后页面点击“删除”直接暴露错误堆栈现象本地IDEA运行得好好的打包成war包放进某个实验室的Tomcat后删除按钮一点页面直接显现一个500或405后台显示MethodNotAllowed。原因多半是列表页的删除操作用了a标签或普通form的GET分支。我用Java Web的一个常识来解释HTTP的GET请求对应用户可见还可能被浏览器的预加载机制提取DELETE这种幂等操作本来不该由GET触发。Tomcat默认的JspServlet会拦截对JSP的POST请求但如果是“链接GET”就相对更容易因为参数不合规而抛出异常。合理的方式是用form表单模拟POST例如在列表页把每一个按钮包在一个表单里form actionstudent methodPOST styledisplay:inline input typehidden nameaction valuedelete input typehidden namestudentId value${stu.studentId} button typesubmit onclickreturn confirm(确定删除)删除/button /form4.4 登录拦截Filter把静态资源拦死整个页面只有文字没有样式现象用户登录成功后首页所有CSS、JS、图片全部失效浏览器F12的控制台看到大量静态资源“blocked”或404。原因Filter路径写的是/*后端把样式表、图片等静态文件也当成受保护资源拦截了Session验证不过的全被重定向到登录页。解决建立约定的目录结构把所有资源放在/static/下并在Filter的放行逻辑中加上这一条if (path.contains(/static/) || path.endsWith(.css) || path.endsWith(.js) || path.endsWith(.png)) { chain.doFilter(request, response); return; }4.5 密码明文入库或SQL拼接导致注入简单的系统也要有安全意识现象数据库表中user的password字段显示为123456写PPT时被其他同学看到脸面挂不住或者搜索框输入一个特殊字符服务端输出SQL报错。原因一是怕麻烦没用哈希二是图省事用了Statement拼接。解决即使只是个简单的学生管理系统也一律用MD5加盐。MD5Util工具可以自己写很简单public class MD5Util { public static String md5(String text) { try { MessageDigest md MessageDigest.getInstance(MD5); byte[] digest md.digest(text.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(e); } } }对所有查询一律换成PreparedStatement。这两件事做完你再在项目文档里写“本项目避免了SQL注入风险”就是真话而不是口号。5. 方案验收与答辩技巧项目能不能拿优秀就看这几个环节5.1 自测验收的标准清单每次新功能写完就手动测一遍。这里给一份可以直接照用的验收清单我建议你按这个顺序走一遍流程比最后期末突击强无数倍。如果所有项都通过这份方案就已经具备提交条件验收项测试动作预期结果登录页访问启动Tomcat访问登录URL页面完整显示无500错误正常登录输入正确账号密码跳转到首页右上角显示当前用户姓名错误登录输入错误密码停留在登录页显示错误提示越权访问未登录状态下直接访问/student页面自动跳转到登录页新增学生录入合法数据列表出现新记录无报错重复学号新增加已存在学号页面提示“学号已存在”模糊查询姓名输入“张”只显示姓张的学生分页跳转造15条数据翻到第二页URL携带pageNum变化列表显示5条删除确认点击删除按钮弹确认框确认后记录消失中文显示检查所有页面的中文无乱码成绩录入选择学生课程输入分数列表多一条JOIN后的完整数据重复成绩再次录入同一学生同一课程数据库层报错页面被捕获拦截5.2 答辩陈述路径从“我会增删改查”到“我做了完整设计”答辩最常见的翻车现场是老师问“为什么用三层架构”答“别人都这么用”问“分页怎么做”答“写了limit”。通常的应对策略是把项目的陈述节奏安排成“问题→设计→验证”这样让老师的提问始终顺着你的思路走。第一分钟讲核心问题管理系统面对的核心问题是数据的快速维护与准确统计。用一句话还原业务场景然后引到技术选型。第二分钟讲架构展示工程包结构截图说明控制层、Service层、DAO层的分层带出BaseServlet和统一Filter的设计强调为什么从控制到数据访问都是单一依赖方向。第三分钟讲亮点两个亮点二选一——连接池的使用与原因防止重复成绩的数据库唯一约束。挑你最有把握的讲深讲透。第四分钟留出时间给老师提问主动问“老师要不要看一下数据库结构”或者“我这里做了一个防止重复录成绩的约束我可以演示一下”。演示总是比纸上谈兵更信服。5.3 几个被问概率极高的难点问题最好提前准备答案问题一为什么不直接用JDBC连接要引连接池标准回答数据库连接的创建和销毁都比较耗时在高并发的场景下每次请求都新建连接会产生明显延迟严重时可能耗尽数据库连接数。连接池在系统启动时预创建一批连接复用用完归还。我配置了initialPoolSize5、maxPoolSize20既满足并发需求也能防止连接耗尽。问题二filter和interceptor有什么区别标准回答Filter是Servlet规范里的作用于Web容器层拦截所有请求无法调用Spring管理的方法Interceptor是框架层概念可以对Controller内部的方法做横切。我这个项目不用Spring所以用Filter实现登录校验与编码处理更符合Servlet规范迁移到Spring MVC后则可以平滑替换成拦截器。问题三分页为什么不用PageHelper这类插件标准回答课设的核心目标是验证基础原理PageHelper自动生成的count查询与分页SQL隐藏了实现过程。手写LIMIT参数并配合PageBean封装能够自己掌控查询过程在内容量大的情况下也能通过修改pageSize适配不同场景。三句话讲完这些问题你的理论分就拿到了。5.4 交付物整理与复盘最后整理提交材料时通常的做法是从这几个维度打包方便自己和别人双层验收源码工程Maven目录完整包含src、pom.xml、.idea可选数据库脚本文档建库建表语句、初始化测试数据语句运行说明JDK/Tomcat/MySQL的版本启动步骤默认账号密码设计文档包括需求分析、数据库设计、系统架构图和核心流程说明答辩PPT控制在10页展示页面截图、核心技术、总结亮点即可从那以后我每做完一个课设级别的小系统都强制自己走一遍“验收清单→答辩问题预演→材料打包”不把“跑通”当成终点。这套习惯后来在准备更正式的项目评审时也帮我节省了大量临时补文档的时间。希望这份方案能帮你把JSPServlet的学生信息管理系统做得既完整又好讲交出一份能让自己满意的课程设计。本文还有配套的精品资源点击获取