☰
Java Web入门必练:Servlet+JSP+MySQL成绩管理系统实战
2026/10/1 13:05:57 网站建设 项目流程

简介:这是一套基于Java Web经典技术栈(Servlet+JSP+MySQL)开发的学生成绩管理系统完整源码与配套文档,专为高校计算机专业学生设计,适用于Java Web课程设计、期末大作业及基础项目实践,帮助学习者系统掌握MVC分层架构、数据库交互、前后端协同与CRUD全流程开发。资源共188个文件,包含42个JSP页面(实现用户界面与交互)、24个Java类(含GradeDao、StudentDao、LoginServlet等核心业务与控制逻辑)、72个编译后Class文件、6个SQL建表与初始化脚本、9个Jar依赖库,以及CSS/JS/HTML等前端资源,压缩包仅11.29MB,结构清晰、模块解耦明确。已有223人下载学习,所有代码均经本地实测可运行,评审得分98分,附带完整设计报告,内容经助教审定,覆盖需求分析、数据库设计、功能模块说明与部署指南,切实降低入门门槛,提升工程实践能力。

1. 为什么这个“Java+Servlet+JSP+MySQL学生成绩管理系统”至今仍是Java Web入门绕不开的硬核练手项目?

不是因为它多酷炫——它没有Vue响应式界面,不跑在Docker里,也不对接微信小程序;恰恰相反,它土得掉渣:一个WAR包丢进Tomcat,靠<%= %>拼HTML,用request.getParameter()接表单,SQL写在Java里还带字符串拼接。但正因如此,它像一台拆开的机械手表:齿轮咬合清晰、动力传递可见、每个故障点都暴露在眼皮底下。你改一行doPost()逻辑,页面立刻报500;删掉web.xml里一个<servlet-mapping>,整个成绩录入页就404;MySQL字段少设个NOT NULL,插入空值时连SQLException堆栈都懒得打全。这系统不是教你怎么“快速上线”,而是逼你亲手把HTTP请求怎么变成数据库记录、JSP怎么被编译成Servlet、Tomcat线程池怎么被耗尽、事务隔离级别怎么影响同班同学查分结果——这些在Spring Boot自动配置下早已黑盒化的环节,全在这里裸奔。适合刚写完HelloWorld、正卡在“Java能干啥”和“Web到底啥结构”之间的新人;也适合面试前想亲手验证Servlet生命周期是不是真如八股文写的那样“init→service→destroy”,或者想确认JSP隐式对象里pageContext到底能不能跨页面传值的老手。它不教你架构设计,但教你怎么让代码不崩——这是所有高阶能力的地基。


2. 搭建最小可运行环境:从零配通Tomcat+MySQL+IDEA,避开90%新手卡点

2.1 JDK与Tomcat版本对齐:别让Java 17毁掉你的第一个HttpServlet

很多新手栽在第一步:下载了最新版JDK 21,却用Tomcat 9(仅支持到Java 11),结果启动时直接报UnsupportedClassVersionError。这不是配置问题,是版本契约断裂。必须严格匹配:

组件推荐版本理由
JDKJava 8(1.8)或 Java 11Servlet 3.1+规范要求,且绝大多数教学源码基于此开发;Java 17+需Tomcat 10+,但JSP支持已移除(Tomcat 10默认用Jakarta EE 9命名空间,javax.*全变jakarta.*)
TomcatTomcat 8.5.x 或 Tomcat 9.0.x兼容Servlet 3.1/4.0,稳定支持JSP 2.3,Windows/Linux双平台无坑
MySQLMySQL 5.7 或 8.0(开启传统认证模式)避免MySQL 8.0默认caching_sha2_password插件导致JDBC连接失败

提示:下载Tomcat后,务必检查bin/catalina.bat(Windows)或bin/catalina.sh(Linux)顶部的JAVA_HOME路径是否指向你实际安装的JDK 8/11目录。别信IDEA里设置的SDK——Tomcat启动时只认系统环境变量或脚本内硬编码路径。

2.2 IDEA新建动态Web项目:拒绝向导式陷阱,手动配web.xml才是真入门

