简介:这份资源是面向Java Web初学者与课程实践者的学生信息管理系统完整项目,以JSP与Servlet为核心,结合MVC设计模式串联起动态页面展示、请求处理与数据模型,适合用于理解Java Web开发流程、完成课程设计或作为入门实战案例。压缩包共576个文件,约2.75MB,包含html、css、png、js等前端页面与静态资源,java与class源码及编译产物,以及jar依赖、jsp页面、sql初始化脚本和json、xml等配置数据,结构上覆盖了从界面到数据库的完整链路。已有999人学习下载。项目涉及JDBC增删改查、HTTP请求响应、会话管理、容器部署与异常日志等知识点,读者可据此梳理Servlet与DAO分层写法、数据库建表脚本的导入方式以及前端表单验证思路,是较为系统的Java Web入门参考。
1. 接手一个 JSP + Servlet 学生信息管理系统,先看清它到底能干什么
很多 Java Web 初学者在找练手项目时,会翻到一堆“学生信息管理系统”的源码包,下载下来打开一看,满屏的.jsp和Servlet,不知道从哪下手。我这次拆的就是这样一个典型项目:用原生 JSP + Servlet 实现的学生信息管理系统,没有 Spring、没有 MyBatis,纯 Servlet 容器跑起来,数据库用 JDBC 直连。它适合两类人:一是刚学完 Java Web 基础、想找一个能跑通完整 CRUD 流程的练手项目;二是想理解“不用框架时,一个请求到底怎么从浏览器走到数据库再返回”的开发者。项目核心就是学生信息的增删改查、分页列表、登录鉴权这几块,麻雀虽小,但 MVC 分层、过滤器、会话管理这些关键点一个不少,拿来当 Java Web 的“解剖样本”正合适。
2. 项目结构与技术栈:没有框架的 MVC 是怎么落地的
2.1 目录结构决定了一切
拿到源码包,第一件事不是急着跑,而是先看目录。这个项目的结构非常“教科书式”,典型 Maven Web 工程或者 Eclipse Dynamic Web Project 的布局:
StudentMS/ ├── src/ │ └── main/ │ ├── java/ │ │ └── com/example/sms/ │ │ ├── controller/ # Servlet 层 │ │ ├── service/ # 业务逻辑层 │ │ ├── dao/ # 数据访问层 │ │ ├── entity/ # 实体类 │ │ └── util/ # JDBC 工具类 │ ├── resources/ │ │ └── db.properties # 数据库配置 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml # Servlet 映射、过滤器配置 │ │ └── lib/ # 依赖 jar │ ├── jsp/ # 视图页面 │ │ ├── login.jsp │ │ ├── student_list.jsp │ │ └── student_edit.jsp │ └── index.jsp └── pom.xml这个分层不是摆设。controller里每个 Servlet 对应一组操作,service做业务判断(比如学号是否重复),dao只负责拼 SQL 和执行,entity是纯 POJO。理解了这个结构,后面改功能就知道该动哪一层,不会把 SQL 写到 JSP 里。
2.2 技术栈清单与选型逻辑
| 层次 | 技术 | 说明 |
|---|---|---|
| 视图 | JSP + JSTL | 用<c:forEach>渲染列表,避免脚本片段 |
| 控制 | Servlet 3.x | 注解或 web.xml 映射 |
| 业务 | 普通 Java 类 | 无框架,手动 new |
| 持久 | JDBC + PreparedStatement | 防 SQL 注入 |
| 数据库 | MySQL | 驱动 mysql-connector-java |
| 会话 | HttpSession | 登录状态保持 |
| 过滤 | Filter | 编码统一、登录拦截 |
为什么不用框架?因为这个项目的价值就在于“裸写”。你在这里能看清request.getParameter()拿到的是什么、response.sendRedirect()和forward()的区别、连接池不配时每次DriverManager.getConnection()有多慢。这些细节在 Spring 里被封装掉了,但排查问题时恰恰需要这些底层认知。
2.3 数据库表设计
学生表通常长这样,字段不多但够用:
CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, sno VARCHAR(20) NOT NULL UNIQUE COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender VARCHAR(4) DEFAULT '男', age INT, major VARCHAR(50), phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP );user表存登录账号,字段是id、username、password。注意sno加了唯一约束,这是后面做“学号重复校验”的数据库兜底。很多新手只在 Service 层查一次就插入,并发下会翻车,唯一索引才是最后一道防线。
3. 核心链路拆解:一个学生列表请求的完整旅程
3.1 从浏览器到 Servlet:映射与参数接收
用户在地址栏输入/student/list?page=1,容器根据web.xml或@WebServlet注解找到对应的 Servlet。这个项目的列表 Servlet 核心代码如下:
@WebServlet("/student/list") public class StudentListServlet extends HttpServlet { private StudentService studentService = new StudentServiceImpl(); @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { // 1. 接收分页参数,做默认值兜底 String pageStr = req.getParameter("page"); int page = (pageStr == null || pageStr.isEmpty()) ? 1 : Integer.parseInt(pageStr); int pageSize = 10; // 2. 调用 Service 拿分页数据 PageResult<Student> result = studentService.findByPage(page, pageSize); // 3. 数据放入 request 域,转发到 JSP req.setAttribute("pageResult", result); req.getRequestDispatcher("/jsp/student_list.jsp").forward(req, resp); } }这里有几个关键点。第一,page参数必须做空值和非法值兜底,否则用户手动改 URL 传个abc进来,Integer.parseInt直接抛NumberFormatException,页面就是 500。第二,用forward而不是redirect,因为要把request域里的数据带到 JSP,重定向会丢数据。第三,Servlet 里不写任何 SQL 和业务判断,只做参数接收和视图调度,这是分层的底线。
3.2 Service 层:分页逻辑与业务校验
Service 层是很多人容易写薄的地方。分页不只是“查第几页”,还要算总页数、处理页码越界:
public class StudentServiceImpl implements StudentService { private StudentDao studentDao = new StudentDaoImpl(); @Override public PageResult<Student> findByPage(int page, int pageSize) { // 总记录数 int total = studentDao.count(); // 总页数,向上取整 int totalPages = (total + pageSize - 1) / pageSize; // 页码越界修正 if (page < 1) page = 1; if (page > totalPages && totalPages > 0) page = totalPages; int offset = (page - 1) * pageSize; List<Student> list = studentDao.findByPage(offset, pageSize); PageResult<Student> pr = new PageResult<>(); pr.setList(list); pr.setPage(page); pr.setTotalPages(totalPages); pr.setTotal(total); return pr; } }(total + pageSize - 1) / pageSize这个向上取整的写法比Math.ceil更稳,因为整数除法不会引入浮点误差。页码越界修正是血泪经验:用户在第 1 页删光了最后一页的数据,再点“下一页”就会查到空列表,不修正的话页面显示“第 2 页,共 1 页”,很别扭。
3.3 DAO 层:PreparedStatement 与结果集映射
DAO 层只做一件事:执行 SQL 并映射结果。分页查询用LIMIT offset, size:
public class StudentDaoImpl implements StudentDao { @Override public List<Student> findByPage(int offset, int pageSize) { String sql = "SELECT id, sno, name, gender, age, major, phone FROM student " + "ORDER BY id DESC LIMIT ?, ?"; List<Student> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, offset); ps.setInt(2, pageSize); try (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.setMajor(rs.getString("major")); s.setPhone(rs.getString("phone")); list.add(s); } } } catch (SQLException e) { throw new RuntimeException("分页查询学生失败", e); } return list; } }用 try-with-resources 确保Connection、PreparedStatement、ResultSet都能关闭,这是不配连接池时最容易漏的地方,漏几次数据库连接数就满了。LIMIT ?, ?用占位符而不是字符串拼接,既防注入也避免类型问题。结果集映射手动写虽然啰嗦,但字段名和实体属性一一对应,改字段时不容易漏。
3.4 JSP 视图:JSTL 渲染与分页条
JSP 里不要写<% %>脚本片段,用 JSTL 和 EL:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <table> <c:forEach items="${pageResult.list}" var="s"> <tr> <td>${s.sno}</td> <td>${s.name}</td> <td>${s.gender}</td> <td>${s.major}</td> <td> <a href="${pageContext.request.contextPath}/student/edit?id=${s.id}">编辑</a> <a href="javascript:del(${s.id})">删除</a> </td> </tr> </c:forEach> </table> <div class="pager"> 第 ${pageResult.page} / ${pageResult.totalPages} 页 <c:if test="${pageResult.page > 1}"> <a href="?page=${pageResult.page - 1}">上一页</a> </c:if> <c:if test="${pageResult.page < pageResult.totalPages}"> <a href="?page=${pageResult.page + 1}">下一页</a> </c:if> </div>${pageContext.request.contextPath}不能省,否则部署到非根路径(比如/sms)时链接全 404。分页条的“上一页/下一页”用<c:if>控制边界,第一页不显示上一页,最后一页不显示下一页。
4. 跑起来:环境准备、建库与部署的完整步骤
4.1 环境与依赖确认
这个项目对版本不挑,但组合要对:
- JDK 8 或 11
- Tomcat 8.5 或 9(Tomcat 10 的包名从
javax.servlet变成jakarta.servlet,直接跑会报ClassNotFoundException) - MySQL 5.7 或 8.0
- Maven 3.6+
先看pom.xml里的依赖,核心就三个:javax.servlet-api(scope 是 provided)、jstl、mysql-connector-java。如果项目是 Eclipse 工程没有 pom,就去WEB-INF/lib下确认 jar 在不在。
4.2 建库建表与配置修改
按第 2 章的建表语句执行,再插一条管理员账号:
INSERT INTO user (username, password) VALUES ('admin', '123456');然后改src/main/resources/db.properties:
jdbc.url=jdbc:mysql://localhost:3306/student_ms?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码 jdbc.driver=com.mysql.cj.jdbc.DriverMySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver,5.x 是com.mysql.jdbc.Driver,写错就是ClassNotFoundException。serverTimezone不配,MySQL 8 下时间字段可能报时区错误。
4.3 部署与首次访问
Maven 项目直接mvn clean package打 war 包,丢进 Tomcat 的webapps目录,启动后访问http://localhost:8080/StudentMS/。Eclipse 工程就右键 Run on Server。首次访问会被登录过滤器拦到login.jsp,用admin/123456登录后进入列表页。
如果启动报NoClassDefFoundError: javax/servlet/jsp/jstl/core/Config,说明 JSTL jar 没进WEB-INF/lib或者 Maven 依赖 scope 写成了 provided。这类问题看启动日志第一行异常就能定位,别急着改代码。
5. 避坑与排查:那些让项目跑不起来的常见问题
5.1 中文乱码:POST 和 GET 要分开治
现象:表单提交的中文姓名存进数据库变成???。原因:Tomcat 默认按 ISO-8859-1 解码请求体。解决:在web.xml配一个编码过滤器,或者每个 Servlet 开头req.setCharacterEncoding("UTF-8")。GET 请求的参数编码还跟 Tomcat 的server.xml里Connector的URIEncoding有关,Tomcat 8 以后默认 UTF-8,老版本要手动加。我一般直接上过滤器,一劳永逸:
@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); } }5.2 登录后仍然跳回登录页:过滤器路径没排除
现象:登录成功却一直循环跳登录页。原因:登录过滤器拦截了/*,把login.jsp和登录 Servlet 也拦了。解决:在过滤器里放行登录相关路径和静态资源:
String uri = ((HttpServletRequest) req).getRequestURI(); if (uri.contains("/login") || uri.contains("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; }5.3 删除后列表页码错乱
现象:在第 3 页删掉最后一条记录,页面空白。原因:删除后重定向回原页码,但总页数已经减一。解决:删除成功后重定向到列表时,让列表 Servlet 自己做页码越界修正(第 3.2 节的逻辑),或者删除后统一回第 1 页。前者体验更好。
5.4 数据库连接耗尽
现象:用一段时间后报Too many connections。原因:DAO 里Connection没关,或者异常路径下没走到close()。解决:全部改 try-with-resources;有条件的话引入 Druid 或 HikariCP 连接池,把DBUtil.getConnection()换成从池里取。这个项目原生写法每次新建连接,压测几十个并发就顶不住,这是它作为教学项目的局限,二次开发时优先补这块。
5.5 Tomcat 10 直接跑报包名错误
现象:java.lang.NoClassDefFoundError: javax/servlet/http/HttpServlet。原因:Tomcat 10 起 Servlet API 包名迁移到jakarta.servlet。解决:要么换 Tomcat 9,要么把源码里所有javax.servlet改成jakarta.servlet并换依赖坐标。教学项目建议直接用 Tomcat 9,省得改一堆 import。
6. 二次开发:从教学项目到能用的系统,我会先动这三处
这个项目跑通之后,大多数人会想加点东西。我的建议是按“先稳后扩”的顺序改,别一上来就堆功能。
第一处是连接池。原生DriverManager每次请求建连接,是性能瓶颈也是稳定性隐患。引入 HikariCP 只需要改DBUtil:
public class DBUtil { private static final HikariDataSource DS; static { HikariConfig config = new HikariConfig(); config.setJdbcUrl(Props.get("jdbc.url")); config.setUsername(Props.get("jdbc.username")); config.setPassword(Props.get("jdbc.password")); config.setMaximumPoolSize(10); config.setMinimumIdle(2); DS = new HikariDataSource(config); } public static Connection getConnection() throws SQLException { return DS.getConnection(); } }改完之后 DAO 层一行不用动,因为对外暴露的还是getConnection()。maximumPoolSize按数据库承受能力设,教学环境 10 足够。
第二处是密码存储。明文存密码是教学项目的通病,改成 BCrypt 哈希,登录时用BCrypt.checkpw()比对。这一步改动小,但能让你理解“为什么不能明文存”。
第三处是搜索与导出。列表页加一个按姓名/学号模糊查询的输入框,DAO 里拼WHERE name LIKE ? OR sno LIKE ?,注意%要拼在参数值里而不是 SQL 里。导出用 Apache POI 写 Excel,或者简单点用 CSV,一个 Servlet 输出流就能搞定。
验证改动是否成功,我习惯先跑一遍“登录 → 新增 → 搜索 → 编辑 → 删除 → 翻页”的完整链路,再看 Tomcat 日志有没有异常堆栈。别只测正常路径,手动传个page=-1、id=abc进去,看兜底逻辑扛不扛得住。从那以后我每次接手这类裸 Servlet 项目,都强制先把编码过滤器和连接池这两件事做掉再谈功能,否则后面每加一个模块都在给前面的坑还债。希望帮到你。
本文还有配套的精品资源,点击获取