简介:一份基于原生Java的JavaWeb学生管理系统源码包,面向JavaWeb初学者以及需要课程实训或毕业设计参考的开发者,系统围绕学生信息、班级分配、教师任课、课程设置四大核心模块,实现了完整的增删改查操作。项目采用MVC分层架构,将Servlet、JSP、DAO、Service各层职责清晰分离,覆盖从页面表单提交到数据库持久化的完整调用链路,是理解JavaWeb经典开发模式的实用范例。资源共40个文件,主要包含Java源码、JSP页面、编译后的class文件、SQL数据库脚本以及Eclipse工程配置文件,压缩包仅1.26MB,结构简洁,下载后可直接导入Eclipse并结合studenttest.sql初始化MySQL数据库运行。目前已有5104人浏览学习,适合作为学生管理系统类项目的参考样例。通过研读源码和SQL脚本,可以掌握DAO封装思路、Servlet跳转控制、JSP页面渲染以及MySQL表设计等关键技能,也能快速上手一套完整JavaWeb项目的开发流程,对课程设计和毕业设计都有实用价值。
1. javaweb学生管理系统:入门第一个完整项目,它到底能给你什么
许多人的第一个Java Web项目,是从一个叫“javaweb学生管理系统(含源码).zip”的压缩包开始的。它通常只有几MB,解压后是JSP页面、Servlet类、一个SQL脚本和一份安装说明。这东西不是黑科技,但恰好覆盖了入门最要命的一条链路:浏览器表单提交到后端Servlet,再经JDBC落到MySQL,最后渲染回页面展示。这篇笔记我从交付视角把它拆开讲:代码结构怎么读、Tomcat与JDK版本怎么配、数据库脚本和核心链路怎么跑通,以及启动失败时最常见的几个坑。适合要交课程设计的学生,也适合想快速把一个旧管理端改造成新框架的从业者。新手能照着把项目跑起来,熟手能直接跳到避坑章节看边界。
2. 拆开zip看门道:JSP+Servlet+JDBC三段式架构为什么能扛住课设
拿到压缩包的第一件事不是双击运行,而是先看目录。绝大多数这类项目用的是经典三段式:JSP负责页面展示,Servlet负责接收请求和跳转,JDBC负责读写数据库。它的好处是每一个环节都能单独调试,坏处是代码量肉眼可见地膨胀,一个学生信息管理可能有五六个Servlet类。先把这个结构认清楚,后面改代码才不会迷路。
2.1 压缩包里最常见的那套目录结构
我解压过不少学生管理系统,目录命名各有差异,但骨架基本一致。这里用一个典型结构说明:
student-management/ ├── src/ │ ├── com/example/entity/ // 学生实体类 Student.java │ ├── com/example/dao/ // 数据访问层 StudentDao.java │ ├── com/example/servlet/ // 控制器层 StudentServlet.java │ └── com/example/util/ // 工具类 DBUtil.java ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml // Servlet映射与欢迎页配置 │ │ └── lib/ // 数据库驱动、JSTL等jar包 │ ├── css/ // 页面样式 │ ├── js/ // 前端脚本 │ ├── index.jsp // 系统入口 │ ├── login.jsp // 登录页 │ ├── list.jsp // 学生列表页 │ └── add.jsp // 新增学生页 └── student.sql // 建库建表和初始化数据脚本这套结构里最容易被忽略的是WEB-INF/lib。很多新手把MySQL驱动jar放在项目的External Libraries里,一部署到Tomcat就报ClassNotFoundException,原因就是Tomcat运行时只认WEB-INF/lib和WEB-INF/classes下的类。另外注意web.xml的作用不只是配置欢迎页,它还把/student这样的路径和具体的Servlet类绑定起来,Servlet 3.0之后可以用注解替代,但课设包里的老项目大多还是走web.xml。
识别一个项目是不是传统JSP+Servlet,不用看代码,看两点:web.xml里有没有一堆<servlet-mapping>,以及src下有没有dao包。如果看到applicationContext.xml或springmvc-servlet.xml,那就是框架项目,本篇不适用。
2.2 一次“新增学生”请求,后端到底做了什么
理解了目录,再看一次完整请求。假设用户在 add.jsp 填了姓名、学号、班级,点提交。整个过程是:浏览器发POST请求到/student?action=add,Tomcat根据web.xml的映射找到对应Servlet,Servlet调用request.getParameter("name")拿到表单值,封装成Student对象,再调用StudentDao.addStudent(student),Dao里用JDBC的PreparedStatement执行INSERT,完成后Servlet重定向到list.jsp或转发并重新查一次列表。
// StudentServlet 中的 doPost 片段,省略了异常处理 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("add".equals(action)) { Student stu = new Student(); stu.setName(request.getParameter("name")); stu.setStudentNo(request.getParameter("studentNo")); stu.setClassName(request.getParameter("className")); StudentDao dao = new StudentDao(); int rows = dao.addStudent(stu); if (rows > 0) { response.sendRedirect("student?action=list"); } else { request.setAttribute("msg", "新增失败"); request.getRequestDispatcher("add.jsp").forward(request, response); } } }这段代码是这类项目的标配写法。几个细节值得注意:request.setCharacterEncoding("UTF-8")必须写在读取任何参数之前,否则中文乱码;sendRedirect是重定向,浏览器地址栏会变,刷新不会重复提交表单;forward是服务端转发,地址栏不变,适合携带错误信息回表单页。这里没有写事务和预编译之外的防护,但对课设场景够用了。
2.3 为什么要分dao包:把JDBC从Servlet里抽出来
不少新手写第一个版本时会把JDBC代码直接堆在Servlet的doGet里,跑通没问题,但后面改需求就很痛苦——查一下代码里有多少处Class.forName("com.mysql.cj.jdbc.Driver")就知道为什么需要dao层了。分层的核心目的是把“取数据”和“处理业务”分开。Servlet只需要调用dao.listStudents()得到一个List<Student>,至于这条SQL是去哪张表查的,查完连接有没有及时关掉,Servlet完全不关心。
// StudentDao 中的 findByName 方法,带模糊查询 public List<Student> findByName(String name) { List<Student> list = new ArrayList<>(); String sql = "SELECT id, name, student_no, class_name FROM student WHERE name LIKE ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + name + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student stu = new Student(); stu.setId(rs.getInt("id")); stu.setName(rs.getString("name")); stu.setStudentNo(rs.getString("student_no")); stu.setClassName(rs.getString("class_name")); list.add(stu); } } } catch (SQLException e) { e.printStackTrace(); } return list; }参数说明:preparedStatement的LIKE ?不能直接写%?%,需要先setString(1, "%" + name + "%")把通配符拼进参数里;try-with-resources会自动关闭Connection、PreparedStatement、ResultSet,省去在finally里手动close的啰嗦代码。如果项目用的是Java 7之前的写法,你会在Dao里看到大量的finally { try { rs.close(); } catch (Exception e) {} },这是老项目的标志,不必惊讶。
3. 从压缩包到可运行:JDK、Tomcat、MySQL与IDE的版本匹配与导入步骤
这一章解决的是“代码都对了却跑不起来”的经典问题。很多课设包是在某台特定机器上开发完打包的,开发者用的是JDK 8 + Tomcat 8.5 + MySQL 5.7,你本机装的是JDK 17 + Tomcat 10 + MySQL 8.0,直接导入必然报错。先做版本匹配,再谈导入,这个顺序不能反。
3.1 版本匹配:四件套的兼容关系表
我一般会把版本关系列成一张表贴在项目说明里,照着选,基本不会出大问题。
| 组件 | 推荐版本 | 不推荐版本 | 关键原因 |
|---|---|---|---|
| JDK | 8或11 | 17+(对老项目) | 老项目用旧语法,高版本JDK不一定兼容 |
| Tomcat | 8.5或9.0 | 10.0以上 | 包名从javax迁移到jakarta,旧代码全部报错 |
| MySQL | 5.7或8.0 | 无特殊限制 | 8.0需要用cj驱动,且要带时区参数 |
| MySQL驱动 | 5.1.49对应5.7,8.0.33对应8.0 | 驱动与数据库版本错配 | 低版本驱动连不上8.0,高版本会报时区错误 |
最简单省事的组合是:JDK 8 + Tomcat 8.5 + MySQL 5.7 + mysql-connector-java-5.1.49.jar。如果本机只有MySQL 8.0,那就用mysql-connector-java-8.0.33.jar,并把JDBC URL改成带serverTimezone=Asia/Shanghai的形式。Tomcat 10不是不能用,而是需要把代码里所有javax.servlet改成jakarta.servlet,课设包基本不会给你改,所以果断换回Tomcat 9以下。
3.2 IDEA导入源码:不要直接解压双击
拿到zip先解压到纯英文路径,比如D:\dev\student-management。路径有中文时,Tomcat能启动,但静态资源和JSP可能加载异常,且报错信息很隐晦。解压后用IntelliJ IDEA导入:
# 命令行里先确认环境,Windows和Mac对应执行 java -version mvn -version # 如果项目是Maven结构才需要然后按下面步骤操作:
- IDEA里选
File -> New -> Project from Existing Sources - 选中解压后的目录,选择
Import project from external model,课设包通常不是Maven,选IntelliJ IDEA projects或直接选Web模块 - 如果识别不到Web结构,手动在
Project Structure -> Facets里添加Web,指定Web根目录为web(或src/main/webapp) - 在
Artifacts里确保输出类型是Web Application: Exploded,并在Output Layout里看到WEB-INF/lib下包含MySQL驱动jar - 配置Tomcat:
Run -> Edit Configurations -> + -> Tomcat Server -> Local,在Deployment页把Artifact添加到Server中
做完这几步后,启动Tomcat,浏览器访问http://localhost:8080/。如果项目里有index.jsp,会自动跳转。这里最容易翻车的是第4步,IDEA默认不把External Libraries打包进Artifact,必须确认lib下的jar出现在Output Layout里。
3.3 导入数据库脚本:两种方式任选
项目里的student.sql通常包含建库、建表、插入测试数据三部分。我习惯先打开看一眼,确认它是否有CREATE DATABASE语句。如果有,直接整文件执行;如果没有,需要先手动建库再导入,否则会报No database selected。
# 方式一:命令行导入 mysql -u root -p < student.sql # 方式二:进入mysql后source执行 mysql -u root -p mysql> source D:/dev/student-management/student.sql; mysql> USE student_db; mysql> SELECT * FROM student;导入成功后,修改DBUtil.java里的连接参数。重点检查三项:URL中的数据库名、用户名、密码。常见的修改位置是com/example/util/DBUtil.java或db.properties。很多包默认密码写的是123456,你本地root密码不一样,不改的话启动不报错,但点查询时页面会白屏并抛Access denied for user。
4. 数据库脚本与核心链路:从建表语句到学生增删改查的完整闭环
跑通了环境,接下来要认真读代码。不要急着改功能,先找到最核心的一条闭环:建表SQL、DBUtil连接、Servlet接收参数、Dao执行SQL、JSP展示结果。这条链路走通,剩下的都是重复。
4.1 student.sql:建库建表与初始化数据
一个规范的student.sql通常长这样:
CREATE DATABASE IF NOT EXISTS student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; DROP TABLE IF EXISTS student; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, student_no VARCHAR(20) NOT NULL UNIQUE, class_name VARCHAR(50), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; INSERT INTO student (name, student_no, class_name) VALUES ('张三', '2023001', '计算机1班'), ('李四', '2023002', '计算机1班'), ('王五', '2023003', '软件2班');几个说明:utf8mb4比utf8多支持emoji和生僻字,老项目写的是utf8,建议建表时统一改成utf8mb4;student_no加UNIQUE约束防止重复学号,这是课设评分时喜欢看到的点;ENGINE=InnoDB支持事务,MyISAM不支持外键和行级锁,管理系统一律用InnoDB。如果你拿到手的脚本里没有这几项,重建表时主动加上,能省掉后面大量脏数据问题。
4.2 DBUtil:一个能少踩三个坑的JDBC连接工具
这可能是整个项目里最值得精读的类。代码不多,但参数写错任何一个,页面都会以“白屏+日志报错”的方式回报你。
package com.example.util; import java.sql.*; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/student_db?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数说明:useSSL=false,本地开发没有证书,true会警告甚至报错;serverTimezone=Asia/Shanghai,MySQL 8.0没有这参数会报时区异常,5.7可以不加;characterEncoding=utf8确保中文读写不乱码。注意Class.forName的驱动类名:MySQL 5.x驱动用com.mysql.jdbc.Driver,8.x驱动必须用com.mysql.cj.jdbc.Driver,用混了直接ClassNotFoundException。
如果你的项目里DBUtil是单个类,直接用上面的模板替换即可。如果项目用了db.properties或c3p0-config.xml,说明作者做了配置外置或连接池,这种项目更接近企业实际,DBUtil只负责加载配置文件。
4.3 StudentServlet:从request到resultSet的增删改查链路
看完了工具类,接着看Servlet。传统项目的Servlet通常用一个类处理多个操作,靠action参数区分。这段代码能看懂,整个系统的逻辑就通了。
package com.example.servlet; import com.example.dao.StudentDao; import com.example.entity.Student; import javax.servlet.ServletException; import javax.servlet.http.*; import java.io.IOException; import java.util.List; public class StudentServlet extends HttpServlet { private StudentDao dao = new StudentDao(); @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action = request.getParameter("action"); if ("list".equals(action) || action == null) { List<Student> list = dao.listAll(); request.setAttribute("students", list); request.getRequestDispatcher("list.jsp").forward(request, response); } else if ("delete".equals(action)) { int id = Integer.parseInt(request.getParameter("id")); dao.deleteById(id); response.sendRedirect("student?action=list"); } } @Override protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String action = request.getParameter("action"); if ("add".equals(action)) { Student stu = new Student(); stu.setName(request.getParameter("name")); stu.setStudentNo(request.getParameter("studentNo")); stu.setClassName(request.getParameter("className")); dao.addStudent(stu); response.sendRedirect("student?action=list"); } else if ("update".equals(action)) { // 更新逻辑与add类似,额外读取id并调用dao.updateById } } }这段代码里,doGet处理查询和删除,doPost处理新增和修改,分工明确。删除操作放在URL上以GET方式触发,虽然简单但不安全——容易被外部链接触发,这就是为什么不建议直接把delete写在<a href>里。更稳的做法是把删除写成一个独立Servlet,或者用POST表单提交。但课设项目里到处都是GET删除,理解它即可,不必强行改造。
4.4 list.jsp与add.jsp:JSP里如何展示和收集数据
JSP部分的关键是JSTL标签和EL表达式。老项目可能有<%=%>脚本片段,新一点的项目会用<c:forEach>遍历列表。下面这个片段是典型的列表页写法:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>学生列表</title></head> <body> <table border="1"> <tr> <th>ID</th><th>姓名</th><th>学号</th><th>班级</th><th>操作</th> </tr> <c:forEach items="${students}" var="stu"> <tr> <td>${stu.id}</td> <td>${stu.name}</td> <td>${stu.studentNo}</td> <td>${stu.className}</td> <td> <a href="student?action=edit&id=${stu.id}">编辑</a> <a href="student?action=delete&id=${stu.id}" onclick="return confirm('确定删除?');">删除</a> </td> </tr> </c:forEach> </table> <a href="add.jsp">新增学生</a> </body> </html>${stu.studentNo}看起来像直接访问字段,实际是调用Student类的getStudentNo()方法,所以实体类里的属性名要和数据库列名对应好。onclick="return confirm(...)"是前端层面预防误删的惯用技巧,但服务端依然需要校验参数,这是入门阶段容易忽略的点。如果访问页面出现javax.el.PropertyNotFoundException,基本就是实体类里没有对应getter,检查属性名是首字母大小写还是下划线风格不一致。
5. 部署运行与避坑排查:Tomcat启动失败的5个高频原因与解决记录
环境配对、代码读懂之后,进入实战中最折磨人的环节:部署运行。这一章我按“现象 → 原因 → 解决”记录5个高频问题,全部来自我处理过的真实报错现场。如果启动遇到问题,先看现象属于哪一类,再对症下药。
5.1 8080端口被占用导致Tomcat闪退
现象:Tomcat启动后立刻停止,控制台日志最后一行出现Port 8080 was already in use。原因:本机已有其他服务占用8080,常见来源是另一个Tomcat实例、Oracle数据库的HTTP端口、或者某些开发工具的代理服务。解决:先确认占用者,再决定换端口还是杀进程。
# Windows netstat -ano | findstr 8080 taskkill /PID <占用进程号> /F # Mac / Linux lsof -i :8080 kill -9 <PID>如果你不想动已有服务,直接改Tomcat端口更省事。打开Tomcat安装目录conf/server.xml,把<Connector port="8080"改成<Connector port="8081",然后重新启动。注意改端口后访问URL要同步改,http://localhost:8081/,以及项目里如果有写死的请求路径,也要一并检查。
5.2 MySQL 8.0驱动报ClassNotFoundException与时区异常
现象:启动或查询时报ClassNotFoundException: com.mysql.jdbc.Driver,或报The server time zone value ... is unrecognized。原因:驱动类名写错,或者JDBC URL缺时区参数。很多老包用的是MySQL 5.x的写法com.mysql.jdbc.Driver,配合MySQL 8.0数据库完全不兼容。解决:确认 lib 下驱动jar版本,若是8.x驱动,把DBUtil里的驱动类名改为com.mysql.cj.jdbc.Driver,URL中加上serverTimezone=Asia/Shanghai。
如果改完仍然报ClassNotFoundException,排除一个隐藏问题:jar包没有被IDEA部署到Tomcat。去Project Structure -> Artifacts -> Output Layout里看WEB-INF/lib下面是否有驱动jar,没有就手动加进去,再重新部署启动。
5.3 页面和数据库全乱码:三层编码必须一起设
现象:JSP页面中文正常,但写入MySQL后变成问号,或者页面直接显示一堆乱码。原因:字符集在JSP、Servlet、JDBC三个环节不一致。这是老项目最高频的问题,解决起来也要三步走。
<%@ page contentType="text/html;charset=UTF-8" language="java" %>Web.xml里加过滤器或者对每个Servlet设置:
request.setCharacterEncoding("UTF-8"); response.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8");JDBC URL里加:
jdbc:mysql://localhost:3306/student_db?characterEncoding=utf8这三处全部统一为UTF-8,基本不会乱码。另外连接MySQL时也可以在URL上增加connectionCollation=utf8mb4_unicode_ci,这属于一句“后悔药”,在前期就写上能省掉后续排查时间。
5.4 IDEA部署后访问404:Artifact里缺lib或Web根目录指定错误
现象:Tomcat能启动,但访问/student?action=list返回404,IDEA控制台没有明显报错。原因:部署到Tomcat的Artifact没有正确组装。最常见的两种情况:一是Artifact类型不是Web Application: Exploded,二是Web根目录指向了web但实际源码里是WebContent。解决:
- 打开
Project Structure -> Artifacts,确认类型 Output Layout里要有WEB-INF/classes和WEB-INF/lib- 看Tomcat的
Deployment页,确保Deploy的Artifact名称与访问路径一致
排查时最有用的动作是把Artifact的输出目录打开,本地直接看生成的文件里有没有WEB-INF/classes/com/example/servlet/StudentServlet.class。如果没有,说明编译或组装环节出问题,先Build -> Rebuild Project再重新部署。
5.5 表单提交后中文变问号:Tomcat的URIEncoding未设置
现象:登录账号名或新增学生姓名为中文时,控制台打印出的值变成??,但数据库里的旧中文数据正常。原因:GET请求的中文参数在Tomcat解析时用了默认编码。Tomcat 8以上默认UTF-8,但有时项目被迁移到旧配置仍然指定ISO-8859-1。解决:编辑Tomcat的conf/server.xml,在Connector上显式加参数。
<Connector port="8080" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" URIEncoding="UTF-8" />需要说明的是,这个参数对POST请求影响不大,POST乱码优先看request.setCharacterEncoding("UTF-8")是否写到了正确位置。如果是GET请求乱码,百分之八九十是这个URIEncoding的问题。改完server.xml要重启Tomcat,注意是重启整个Tomcat服务,不是IDEA里的Reload。
6. 拿到源码后怎么二开:从登录加密到分页再到替换JDBC的落地顺序
项目跑通只是起点。如果你要拿它交作业或者接受面试考察,只跑通不够,至少要做三件事:给登录加上密码加密、把列表改成真分页、把JDBC换成连接池或MyBatis。这三件事顺序不要乱,每完成一个就测试一次。
第一件事最小,也最能体现安全意识。原始的登录通常是查数据库比对明文密码,可以用MD5加盐改掉。所谓加盐,就是拼接一个随机字符串再计算哈希,防止彩虹表反查。
// 登录密码加密的简化实现 public static String md5WithSalt(String password, String salt) { String input = password + salt; try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] bytes = md.digest(input.getBytes("UTF-8")); StringBuilder sb = new StringBuilder(); for (byte b : bytes) { sb.append(String.format("%02x", b)); } return sb.toString(); } catch (Exception e) { throw new RuntimeException("加密失败", e); } }参数说明:salt可以用System.currentTimeMillis()生成并存入用户的salt字段;String.format("%02x", b)把字节转成两位十六进制,结果是32位字符串。注册时算密文入库,登录时取盐再算一次比对,明文密码永远不落库。注意MD5本身有已知碰撞,正式系统请用BCrypt,但给学生管理系统做加固,MD5加盐已经能体现出思路了。
第二件事是分页。把list.jsp一次查出全部记录的写法改成LIMIT ?, ?。需要传pageNo和pageSize两个参数,再查一条总数算出总页数。这里注意LIMIT的偏移量是(pageNo-1) * pageSize,且PreparedStatement中偏移量和条数都要setInt,不能拼进SQL字符串,否则有注入风险。
第三件事可以放在前两件事顺利完成之后,把DBUtil替换成Druid连接池,或者把Dao层改成MyBatis。替换时不要动Servlet的调用接口,只改Dao实现类的内部代码,这个边界守住了,回归测试就会非常快。如果你打算用MyBatis,引入mybatis.jar和mybatis-config.xml,把StudentDao里的方法名和Mapper里的select id="listAll"一一对应,Servlet层一行都不用改。
这套二开顺序实际是在训练一种能力:在老代码上加功能而不破坏原有逻辑。我自己的习惯是每改完一小步就重新部署一次,跑通再往下走,不要攒在一起改。这样即使出了新问题,也一定能定位到最近这次改动上。希望这篇笔记能帮你少走几段弯路,祝你顺利跑通这个项目。
本文还有配套的精品资源,点击获取