IDEA的“New Project → Jakarta EE”向导会自动生成web.xml并设为Servlet 4.0,但教学源码普遍基于Servlet 3.1,且大量依赖web.xml显式配置(如<welcome-file-list>、<filter>顺序)。必须手动创建标准结构:

your-project/ ├── src/ │ └── main/ │ ├── java/ # Servlet类放这里 │ └── resources/ # db.properties等配置文件 ├── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # 手动创建,非IDEA自动生成 │ │ └── lib/ # 放mysql-connector-java-5.1.47.jar(勿用8.x) │ ├── index.jsp │ └── css/js/img/ # 静态资源

关键步骤:

  1. 新建Module → Maven → 勾选“Create from archetype” → 选maven-archetype-webapp(不要选任何Spring或Jakarta模板);
  2. 右键Project → Add Framework Support → Web Application(version 3.1);
  3. 手动创建webapp/WEB-INF/web.xml,内容必须含<web-app>根节点及version="3.1"属性:
<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1"> <display-name>StudentScoreSystem</display-name> <welcome-file-list> <welcome-file>index.jsp</welcome-file> </welcome-file-list> </web-app>

参数说明:version="3.1"是硬性要求——Servlet 3.1规范才定义@WebServlet注解,但教学源码多用XML配置,且Tomcat 8.5默认解析3.1 schema。若写成4.0,部分老源码的<servlet-class>路径可能被IDEA误判为无效。

2.3 MySQL连接池实战:别再用Class.forName()+DriverManager.getConnection()写死密码

源码里常见DBUtil.java直接new com.mysql.jdbc.Driver(),这在生产环境是定时炸弹:每次查询都新建连接,100并发瞬间打爆MySQL。必须上连接池,但别碰HikariCP(太重)或Druid(配置复杂),用Tomcat自带的BasicDataSource最稳妥:

  1. 在webapp/WEB-INF/lib/放入commons-dbcp-1.4.jar和commons-pool-1.6.jar(Tomcat 8.5兼容);
  2. 在webapp/WEB-INF/web.xml中添加资源引用:
<resource-ref> <description>DB Connection</description> <res-ref-name>jdbc/StudentDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>
  1. 在conf/context.xml(Tomcat全局)或webapp/META-INF/context.xml(项目级)中配置数据源:
<?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/StudentDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory" maxActive="20" maxIdle="10" minIdle="5" initialSize="5" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/studentdb?useSSL=false&amp;serverTimezone=GMT%2B8" username="root" password="123456" validationQuery="SELECT 1" testOnBorrow="true"/> </Context>

逻辑说明:factory="org.apache.tomcat.dbcp.dbcp.BasicDataSourceFactory"指定Tomcat DBCP工厂类;validationQuery="SELECT 1"确保连接有效性;testOnBorrow="true"每次借连接时校验,避免拿到失效连接。URL中useSSL=false是MySQL 5.7+必需参数,否则报SSL握手失败;serverTimezone=GMT%2B8解决时区错误(否则存入时间全错8小时)。


3. 核心功能落地:从登录验证到成绩录入,手撕三层结构每一行代码

3.1 登录模块:LoginServlet如何用Session实现状态保持

教学源码常把登录逻辑写在JSP里(<% if(request.getParameter("login")!=null){...}%>),这是反模式。必须分离:Servlet处理业务,JSP只负责展示。

LoginServlet.java核心逻辑:

@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); // 1. 调用DAO验证用户(此处简化,实际应查库) boolean isValid = "admin".equals(username) && "123".equals(password); if (isValid) { // 2. 创建Session并存用户信息 HttpSession session = request.getSession(true); // true表示不存在则创建 session.setAttribute("user", username); session.setMaxInactiveInterval(30 * 60); // 30分钟无操作过期 // 3. 重定向到主页,避免重复提交 response.sendRedirect("main.jsp"); } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("index.jsp").forward(request, response); } } }

参数说明:request.getSession(true)确保Session存在;setMaxInactiveInterval(1800)单位是秒;response.sendRedirect()是302重定向,地址栏变main.jsp;而forward()是服务器端跳转,地址栏仍为/login,且request属性可传递。

对应index.jsp登录表单:

