简介:这份资源是面向高校计算机相关专业学生的Java Web课程设计参考项目,采用JSP+Servlet+MySQL技术栈实现学生信息管理系统,适合正在准备期末大作业、课程设计或需要练手Java Web完整开发流程的初学者与中级学习者。压缩包共241个文件,约9.17MB,其中109个java源文件承载后端业务逻辑,56个jsp页面负责前端展示与交互,另有23个js脚本、8个css样式及15个jar依赖包,并附带sql建库脚本与项目说明文档,结构完整、层次清晰。目前已有247人学习下载,说明该方案在同类课设资源中具有一定参考价值。项目源码经过本地编译验证可正常运行,评审分达到98分,内容经助教老师审定,难度适中。读者可据此快速理解分层架构、数据库表设计与前后端数据交互方式,并在此基础上完成功能扩展与二次开发,满足学习、答辩与提交需求。
1. Java+JSP+MySQL 学生信息管理系统:为什么这套“老技术栈”仍是课程设计最稳的落地方案
如果你正在搜“Java+JSP+Mysql实现Web学生信息管理系统源码+数据库+文档说明”,大概率不是想研究什么前沿架构,而是要在有限时间里交出一个能跑、能演示、能讲清楚、还能应付答辩追问的完整 Web 项目。这套组合看着“老”,但它恰好卡在课程设计最舒服的位置:Java 负责业务逻辑,JSP 负责页面渲染,MySQL 负责数据持久化,三层职责清晰,环境依赖少,一台普通笔记本就能跑通。更关键的是,它不像 Spring Boot 那样把大量细节藏在自动配置里,你写的每一行代码都能在答辩时讲出“为什么”。所以这篇文章不聊虚的,直接按“环境怎么搭、库表怎么建、JSP 页面怎么写、增删改查怎么串、坑在哪”这条线,把一套可复现的学生信息管理系统讲透。适合正在做课程设计、毕设选题,或者想用 JSP Model2 思想练手 JavaWeb 的从业者和学生。
2. 环境与工程骨架:把 JDK、Tomcat、MySQL 和目录结构一次定死
2.1 版本选型:为什么我建议 JDK 8 + Tomcat 8.5 + MySQL 5.7/8.0
这套技术栈的版本选择,直接决定你后面会不会被“玄学报错”折磨。血泪经验是:不要追新。JDK 8 对 JSP/Servlet 的兼容性最稳,Tomcat 8.5 对 Servlet 3.1 支持完整,MySQL 5.7 和 8.0 在语法上差异不大,但 8.0 的驱动类名和时区配置变了,很多人第一次连库就翻车在这里。
| 组件 | 推荐版本 | 关键原因 |
|---|---|---|
| JDK | 1.8 | 与 Tomcat 8.5、旧版 JSTL 兼容性最好 |
| Tomcat | 8.5.x | Servlet 3.1,支持注解配置,启动快 |
| MySQL | 5.7 或 8.0 | 5.7 省心,8.0 需注意驱动和时区 |
| JDBC 驱动 | mysql-connector-java 5.1.x(配 5.7)/ 8.0.x(配 8.0) | 版本必须和数据库对应 |
| IDE | Eclipse / IntelliJ IDEA | 能配 Tomcat 即可,不挑 |
安装 MySQL 时,8.0 版本一定要记住 root 密码,并在安装向导里勾选“Use Legacy Authentication Method”,否则后面 JDBC 连接会报Unable to load authentication plugin 'caching_sha2_password'。这是新手最容易卡住的一步。
2.2 工程目录结构:Model2 思想下的标准分层
按 JSP Model2 思想,Servlet 当控制器,JSP 只做视图,JavaBean 做模型。目录结构建议这样定:
StudentMS/ ├── src/ │ └── com/student/ │ ├── dao/ # 数据库访问层 │ ├── entity/ # 实体类 │ ├── servlet/ # 控制器 │ └── util/ # DBUtil 工具类 ├── WebContent/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # 放 mysql-connector-java.jar │ ├── css/ │ ├── js/ │ ├── index.jsp # 登录页 │ ├── studentList.jsp # 学生列表 │ └── studentForm.jsp # 新增/编辑表单这个结构的好处是:WEB-INF下的 JSP 不能被浏览器直接访问,必须经过 Servlet 转发,天然实现了“控制器先行”的 Model2 约束。很多人把 JSP 全放在根目录,结果页面能被直接打开,业务逻辑绕过了 Servlet,答辩时被问“你的控制器在哪”就答不上来。
2.3 数据库连接工具类:一次写对,后面不再改
package com.student.util; import java.sql.*; public class DBUtil { // MySQL 8.0 用 com.mysql.cj.jdbc.Driver // MySQL 5.7 用 com.mysql.jdbc.Driver private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/student_db" + "?useUnicode=true&characterEncoding=utf8" + "&serverTimezone=Asia/Shanghai&useSSL=false"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("JDBC驱动加载失败", 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 ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }逻辑说明:静态代码块保证驱动只加载一次;getConnection()每次返回新连接,适合课程设计规模;close()按 ResultSet → Statement → Connection 顺序关闭,避免资源泄漏。参数说明:serverTimezone在 MySQL 8.0 下必须加,否则报时区错误;useSSL=false避免本地连接时的 SSL 警告;characterEncoding=utf8保证中文不乱码。如果你的 MySQL 是 5.7,把驱动类改成com.mysql.jdbc.Driver,URL 里的serverTimezone可以去掉。
3. 库表设计与实体映射:学生、班级、用户三张表怎么建才不返工
3.1 建库建表 SQL:字段类型和约束一次到位
CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(2) DEFAULT '男' COMMENT '性别', age INT DEFAULT 0 COMMENT '年龄', class_id INT COMMENT '班级ID', phone VARCHAR(20) COMMENT '联系电话', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE class_info ( id INT PRIMARY KEY AUTO_INCREMENT, class_name VARCHAR(50) NOT NULL COMMENT '班级名称', major VARCHAR(50) COMMENT '专业' ); CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(10) DEFAULT 'admin' ); INSERT INTO sys_user (username, password) VALUES ('admin', '123456'); INSERT INTO class_info (class_name, major) VALUES ('计科2101', '计算机科学与技术');逻辑说明:sno加 UNIQUE 约束,防止学号重复录入;class_id作为外键关联班级表,但课程设计里不强制加 FOREIGN KEY,方便删改;create_time默认当前时间,省去手动赋值。参数说明:utf8mb4比utf8更完整,能存 emoji;VARCHAR(64)存密码,如果做 MD5 加密也够用。注意:MySQL 8.0 默认字符集已经是 utf8mb4,但显式写出来更保险。
3.2 实体类与 DAO:把 ResultSet 映射成对象的标准写法
package com.student.entity; public class Student { private int id; private String sno; private String name; private String gender; private int age; private int classId; private String phone; // 省略 getter/setter,IDE 一键生成 }package com.student.dao; import com.student.entity.Student; import com.student.util.DBUtil; import java.sql.*; import java.util.*; public class StudentDao { public List<Student> findAll() { List<Student> list = new ArrayList<>(); String sql = "SELECT * FROM student ORDER BY id DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { Student s = new Student(); s.setId(rs.getInt("id")); s.setSno(rs.getString("sno")); s.setName(rs.getString("name")); s.setGender(rs.getString("gender")); s.setAge(rs.getInt("age")); s.setClassId(rs.getInt("class_id")); s.setPhone(rs.getString("phone")); list.add(s); } } catch (SQLException e) { e.printStackTrace(); } return list; } public int insert(Student s) { String sql = "INSERT INTO student(sno,name,gender,age,class_id,phone) VALUES(?,?,?,?,?,?)"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, s.getSno()); ps.setString(2, s.getName()); ps.setString(3, s.getGender()); ps.setInt(4, s.getAge()); ps.setInt(5, s.getClassId()); ps.setString(6, s.getPhone()); return ps.executeUpdate(); } catch (SQLException e) { e.printStackTrace(); return 0; } } }逻辑说明:用 try-with-resources 自动关闭连接,比手动 close 更安全;PreparedStatement用占位符,天然防 SQL 注入;findAll()返回 List,方便 JSP 用 JSTL 遍历。参数说明:ORDER BY id DESC让最新录入的学生排前面;executeUpdate()返回受影响行数,大于 0 表示成功。注意:如果字段名和实体属性名不一致(如class_idvsclassId),映射时要手动对应,别指望自动映射。
3.3 分页查询:课程设计里最容易被追问的点
学生列表数据一多,不分页就显得很业余。最简单的分页 SQL:
SELECT * FROM student ORDER BY id DESC LIMIT ?, ?;第一个?是偏移量(pageNo - 1) * pageSize,第二个?是每页条数。在 DAO 里加一个findByPage(int pageNo, int pageSize)方法即可。答辩时如果被问“总页数怎么算”,回答:先SELECT COUNT(*) FROM student拿到总数,再Math.ceil(total * 1.0 / pageSize)。这是最朴素也最稳的做法。
4. JSP 页面与 Servlet 控制器:登录、列表、增删改查怎么串起来
4.1 登录功能:Session 存用户,过滤器拦未登录请求
登录 Servlet 核心逻辑:
@WebServlet("/login") public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username = req.getParameter("username"); String password = req.getParameter("password"); UserDao dao = new UserDao(); User user = dao.findByUsernameAndPassword(username, password); if (user != null) { req.getSession().setAttribute("currentUser", user); resp.sendRedirect("student?action=list"); } else { req.setAttribute("msg", "用户名或密码错误"); req.getRequestDispatcher("index.jsp").forward(req, resp); } } }逻辑说明:登录成功后把 User 对象放进 Session,后续页面通过session.getAttribute("currentUser")判断是否登录;失败则转发回登录页并带错误提示。参数说明:sendRedirect是重定向,地址栏会变,避免刷新重复提交;forward是转发,地址栏不变,适合带错误信息回显。
再配一个登录过滤器:
@WebFilter("/*") public class LoginFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行登录页、登录请求、静态资源 if (uri.endsWith("index.jsp") || uri.endsWith("/login") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } if (request.getSession().getAttribute("currentUser") == null) { response.sendRedirect("index.jsp"); return; } chain.doFilter(req, resp); } }逻辑说明:过滤器拦截所有请求,白名单放行登录相关和静态资源,其余检查 Session。参数说明:@WebFilter("/*")表示拦截全部;uri.endsWith判断比contains更精确,避免误放行。注意:如果用了 JSTL 标签,/WEB-INF/lib下的 jar 不需要放行,因为不是浏览器直接请求。
4.2 学生列表页:JSTL 遍历 + 删除确认
<%@ page contentType="text/html;charset=UTF-8" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <html> <head><title>学生列表</title></head> <body> <h2>学生信息列表</h2> <a href="studentForm.jsp">新增学生</a> <table border="1" width="80%"> <tr> <th>学号</th><th>姓名</th><th>性别</th> <th>年龄</th><th>电话</th><th>操作</th> </tr> <c:forEach items="${studentList}" var="s"> <tr> <td>${s.sno}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.age}</td> <td>${s.phone}</td> <td> <a href="student?action=edit&id=${s.id}">编辑</a> <a href="student?action=delete&id=${s.id}" onclick="return confirm('确定删除?')">删除</a> </td> </tr> </c:forEach> </table> </body> </html>逻辑说明:<c:forEach>遍历 Servlet 放进 request 的studentList;${s.sno}是 EL 表达式,等价于s.getSno();删除前用confirm弹窗二次确认,防止误操作。参数说明:taglib引入 JSTL 核心库,需要jstl.jar和standard.jar放进WEB-INF/lib;如果页面报The absolute uri cannot be resolved,就是 jar 没放对。
4.3 统一控制器:一个 Servlet 用 action 参数分发
@WebServlet("/student") public class StudentServlet extends HttpServlet { private StudentDao dao = new StudentDao(); protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action = req.getParameter("action"); if (action == null) action = "list"; switch (action) { case "list": req.setAttribute("studentList", dao.findAll()); req.getRequestDispatcher("studentList.jsp").forward(req, resp); break; case "delete": dao.deleteById(Integer.parseInt(req.getParameter("id"))); resp.sendRedirect("student?action=list"); break; case "edit": req.setAttribute("student", dao.findById( Integer.parseInt(req.getParameter("id")))); req.getRequestDispatcher("studentForm.jsp").forward(req, resp); break; default: resp.sendRedirect("student?action=list"); } } protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); Student s = new Student(); s.setSno(req.getParameter("sno")); s.setName(req.getParameter("name")); s.setGender(req.getParameter("gender")); s.setAge(Integer.parseInt(req.getParameter("age"))); s.setClassId(Integer.parseInt(req.getParameter("classId"))); s.setPhone(req.getParameter("phone")); String id = req.getParameter("id"); if (id == null || id.isEmpty()) { dao.insert(s); } else { s.setId(Integer.parseInt(id)); dao.update(s); } resp.sendRedirect("student?action=list"); } }逻辑说明:GET 处理查询和删除,POST 处理新增和修改;用action参数分发,避免写多个 Servlet;新增和修改共用一个表单,靠隐藏域id是否为空区分。参数说明:req.setCharacterEncoding("UTF-8")必须在取参数之前调用,否则中文乱码;sendRedirect后跟return或直接结束方法,避免继续执行。
5. 避坑与排查:这套系统最容易翻车的 5 个地方
5.1 中文乱码:POST 和 GET 要分开治
现象:表单提交后,数据库里存的是???或者页面显示乱码。原因:POST 请求默认 ISO-8859-1 解码,GET 请求在 Tomcat 8.5 里由server.xml的URIEncoding决定。解决:POST 在 Servlet 第一行加req.setCharacterEncoding("UTF-8");GET 要么改 Tomcat 的server.xml加URIEncoding="UTF-8",要么在代码里手动new String(str.getBytes("ISO-8859-1"), "UTF-8")。JSP 页面头部必须写<%@ page contentType="text/html;charset=UTF-8" %>,数据库连接 URL 加characterEncoding=utf8。三处都对齐,乱码基本绝迹。
5.2 驱动加载失败:ClassNotFoundException
现象:启动时报java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因:mysql-connector-java.jar没放进WEB-INF/lib,或者只加到了 IDE 的 build path 但没随项目部署。解决:确认 jar 在WEB-INF/lib下,Eclipse 里右键项目 → Properties → Deployment Assembly 确认 jar 被部署到/WEB-INF/lib。MySQL 8.0 用com.mysql.cj.jdbc.Driver,5.7 用com.mysql.jdbc.Driver,别搞混。
5.3 时区报错:The server time zone value is unrecognized
现象:连接 MySQL 8.0 时报The server time zone value '?D1ú±ê×?ê±??' is unrecognized。原因:MySQL 8.0 默认时区是系统时区,JDBC 驱动无法识别中文时区名。解决:URL 里加serverTimezone=Asia/Shanghai,或者登录 MySQL 执行SET GLOBAL time_zone = '+8:00'。前者更省事,推荐直接写在连接串里。
5.4 JSTL 标签不生效:页面直接输出 ${s.name}
现象:JSP 页面把${s.name}原样显示出来,没有被解析。原因:缺少 JSTL jar,或者web.xml版本太低导致 EL 被禁用。解决:把jstl.jar和standard.jar放进WEB-INF/lib;确认web.xml的web-app版本是 3.0 以上,并且没有<%@ page isELIgnored="true" %>。如果用的是 Tomcat 8.5,Servlet 3.1 默认开启 EL,不需要额外配置。
5.5 删除后列表不刷新:浏览器缓存了旧页面
现象:删除一条记录后,列表页还是显示旧数据。原因:sendRedirect后浏览器可能从缓存加载列表页。解决:在列表 Servlet 里设置响应头resp.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"),或者删除后重定向时加一个时间戳参数student?action=list&t=System.currentTimeMillis()。最稳的是前者,一行代码解决。
6. 进阶技巧:把课程设计做出“可讲性”的三个加分项
6.1 用 MD5 加密密码,答辩时能多讲两分钟
课程设计里密码明文存库是常态,但如果你加一层 MD5,答辩老师问“安全性怎么考虑”时就有话可说。写一个工具类:
public class MD5Util { public static String md5(String input) { try { MessageDigest md = MessageDigest.getInstance("MD5"); byte[] digest = md.digest(input.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); } } }登录时把用户输入的密码md5()后再和数据库比对,注册时也存 MD5 值。注意:MD5 现在不算安全,但课程设计里足够体现“有安全意识”。如果想再进一步,可以讲“加盐”的思路,不用真实现。
6.2 用过滤器统一编码,少写重复代码
前面每个 Servlet 都写setCharacterEncoding很烦,可以抽一个 EncodingFilter:
@WebFilter("/*") public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { req.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }这样所有请求的编码一次性解决,Servlet 里不用再写。注意:多个 Filter 的执行顺序由web.xml里的<filter-mapping>顺序决定,如果用注解,顺序不确定,所以编码过滤器最好用web.xml配置并放在最前面。
6.3 导出 Excel:一个容易被忽略但很加分的功能
学生列表页加一个“导出”按钮,用 POI 把数据写成 Excel。核心代码:
// 在 Servlet 里 resp.setContentType("application/vnd.ms-excel"); resp.setHeader("Content-Disposition", "attachment;filename=students.xls"); HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("学生信息"); HSSFRow header = sheet.createRow(0); header.createCell(0).setCellValue("学号"); header.createCell(1).setCellValue("姓名"); // ... 遍历 studentList 填充数据 workbook.write(resp.getOutputStream()); workbook.close();需要引入poi.jar。这个功能代码量不大,但演示时很直观,答辩老师看到“还能导出”通常会点头。注意:Content-Disposition的文件名如果含中文,要做 URL 编码,否则下载下来文件名乱码。
6.4 我自己的习惯:先跑通一条线,再复制粘贴
做这类系统,我一般不会一上来就把所有功能写完。先跑通“登录 → 列表 → 新增”这一条线,确认数据库连通、编码正确、页面能跳转,然后再复制这套模式去写删除、修改、分页。这样一旦出问题,排查范围小,不会在一堆代码里找 bug。另外,每改完一个功能就重启 Tomcat 测一次,别攒着一起测,否则报错都不知道是哪一步引入的。希望帮到你。
本文还有配套的精品资源,点击获取