Java+Servlet+JSP+MySQL成绩管理系统:从环境搭建到避坑实战

发布时间:2026/10/7 18:25:57
Java+Servlet+JSP+MySQL成绩管理系统:从环境搭建到避坑实战
简介这是一套面向高校计算机相关专业学生的Java Web课程设计资源采用ServletJSPMySQL技术栈实现学生成绩管理系统适合作为期末大作业、课程设计或Java Web入门练手项目。压缩包共188个文件约11.29MB其中包含24个Java源文件、42个JSP页面、72个编译后的class文件、12个XML配置、9个依赖jar包以及6个SQL脚本另附项目报告文档覆盖登录鉴权、成绩录入、查询统计等典型模块源码经本地编译可运行评审得分98分难度适中。已有223人学习下载。读者可据此获得一套结构完整的系统实现方案对照DAO、Servlet与JSP分层代码理解MVC流程并借助SQL脚本快速还原数据库省去从零搭建的时间适合需要提交规范课程设计或想通过实战巩固Java Web基础的学习者参考使用。1. 从一份“能跑起来”的成绩管理系统说起JavaServletJSPMySQL 到底能解决什么很多同学做课程设计或者毕业设计时拿到“学生成绩管理系统”这个题目第一反应是去网上找一份源码下载下来改改界面写个报告交差。但真正动手跑的时候Tomcat 启动报 500、JSP 页面中文乱码、MySQL 连不上、Servlet 映射 404一个都跑不掉。这套 JavaServletJSPMySQL 的技术组合虽然看起来是“老一套”但它恰恰是理解 Java Web 请求流转、数据库交互、前后端数据传递最直白的路径。它适合谁适合刚学完 Java 基础、想搞明白一个 Web 项目从浏览器到数据库再回到页面到底经历了什么的人也适合需要交一份能演示、能讲清楚、能自己改功能的课程设计的人。这一篇不讲空泛的架构演进只讲怎么把这份源码在本地跑通、怎么改参数、怎么避开那些让新手卡一整天的坑。2. 环境搭建与数据库准备把 MySQL 和 Tomcat 先伺候好2.1 选 JDK、Tomcat、MySQL 的版本组合这套技术栈最怕的就是版本打架。我一般会这样配JDK 用 8 或者 11Tomcat 用 8.5 或 9.0MySQL 用 5.7 或者 8.0。为什么不是 JDK 17 加 Tomcat 10因为 Tomcat 10 把 javax.servlet 包名改成了 jakarta.servlet网上绝大多数 ServletJSP 的源码还是 javax 的你一换 Tomcat 10所有 import javax.servlet 的地方全部报错新手根本不知道从哪改起。MySQL 8.0 和 5.7 的区别主要在驱动类和连接 URL 参数上后面会具体说。安装 MySQL 的时候记住 root 密码别设太复杂本地开发用 123456 或者 root 都行。安装完成后用命令行或者 Navicat 连上去先确认服务是启动状态。Windows 下 services.msc 里找 MySQL 服务Linux 下 systemctl status mysql。如果连不上先看端口 3306 是不是被占用再看防火墙。2.2 建库建表成绩管理系统的核心表结构成绩管理系统的数据库不需要太复杂但几张核心表必须设计清楚。我一般会建这几张学生表、课程表、成绩表、用户表用于登录。下面是一份可以直接抄的 SQL-- 创建数据库字符集用 utf8mb4避免中文乱码 CREATE DATABASE student_score DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE student_score; -- 学生表 CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT 学号, sname VARCHAR(50) NOT NULL COMMENT 姓名, gender VARCHAR(4) DEFAULT 男, class_name VARCHAR(50) COMMENT 班级 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表 CREATE TABLE course ( id INT PRIMARY KEY AUTO_INCREMENT, cno VARCHAR(20) NOT NULL UNIQUE COMMENT 课程编号, cname VARCHAR(50) NOT NULL COMMENT 课程名称, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 成绩表关联学生和课程 CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id INT NOT NULL, course_id INT NOT NULL, score DECIMAL(5,1) DEFAULT 0 COMMENT 分数, exam_time DATE COMMENT 考试日期, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 用户表用于登录 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(10) DEFAULT student COMMENT admin或student ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 插入一条管理员账号密码先用明文后面在代码里说明加密 INSERT INTO users (username, password, role) VALUES (admin, 123456, admin);这段 SQL 里字符集统一用 utf8mb4是因为 JSP 页面和数据库交互时只要有一处不是 utf8中文就会变成问号。外键约束加上是为了保证成绩表里的 student_id 和 course_id 一定对应真实存在的数据避免脏数据。users 表里密码先存明文是为了让你第一次能登录进去后面在 Servlet 里可以加 MD5 或者 BCrypt但课程设计阶段明文也能跑。2.3 导入项目到 Eclipse 或 IDEA 并配置 Tomcat拿到源码后先看项目结构。如果是 Eclipse 的动态 Web 项目应该有 src 目录、WebContent 目录或者 webapplib 目录下放 mysql-connector-java 的 jar 包。如果是 Maven 项目pom.xml 里会有 servlet-api 和 mysql-connector 的依赖。我一般用 IDEA 导入步骤是File - New - Project from Existing Sources选项目根目录然后一路下一步。导入后检查 Project Structure 里的 Modules确认 Web facet 已经加上Web Resource Directory 指向 webapp 或 WebContent。然后配置 TomcatRun - Edit Configurations - 加一个 Tomcat Server Local在 Deployment 里加一个 Artifact选 exploded 的 war 包。Application context 设成 /score 或者 /看源码里怎么写的。这里有个细节如果源码里的 Servlet 是用 WebServlet(/login) 注解配置的那 web.xml 里可能没有映射你不需要改。如果是老项目用 web.xml 配置 servlet 和 servlet-mapping那你要确认 url-pattern 和你访问的地址一致。我见过太多人访问 /login 结果 404最后发现 web.xml 里写的是 /LoginServlet。3. Servlet 与 JSP 的分工请求怎么进来页面怎么出去3.1 一个登录请求的完整流转从 login.jsp 到 LoginServlet 再到数据库很多人写了几十个 Servlet还是没搞明白 request 和 response 到底怎么走。我拿登录这个最典型的场景拆一遍。用户在 login.jsp 里输入用户名密码点提交表单的 action 指向 LoginServletmethod 是 post。Tomcat 收到请求后根据 web.xml 或者注解找到对应的 Servlet 类调用它的 doPost 方法。在 doPost 里你通过 request.getParameter(username) 拿到用户输入然后去数据库查 users 表如果查到了并且密码匹配就把用户信息塞进 session然后 response.sendRedirect(main.jsp)如果没查到就 request.setAttribute(msg, 用户名或密码错误)然后 request.getRequestDispatcher(login.jsp).forward(request, response)。这里的关键区别sendRedirect 是让浏览器重新发一个请求地址栏会变request 里的数据会丢forward 是服务器内部转发地址栏不变request 里的数据还在。登录失败要用 forward才能把错误信息带回 login.jsp 显示。下面是一个 LoginServlet 的核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 设置编码防止中文乱码 request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); String username request.getParameter(username); String password request.getParameter(password); // 简单校验实际项目要加更多判断 if (username null || username.trim().isEmpty()) { request.setAttribute(msg, 用户名不能为空); request.getRequestDispatcher(login.jsp).forward(request, response); return; } Connection conn null; PreparedStatement ps null; ResultSet rs null; try { conn DBUtil.getConnection(); // 用 PreparedStatement 防止 SQL 注入 String sql SELECT id, username, role FROM users WHERE username ? AND password ?; ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); rs ps.executeQuery(); if (rs.next()) { // 登录成功把用户信息放进 session HttpSession session request.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(username, rs.getString(username)); session.setAttribute(role, rs.getString(role)); response.sendRedirect(studentList); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } catch (SQLException e) { e.printStackTrace(); request.setAttribute(msg, 数据库连接失败 e.getMessage()); request.getRequestDispatcher(login.jsp).forward(request, response); } finally { DBUtil.close(conn, ps, rs); } } }这段代码里DBUtil 是我自己封装的一个工具类负责加载驱动、获取连接、关闭资源。PreparedStatement 用 ? 占位是为了防止用户输入 or 11 这种 SQL 注入。session.setAttribute 把用户信息存起来后面其他页面就能通过 session.getAttribute(username) 拿到当前登录人。sendRedirect 跳转到 studentList这个路径对应另一个 Servlet 的 WebServlet(/studentList)。3.2 JSP 页面里怎么取数据EL 表达式和 JSTL 标签的配合Servlet 处理完数据要么转发到 JSP要么重定向到另一个 Servlet 再查数据转发到 JSP。JSP 里不要写 Java 代码块用 EL 表达式 ${} 和 JSTL 标签最干净。比如在 studentList.jsp 里展示学生列表% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html head title学生列表/title /head body h2学生成绩管理系统 - 学生列表/h2 p当前用户${sessionScope.username}/p table border1 cellpadding5 cellspacing0 tr th学号/th th姓名/th th性别/th th班级/th th操作/th /tr c:forEach items${studentList} varstu tr td${stu.sno}/td td${stu.sname}/td td${stu.gender}/td td${stu.className}/td td a hrefscoreList?studentId${stu.id}查看成绩/a /td /tr /c:forEach /table /body /html这里 ${studentList} 是 Servlet 里 request.setAttribute(studentList, list) 传过来的。c:forEach 是 JSTL 的循环标签items 写 EL 表达式var 是每次循环的变量名。${stu.sno} 会自动调用 Student 类的 getSno() 方法。注意JSTL 的 jar 包要放到 WEB-INF/lib 下否则 c:forEach 会报错。很多人只放了 servlet-api 和 mysql 驱动忘了 jstl 和 standard 两个 jar结果页面直接 500。3.3 用 Filter 统一处理编码和登录拦截每个 Servlet 里都写 request.setCharacterEncoding(UTF-8) 太啰嗦而且容易漏。我一般会加一个 CharacterEncodingFilter在 web.xml 里配成拦截所有请求。再加一个 LoginFilter判断 session 里有没有 userId没有就跳回 login.jsp。这样就不用每个页面都写登录判断了。WebFilter(/*) public class CharacterEncodingFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); chain.doFilter(request, response); } }这个 Filter 用 WebFilter(/*) 拦截所有请求在 chain.doFilter 之前设置编码。注意如果是 POST 请求setCharacterEncoding 必须在 getParameter 之前调用才有效。GET 请求的参数编码取决于 Tomcat 的 server.xml 里 Connector 的 URIEncodingTomcat 8 以后默认就是 UTF-8一般不用改。4. 成绩管理核心功能的实现增删改查与分页4.1 学生成绩的录入与修改表单提交到 Servlet 的完整链路成绩录入一般是在 scoreAdd.jsp 里选学生、选课程、填分数提交到 ScoreServlet 的 add 方法。这里要注意学生和课程的下拉框数据要从数据库查出来在 Servlet 里查好放到 request 里转发到 JSP 用 c:forEach 渲染成 option。// 在 ScoreServlet 的 doGet 里根据 action 参数区分操作 String action request.getParameter(action); if (toAdd.equals(action)) { // 查询所有学生和课程供下拉框使用 ListStudent students studentDao.findAll(); ListCourse courses courseDao.findAll(); request.setAttribute(students, students); request.setAttribute(courses, courses); request.getRequestDispatcher(scoreAdd.jsp).forward(request, response); } else if (add.equals(action)) { // 接收表单数据插入成绩表 int studentId Integer.parseInt(request.getParameter(studentId)); int courseId Integer.parseInt(request.getParameter(courseId)); double score Double.parseDouble(request.getParameter(score)); Score s new Score(); s.setStudentId(studentId); s.setCourseId(courseId); s.setScore(score); scoreDao.insert(s); response.sendRedirect(scoreList?actionlist); }这段代码里action 参数是区分同一个 Servlet 处理不同请求的常用手法。toAdd 是打开录入页面add 是提交录入数据。Integer.parseInt 和 Double.parseDouble 要做异常捕获否则用户输入非数字直接 500。实际项目中我一般会在前端加 required 和 typenumber 做第一层校验后端再做第二层。4.2 成绩列表的分页查询SQL limit 与页码计算成绩数据一多不分页会卡。分页的核心就两个参数当前页码 pageNo 和每页条数 pageSize。SQL 里用 LIMIT (pageNo-1)*pageSize, pageSize。同时要查一个总数用来算总页数。public ListScore findByPage(int pageNo, int pageSize) { ListScore list new ArrayList(); String sql SELECT s.id, st.sname, c.cname, s.score, s.exam_time FROM score s JOIN student st ON s.student_id st.id JOIN course c ON s.course_id c.id ORDER BY s.id DESC LIMIT ?, ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, (pageNo - 1) * pageSize); ps.setInt(2, pageSize); ResultSet rs ps.executeQuery(); while (rs.next()) { Score score new Score(); score.setId(rs.getInt(id)); score.setStudentName(rs.getString(sname)); score.setCourseName(rs.getString(cname)); score.setScore(rs.getDouble(score)); score.setExamTime(rs.getDate(exam_time)); list.add(score); } } catch (SQLException e) { e.printStackTrace(); } return list; }这里用了 JOIN 把学生姓名和课程名称一起查出来避免在 JSP 里再根据 id 去查一次。ORDER BY s.id DESC 让最新的成绩排在前面。LIMIT 的第一个参数是偏移量第二个是每页条数。总页数的计算是 int totalPage (totalCount pageSize - 1) / pageSize这个公式要记住不然最后一页会算少。4.3 删除与批量删除注意外键约束和事务删除成绩记录本身简单但如果学生表或课程表里有外键引用直接删学生会报错。我一般会先删成绩表里关联的记录再删学生。批量删除就是前台用 checkbox 收集 id后台用 IN 语句一次删多条。public void deleteByIds(String[] ids) { if (ids null || ids.length 0) return; StringBuilder sql new StringBuilder(DELETE FROM score WHERE id IN (); for (int i 0; i ids.length; i) { sql.append(i 0 ? ? : ,?); } sql.append()); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql.toString())) { for (int i 0; i ids.length; i) { ps.setInt(i 1, Integer.parseInt(ids[i])); } ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); } }这段代码动态拼接 IN 里的问号个数然后用循环 setInt。注意ids 数组的长度决定了问号的数量不能写死。如果要做事务比如删除学生同时删除他的所有成绩就要把 conn.setAutoCommit(false) 打开两个操作都成功再 commit有一个失败就 rollback。5. 避坑与排查那些让新手通宵的常见问题5.1 中文乱码从 JSP 到数据库的每一层都要查现象页面输入中文存到数据库变成 ???或者从数据库查出来在页面显示问号。原因编码不统一。JSP 页面顶部要写 % page contentTypetext/html;charsetUTF-8 %Servlet 里要 request.setCharacterEncoding(UTF-8)数据库连接 URL 要加 useUnicodetruecharacterEncodingutf8数据库和表的字符集要是 utf8mb4。解决按这个顺序一层层查哪层不对改哪层。Tomcat 的 server.xml 里 Connector 加 URIEncodingUTF-8 对 GET 请求也有效。5.2 MySQL 连接报错驱动类、时区、SSL 三个高频点现象ClassNotFoundException: com.mysql.jdbc.Driver 或者 Access denied for user 或者 The server time zone value is unrecognized。原因MySQL 8.0 的驱动类是 com.mysql.cj.jdbc.Driver不是 com.mysql.jdbc.Driver时区问题要在 URL 里加 serverTimezoneAsia/ShanghaiSSL 警告加 useSSLfalse。解决连接 URL 写成 jdbc:mysql://localhost:3306/student_score?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse。驱动 jar 包用 mysql-connector-java-8.0.x。5.3 404 与 500Servlet 映射和空指针的排查顺序现象访问 /login 报 404或者页面报 500 但控制台有异常。原因404 多半是 url-pattern 写错或者 WebServlet 的 value 和访问路径不一致或者项目没部署到 Tomcat 的 webapps 下。500 要看控制台堆栈最常见的是 NullPointerException比如 session 里没取到值就调用方法或者数据库连接返回 null。解决404 先看 web.xml 和注解再看 Tomcat 的 Deployment 里 Artifact 有没有加。500 先看异常第一行定位到具体类和行号。5.4 JSP 页面加载后自动刷新一次meta 标签与定时器的取舍现象想让 JSP 页面加载完后自动刷新一次比如显示最新成绩。原因可能是想用定时刷新。解决在 head 里加表示 5 秒刷新一次但这样会一直刷。如果只想加载后刷一次可以用 JavaScript 的 setTimeout 配合 location.reload()或者干脆在 Servlet 里查最新数据转发过来不需要刷新。我一般不建议用 meta refresh因为用户体验不好而且会重复提交表单。5.5 密码明文存储与 SQL 注入课程设计也要守住的底线现象数据库里密码是 123456登录 SQL 用字符串拼接。原因图省事。解决密码至少用 MD5 加盐存SQL 一律用 PreparedStatement。MD5 虽然现在不安全但比明文强。加盐就是每个用户生成一个随机 salt存数据库登录时用同样的 salt 加密再比对。PreparedStatement 的 ? 占位是防注入最有效的手段不要用 where username username 这种写法。6. 从能跑到能讲清楚报告里该写什么、答辩时会被问什么6.1 报告的核心章节别把源码贴进去凑页数课程设计报告不是代码说明书。我一般会按这个结构写需求分析画用例图学生、教师、管理员分别能做什么、数据库设计E-R 图加表结构说明、系统架构B/S 结构Servlet 做控制JSP 做视图DAO 做数据访问、核心功能实现挑登录、成绩录入、分页查询三个讲透、测试正常流程加异常流程比如空用户名、错误密码、重复学号。源码只贴关键片段不要整页整页贴。6.2 答辩高频问题Servlet 生命周期、forward 与 redirect 区别、session 与 cookie 区别这三个问题几乎每场答辩都会问。Servlet 生命周期实例化、init、service、doGet/doPost、destroy。forward 是服务器内部跳转一次请求地址栏不变request 数据共享redirect 是浏览器重新请求两次请求地址栏变request 数据丢失。session 存在服务器cookie 存在浏览器session 依赖 cookie 传 JSESSIONID如果浏览器禁用 cookie可以用 URL 重写。把这些用自己的话讲一遍比背概念强。6.3 一个具体技巧用 DBUtil 统一管理连接避免到处写 Class.forName我见过太多源码在每个 DAO 方法里都写一遍 Class.forName(com.mysql.jdbc.Driver) 和 DriverManager.getConnection。这样代码重复改数据库密码要改几十处。我一般会写一个 DBUtilpublic class DBUtil { private static final String URL jdbc:mysql://localhost:3306/student_score?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (stmt ! null) stmt.close(); } catch (SQLException e) { e.printStackTrace(); } try { if (conn ! null) conn.close(); } catch (SQLException e) { e.printStackTrace(); } } }这个类用静态代码块只加载一次驱动URL、用户名、密码都集中在一处。以后换数据库只改这三个常量。close 方法按 ResultSet、Statement、Connection 的顺序关闭避免资源泄漏。如果你用连接池比如 Druid 或 HikariCP也是在这个类里改 getConnection 的实现上层 DAO 不用动。6.4 我踩过的一个坑Tomcat 部署路径与相对路径有一次我把项目部署到 Tomcat访问 login.jsp 正常但提交表单后 404。查了半天发现login.jsp 里 form 的 action 写的是 LoginServlet而我的项目 context path 是 /score实际访问路径应该是 /score/LoginServlet。相对路径在 JSP 里容易出问题我后来统一用 ${pageContext.request.contextPath}/LoginServlet这样不管部署路径是什么都能正确拼接。这个习惯帮我省了很多调试时间。6.5 这套源码值不值得改我的建议如果你只是交课程设计把登录、增删改查、分页跑通报告写清楚足够了。但如果你想用它去面试 Java 开发我建议你在上面加两个东西一是把 JDBC 换成 MyBatis 或 JPA二是把 JSP 换成 Thymeleaf 或前后端分离的接口。面试官看到 ServletJSP 不会扣分但如果你能讲清楚为什么后来不用 JSP 了以及 MyBatis 解决了 JDBC 的哪些痛点那就是加分项。我一般会保留这套源码作为理解原理的底子但新项目不会再从它开始写。希望帮到你。本文还有配套的精品资源点击获取