<form action="login" method="post"> 用户名:<input type="text" name="username" required><br> 密码:<input type="password" name="password" required><br> <input type="submit" value="登录"> </form> <c:if test="${not empty error}"> <font color="red">${error}</font> </c:if>

注意:JSP中${error}能取到,是因为forward()时request作用域未销毁;若用sendRedirect(),则需改用session.setAttribute("error", "xxx")并在index.jsp中<%= session.getAttribute("error") %>获取。

3.2 成绩录入:ScoreServlet的事务控制与SQL注入防御

原始源码常这样写:

String sql = "INSERT INTO score VALUES(null, '" + studentId + "', '" + subject + "', " + score + ")"; stmt.executeUpdate(sql);

——这是SQL注入温床。必须用PreparedStatement:

@WebServlet("/addScore") public class ScoreServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String studentId = request.getParameter("studentId"); String subject = request.getParameter("subject"); String scoreStr = request.getParameter("score"); Connection conn = null; PreparedStatement pstmt = null; try { Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/StudentDB"); conn = ds.getConnection(); // 1. 开启事务 conn.setAutoCommit(false); // 2. 使用预编译防止注入 String sql = "INSERT INTO score(student_id, subject, score) VALUES(?, ?, ?)"; pstmt = conn.prepareStatement(sql); pstmt.setString(1, studentId); pstmt.setString(2, subject); pstmt.setDouble(3, Double.parseDouble(scoreStr)); pstmt.executeUpdate(); // 3. 提交事务 conn.commit(); request.setAttribute("msg", "录入成功"); } catch (Exception e) { // 4. 回滚事务 if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } request.setAttribute("msg", "录入失败:" + e.getMessage()); e.printStackTrace(); } finally { // 5. 关闭资源(按pstmt→conn顺序) if (pstmt != null) try { pstmt.close(); } catch (SQLException e) {} if (conn != null) try { conn.close(); } catch (SQLException e) {} } request.getRequestDispatcher("score_add.jsp").forward(request, response); } }

逻辑说明:conn.setAutoCommit(false)关闭自动提交;pstmt.setString(1, studentId)将参数绑定到?占位符,彻底杜绝SQL注入;conn.rollback()在异常时回滚,保证数据一致性;finally块确保资源释放,避免连接泄漏。

3.3 成绩查询:JSP+JSTL+EL实现动态表格渲染

别再用<% while(rs.next()){ out.print("<tr>..."); } %>拼HTML!用JSTL标签库+EL表达式:

  1. 在webapp/WEB-INF/lib/放入jstl-1.2.jar和standard-1.1.2.jar;
  2. score_list.jsp中引入标签库并遍历List:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>成绩列表</title></head> <body> <table border="1"> <tr><th>学号</th><th>姓名</th><th>科目</th><th>成绩</th></tr> <c:forEach items="${scoreList}" var="score"> <tr> <td>${score.studentId}</td> <td>${score.studentName}</td> <td>${score.subject}</td> <td>${score.score}</td> </tr> </c:forEach> </table> </body> </html>

参数说明:<c:forEach>遍历request.getAttribute("scoreList")(Servlet中request.setAttribute("scoreList", list)传入);${score.studentId}调用Score实体类的getStudentId()方法;JSTL自动处理null值(显示空字符串而非null),比<%= score.getStudentId() == null ? "" : score.getStudentId() %>简洁百倍。


4. 避坑指南:那些让90%人调试到凌晨三点的致命细节

4.1 现象:访问http://localhost:8080/显示404,但Tomcat控制台无报错

原因:IDEA部署时未将webapp目录设为Web资源根路径,或web.xml中<welcome-file-list>指定的index.jsp不存在于webapp/下(比如误放在src/main/webapp/子目录)。
解决:右键项目 →Open Module Settings→Facets→Web→ 检查Web Resource Directory是否指向your-project/webapp;确认webapp/index.jsp真实存在,且文件编码为UTF-8(Windows记事本保存时选“UTF-8无BOM”)。

4.2 现象:登录后跳转main.jsp,但页面显示HTTP Status 404 – Not Found

原因:main.jsp中用了<%@ page contentType="text/html;charset=UTF-8" %>,但文件本身是GBK编码(尤其Windows系统用记事本创建),导致JSP编译时中文乱码,Tomcat无法识别文件。
解决:用Notepad++或VS Code打开main.jsp,右下角查看编码,若为ANSI/GBK,点击“编码 → 转为UTF-8”并保存;同时在web.xml中全局声明编码:

<filter> <filter-name>CharacterEncodingFilter</filter-name> <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>CharacterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>

4.3 现象:MySQL插入中文显示???,查库看到乱码

原因:MySQL服务端、数据库、表、连接URL四层编码未统一为utf8mb4。
解决:

