简介:这是一套面向JavaWeb初学者与进阶练习者的综合实战项目源码,以「快递e栈」业务为背景,帮助读者把零散的Servlet、Filter、Session、MVC等知识点串联成完整可运行的系统。项目覆盖HTML、CSS、JavaScript、jQuery、Bootstrap、Layui、Layer、Ajax等前端技术,后端涉及JSP、Servlet、Filter、Listener与MySQL数据库,并部署于Tomcat及公有云服务器,适合课程设计、毕业设计或面试前的项目复盘。压缩包共831个文件,约17.23MB,其中png与gif图片234与160个,class与java文件109与52个,js、html、css分别有72、58、38个,另含jar依赖、xml配置、properties及字体等资源,结构完整。目前已有886人学习下载。读者可从中获得分层清晰的MVC目录结构、前后端交互与过滤器会话控制的实现思路,以及数据库访问与页面渲染的完整参考,便于对照排错与二次开发。
1. 快递e栈项目到底在解决什么问题
很多同学做 JavaWeb 课设时,第一反应是“先建表、再写 Servlet、最后拼页面”,结果做到一半发现登录状态到处乱传、权限控制全靠 if-else 堆、改一个字段要动五个文件。快递e栈这个项目的价值,恰恰在于它用一个真实业务场景——快递代收点的入库、取件、查询——把 Servlet、Filter、Session、MVC 这几个核心机制串成了一条完整的链路。它适合正在做 JavaWeb 毕设或课设的学生,也适合想从“能跑就行”过渡到“结构清晰、能讲清楚”的开发者。这套方案要解决的不是“怎么连数据库”,而是“怎么让一个 Web 项目在功能增长时不失控”。
2. 需求拆解与技术选型:为什么是 Servlet + Filter + Session + MVC
2.1 业务角色与核心用例
快递e栈的业务并不复杂,但角色边界必须一开始就划清楚。通常的做法是拆成三类角色:管理员、快递员(或入库操作员)、取件用户。管理员负责网点信息维护和用户管理;快递员负责快件入库、修改状态;取件用户通过取件码或手机号查询自己的快件。
核心用例可以收敛为六个:用户登录与注销、快件入库登记、快件状态变更(待取件→已取件)、按取件码查询、按手机号查询历史、管理员查看当日入库统计。这六个用例覆盖了从请求进入到数据落库再到页面渲染的完整路径,也正好对应 MVC 中 Controller、Service、DAO、View 四层的职责划分。
注意:不要一上来就设计十几张表。快递e栈的核心表通常只有三张——用户表、快件表、网点表。表越少,MVC 各层的职责越容易讲清楚。
2.2 为什么不用框架而坚持原生 Servlet
这是课设和毕设里最容易被质疑的点。我的判断是:如果目标是理解 HTTP 请求的生命周期、Session 的创建与销毁、Filter 链的执行顺序,那原生 Servlet 比 Spring MVC 更合适。Spring 帮你屏蔽了太多细节,导致答辩时被问“请求怎么到 Controller 的”只能回答“框架自动做的”。
原生 Servlet 的代价是代码量大,但换来的是每一行都能解释。比如HttpServletRequest怎么取参数、HttpServletResponse怎么设编码、HttpSession什么时候创建、Filter的doFilter里chain.doFilter前后分别能做什么——这些在框架里是黑匣子,在这里是白盒。
技术栈建议锁定在:Servlet 3.1+、JSP 或 Thymeleaf(视学校要求)、MySQL 5.7/8.0、JDBC 或轻量连接池、Tomcat 8.5/9.0。不需要引入 Redis、消息队列这些超出课设范围的东西,否则部署和答辩都会变成负担。
2.3 MVC 分层在项目里的具体落法
MVC 不是三个文件夹,而是三条规则。第一,Controller 只做参数校验和流程编排,不写 SQL;第二,Service 只做业务逻辑和事务边界,不碰HttpServletRequest;第三,DAO 只做数据访问,返回实体或列表,不返回ResultSet。
我一般会这样建包结构:
com.kuaidi.web ├── controller // LoginServlet, PickupServlet, AdminServlet ├── service // UserService, PackageService ├── dao // UserDao, PackageDao ├── entity // User, Package, Station ├── filter // EncodingFilter, AuthFilter └── util // DBUtil, CodeUtil这个结构的好处是:答辩时随便指一个类,都能说清楚它属于哪一层、依赖谁、被谁依赖。坏处是前期写起来比“一个 Servlet 干到底”慢,但后期改需求时省下的时间远超前期投入。
3. 总体架构设计:请求链路与四个核心模块的职责划分
3.1 从浏览器到数据库的完整请求链路
一个典型的取件请求会经过这些节点:浏览器发起GET /pickup?code=123456→ EncodingFilter 设置字符编码 → AuthFilter 检查 Session 中是否有登录用户 → PickupServlet 接收请求并调用 PackageService → PackageService 校验取件码格式和状态 → PackageDao 执行SELECT ... FOR UPDATE并更新状态 → 返回结果给 Servlet → Servlet 转发到 JSP 渲染 → 响应回到浏览器。
这条链路里,Filter 负责横切关注点(编码、认证),Servlet 负责路由和编排,Service 负责业务规则,DAO 负责数据一致性。每一层只做一件事,出问题时定位范围就小。
3.2 四个核心模块的职责边界
认证模块:由 LoginServlet + AuthFilter + Session 组成。登录成功后把user对象放入 Session,AuthFilter 拦截所有非登录请求,检查 Session 中是否存在该对象。关键点是 Filter 的urlPattern要排除登录页和静态资源,否则会死循环。
入库模块:由 PackageServlet + PackageService + PackageDao 组成。入库时需要生成唯一取件码,通常用“日期 + 随机数 + 校验位”的方式,保证同一天内不重复。状态初始为“待取件”。
取件模块:核心是并发控制。两个人同时用同一个取件码取件时,必须保证只有一个成功。做法是在 Service 层加事务,DAO 层用UPDATE package SET status='已取件' WHERE code=? AND status='待取件',根据返回的受影响行数判断是否成功。
查询模块:按手机号查历史时要注意分页和脱敏。手机号在页面上通常显示为138****1234,但数据库里存完整号码。查询时用LIKE匹配后四位即可,不需要全表扫描。
3.3 数据库表结构设计
三张核心表的字段设计如下:
CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(100) NOT NULL, phone VARCHAR(20), role ENUM('admin','courier','customer') DEFAULT 'customer', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE package ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) UNIQUE NOT NULL, phone VARCHAR(20) NOT NULL, station_id INT, status ENUM('待取件','已取件','已退回') DEFAULT '待取件', in_time DATETIME DEFAULT CURRENT_TIMESTAMP, out_time DATETIME, FOREIGN KEY (station_id) REFERENCES station(id) ); CREATE TABLE station ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, address VARCHAR(200), manager_phone VARCHAR(20) );package表的code字段加唯一索引,既保证取件码不重复,也加速按码查询。status用枚举而不是整数,是为了在数据库层面就限制非法状态,避免代码里出现status=5这种需要查注释才知道含义的情况。
4. 关键模块实现:Filter、Session 与 MVC 的代码落地
4.1 编码 Filter 与认证 Filter 的写法
编码 Filter 要放在 Filter 链的最前面,否则后续 Filter 和 Servlet 取到的参数可能是乱码。
@WebFilter("/*") public class EncodingFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; // 统一请求和响应编码,防止中文乱码 request.setCharacterEncoding("UTF-8"); response.setContentType("text/html;charset=UTF-8"); chain.doFilter(request, response); // 放行到下一个 Filter 或 Servlet } }认证 Filter 要排除登录接口和静态资源,否则用户永远登不进来。
@WebFilter("/admin/*") public class AuthFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); // 不自动创建 Session if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; // 不调用 chain.doFilter,请求到此为止 } chain.doFilter(request, response); } }getSession(false)和getSession()的区别是血泪经验:前者在 Session 不存在时返回 null,后者会直接创建一个新 Session。认证场景下必须用false,否则每个未登录请求都会创建一个空 Session,内存会被慢慢吃掉。
4.2 Session 管理登录状态与注销
登录成功后的 Session 写入:
User user = userService.login(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("user", user); session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作自动失效 response.sendRedirect(request.getContextPath() + "/admin/index.jsp"); } else { request.setAttribute("msg", "用户名或密码错误"); request.getRequestDispatcher("/login.jsp").forward(request, response); }注销时不仅要invalidate(),还要确保浏览器回退不能看到之前的页面。做法是在需要保护的 JSP 顶部加一段脚本,或者依赖 AuthFilter 拦截。invalidate()会清空 Session 中所有属性,但不会删除 JSESSIONID 这个 Cookie,所以下次请求会创建新 Session。
4.3 取件业务的 Service 与 DAO 协作
取件是并发敏感操作,Service 层必须加事务。
public class PackageService { private PackageDao packageDao = new PackageDao(); public boolean pickup(String code, String phone) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 Package pkg = packageDao.findByCode(conn, code); if (pkg == null || !pkg.getPhone().equals(phone)) { conn.rollback(); return false; // 取件码不存在或手机号不匹配 } int rows = packageDao.updateStatus(conn, code, "已取件"); if (rows == 1) { conn.commit(); return true; } conn.rollback(); return false; // 状态已被其他请求修改 } catch (SQLException e) { if (conn != null) try { conn.rollback(); } catch (SQLException ignored) {} throw new RuntimeException("取件失败", e); } finally { DBUtil.close(conn); } } }DAO 层的更新语句必须带状态条件:
public int updateStatus(Connection conn, String code, String status) throws SQLException { String sql = "UPDATE package SET status=?, out_time=NOW() WHERE code=? AND status='待取件'"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, status); ps.setString(2, code); return ps.executeUpdate(); // 返回受影响行数,0 表示状态已变 } }WHERE status='待取件'是并发控制的关键。两个请求同时到达时,数据库的行锁保证只有一个能更新成功,另一个返回 0 行,Service 据此回滚并提示“该快件已被取走”。
4.4 查询模块的分页与脱敏
按手机号查询历史时,通常需要分页。DAO 层用LIMIT和OFFSET:
public List<Package> findByPhone(Connection conn, String phone, int page, int size) throws SQLException { String sql = "SELECT * FROM package WHERE phone=? ORDER BY in_time DESC LIMIT ? OFFSET ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, phone); ps.setInt(2, size); ps.setInt(3, (page - 1) * size); // 结果集映射省略 } }页面上手机号脱敏用 JSTL 或 EL 表达式处理,不要在 DAO 里改数据。脱敏是展示层的事,数据库里必须存完整号码,否则后续按手机号查询会失效。
5. 避坑与常见问题排查
5.1 中文乱码:现象、原因与解决
现象是表单提交后数据库里出现???或头。原因通常是三处编码不一致:JSP 页面编码、请求体编码、数据库连接编码。解决顺序是:JSP 顶部<%@ page contentType="text/html;charset=UTF-8" %>,EncodingFilter 里request.setCharacterEncoding("UTF-8"),JDBC URL 加useUnicode=true&characterEncoding=UTF-8。三处都对齐后,乱码基本消失。
5.2 Session 失效后操作报空指针
现象是用户登录后放置一段时间,再点任何按钮都报NullPointerException。原因是 Session 超时后getAttribute("user")返回 null,而代码没有判空。解决是在 AuthFilter 里统一拦截,或者在每个需要用户信息的 Servlet 开头加if (session.getAttribute("user") == null)判断并跳转登录页。我一般选前者,因为只写一次。
5.3 Filter 顺序导致认证失效
现象是 AuthFilter 明明配了,但未登录用户还是能访问管理页面。原因是 EncodingFilter 和 AuthFilter 的执行顺序不确定。@WebFilter注解方式下,Filter 顺序由类名或配置顺序决定,不可靠。解决是在web.xml里显式配置<filter-mapping>的顺序,把 EncodingFilter 放最前,AuthFilter 放其后。
5.4 取件码重复导致入库失败
现象是入库时偶尔报唯一索引冲突。原因是取件码生成算法用了纯随机数,没有查重。解决是生成后先查一次数据库,或者用“时间戳后六位 + 随机两位”的方式降低碰撞概率,并在 DAO 层捕获DuplicateKeyException后重试一次。
5.5 事务未提交导致数据“消失”
现象是取件操作返回成功,但刷新页面后状态没变。原因是 Service 里开了事务但忘记commit,或者autoCommit被设成 false 后没有恢复。解决是确保每个setAutoCommit(false)都有对应的commit或rollback,并在finally里关闭连接。用连接池时尤其要注意,连接归还前必须恢复autoCommit=true。
6. 验收与答辩:怎么证明这套方案真的跑通了
验收不是“能登录就行”,而是要证明每个模块的边界和异常路径都处理了。我一般会准备一份验收清单,按模块逐项过。
| 验收项 | 操作步骤 | 预期结果 |
|---|---|---|
| 编码一致性 | 提交含中文的入库表单 | 数据库和页面均显示正常中文 |
| 认证拦截 | 未登录直接访问/admin/index.jsp | 跳转到登录页 |
| Session 超时 | 登录后等待 30 分钟再操作 | 跳转登录页,不报 500 |
| 并发取件 | 两个浏览器同时用同一取件码取件 | 一个成功,一个提示“已被取走” |
| 分页查询 | 按手机号查询超过 10 条记录 | 分页正常,手机号脱敏显示 |
| 事务回滚 | 取件时故意让 DAO 抛异常 | 状态不变,无脏数据 |
答辩时最容易被问的三个问题是:“Filter 和 Servlet 的区别是什么”“Session 和 Cookie 的关系”“为什么取件要用事务”。前两个考基础,第三个考设计。回答时不要背概念,直接拿项目里的代码举例:比如“AuthFilter 里用getSession(false)就是为了避免给未登录用户创建 Session”,这种回答比背定义有说服力。
还有一个技巧:提前准备一张请求链路图,从浏览器到 Filter 到 Servlet 到 Service 到 DAO 到数据库,每个节点标注对应的类和关键代码行。答辩时指着图讲,比翻代码快得多,也显得对整个流程有掌控。
从那以后我每次带课设,都要求先画这张链路图再动手写代码。图能画出来,说明想清楚了;画不出来,写出来的代码大概率要返工。希望帮到你。
本文还有配套的精品资源,点击获取