  1. 修改MySQL配置文件my.cnf(Linux)或my.ini(Windows):
[client] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci
  1. 重启MySQL后,执行SQL重建库表:
CREATE DATABASE studentdb CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE studentdb; CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT, student_id VARCHAR(20), subject VARCHAR(50), score DOUBLE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. JDBC URL追加参数:?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8

4.4 现象:<c:forEach>不生效,表格空白,控制台无报错

原因:JSTL JAR包未正确加载,或web.xml未声明Servlet 3.1+版本导致JSP容器忽略taglib指令。
解决:

  • 检查webapp/WEB-INF/lib/下是否有jstl-1.2.jar(非jstl-api-1.2.jar);
  • 确认web.xml根节点<web-app>有version="3.1"属性;
  • 在JSP顶部强制声明:<%@ page isELIgnored="false" %>(某些旧Tomcat默认禁用EL)。

4.5 现象:修改JSP后刷新页面无变化,仍显示旧内容

原因:Tomcat的JSP热编译缓存未更新,或浏览器强缓存了.jsp响应。
解决:

  • 清空Tomcat工作目录:rm -rf tomcat/work/Catalina/localhost/your-app-name(Linux)或删除tomcat\work\Catalina\localhost\your-app-name(Windows);
  • 浏览器按Ctrl+F5强制刷新;
  • 在conf/web.xml中全局禁用JSP缓存(仅开发用):
<servlet> <servlet-name>jsp</servlet-name> <servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class> <init-param> <param-name>development</param-name> <param-value>true</param-value> </init-param> </servlet>

5. 进阶验证:用Postman+MySQL命令行交叉验证,揪出隐藏Bug

5.1 用Postman模拟表单提交,绕过JSP前端验证

JSP里的required属性只是浏览器校验,后端Servlet必须二次验证。用Postman发POST请求直击Servlet,测试边界情况:

测试场景Postman配置预期结果实际验证点
空学号提交Body → x-www-form-urlencoded →studentId=返回错误提示request.getParameter("studentId")是否为空字符串而非null
SQL注入尝试subject=' OR '1'='1插入失败或报错查看MySQL日志,确认INSERT语句是否含恶意SQL片段
成绩超范围score=150应拦截并提示“成绩应在0-100之间”Servlet中是否做`if(score<0

技巧:在Servlet中加日志输出真实参数:

System.out.println("Received: studentId=" + studentId + ", subject=" + subject + ", score=" + scoreStr);

Postman发送后,立即看Tomcat控制台输出,比断点调试快10倍。

5.2 MySQL命令行验证数据一致性:别信JSP页面显示

JSP页面可能因EL表达式错误、JSTL标签未加载而显示空表格,但数据其实已入库。必须直连MySQL验证:

# 进入MySQL命令行 mysql -u root -p # 选择数据库 USE studentdb; # 查看score表结构(确认字段类型) DESCRIBE score; # 查询所有记录(验证中文是否正常) SELECT * FROM score; # 检查事务是否生效:故意让Servlet抛异常,看是否回滚 INSERT INTO score(student_id, subject, score) VALUES('S001', '数学', 95); # 手动开启事务测试 START TRANSACTION; INSERT INTO score(student_id, subject, score) VALUES('S002', '英语', 88); ROLLBACK; -- 执行后S002记录应消失

参数说明:DESCRIBE score检查student_id是否为VARCHAR(非INT),避免学号001存成1;SELECT *确认subject字段显示“数学”而非“???”;ROLLBACK验证事务回滚能力——这是成绩系统的核心保障。

5.3 Tomcat线程池压测:100并发登录会不会挂?

教学系统没考虑并发,但真实场景下HttpSession和数据库连接池是瓶颈。用ab(Apache Bench)简单压测:

# 模拟100个用户,每个用户请求10次登录 ab -n 1000 -c 100 http://localhost:8080/login

观察Tomcat控制台:

  • 若出现SEVERE: All threads (200) are currently busy,说明maxThreads不足;
  • 若MySQL报Too many connections,说明连接池maxActive设太小;

调优方案(conf/server.xml):

<Executor name="tomcatThreadPool" namePrefix="catalina-exec-" maxThreads="200" minSpareThreads="25" maxIdleTime="60000"/> <Connector executor="tomcatThreadPool" port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

血泪经验:minSpareThreads设为25,避免突发流量时线程创建延迟;maxIdleTime="60000"(60秒)防止空闲线程长期占用内存;压测后务必恢复maxThreads=200,生产环境按CPU核心数*2设(如4核设80)。


6. 我的三个硬核习惯:让这套老技术栈在2024年依然高效可靠

6.1 每个Servlet类顶部写明职责与输入输出契约

别再让团队新人猜ScoreServlet是增删改查哪个。我在类开头强制写:

/** * 成绩录入Servlet * 输入参数(POST): * - studentId: 学号(非空,长度≤20) * - subject: 科目名称(非空,长度≤50) * - score: 成绩(数字,0-100) * 输出: * - 成功:跳转score_add.jsp,request.setAttribute("msg","录入成功") * - 失败:跳转score_add.jsp,request.setAttribute("msg","错误详情") * 数据库操作:INSERT INTO score(student_id, subject, score) */ @WebServlet("/addScore") public class ScoreServlet extends HttpServlet { // ... 实现 }

为什么有效:契约文档化后,前端传参不敢乱来,测试人员知道该填什么值,交接时不用口头解释。我见过太多项目因为studentId传了"S001 "(带空格)导致查不到学生,就因为没人写清楚“是否trim”。

6.2web.xml里所有<servlet>按字母序排列,<servlet-mapping>严格对应

老源码常把LoginServlet映射写在ScoreServlet后面,导致/login路径被覆盖。我的做法:

<!-- 按servlet-name字母升序 --> <servlet> <servlet-name>AddScoreServlet</servlet-name> <servlet-class>com.example.servlet.ScoreServlet</servlet-class> </servlet> <servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.example.servlet.LoginServlet</servlet-class> </servlet> <!-- mapping与servlet-name严格一一对应,不交叉 --> <servlet-mapping> <servlet-name>AddScoreServlet</servlet-name> <url-pattern>/addScore</url-pattern> </servlet-mapping> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>

玄学效果:排序后一眼看出URL路径是否重复(如两个/login),grep -A 2 "login"就能定位全部相关配置;<servlet-mapping>紧贴<servlet>,避免复制粘贴时错位。

6.3 MySQL建表时,每个字段加COMMENT,用SHOW CREATE TABLE导出DDL

别让score表字段变成谜题。建表时强制写注释:

CREATE TABLE score ( id INT PRIMARY KEY AUTO_INCREMENT COMMENT '主键ID', student_id VARCHAR(20) NOT NULL COMMENT '学号,唯一标识学生', subject VARCHAR(50) NOT NULL COMMENT '科目名称,如数学、英语', score DOUBLE NOT NULL COMMENT '成绩,0-100之间', create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生成绩表';

然后用SHOW CREATE TABLE score\G导出带注释的建表语句,存为score_table.sql。每次部署新环境,直接source score_table.sql,注释自动同步。

后悔药:某次线上事故,发现student_id被误设为INT,导致学号001存成1。有了COMMENT,grep "学号" *.sql三秒定位问题文件;没有注释,只能翻Git历史逐行diff。

这套组合拳下来,一个看似陈旧的Java Web项目,反而成了最透明、最可控、最容易排查的系统。它不追求时髦,但每一步都踩在工程底线之上——连接池防泄漏、PreparedStatement防注入、事务保一致、注释保可读。这些年我带过的实习生,凡是能把这套流程走通的,三个月后都能独立维护银行核心系统的报表模块。技术会迭代,但对数据的责任、对请求的敬畏、对边界的清醒,永远不过时。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询