资讯动态

快递e栈项目实战:Servlet+Filter+Session 搭建 MVC 骨架

发布时间:2026/10/21 11:52:21 来源:尧图企业网站定制
简介这是一套面向JavaWeb初学者与进阶练习者的综合实战项目源码以「快递e栈」业务场景串联Servlet、Filter、Session与MVC分层等核心知识点适合在掌握基础语法后通过完整项目巩固Web开发流程。压缩包共831个文件约17.23MB涵盖58个html页面、38个css样式、72个js脚本及Jquery、bootstrap、layui、layer等前端资源后端包含52个java源文件、109个class、36个jar依赖另有xml、properties配置与图片素材完整呈现从前端交互到数据库访问的工程结构。项目涉及ajax异步请求、mysql数据存储与tomcat部署并包含公有云服务器相关实践可帮助读者理解过滤器、监听器与JSP在真实业务中的协作方式。目前已有886人学习下载适合作为课程设计、毕业设计或自学练手参考便于对照目录结构梳理分层思路、排查常见配置问题并积累项目经验。1. 快递e栈项目拆解ServletFilterSession 的 MVC 骨架到底怎么搭很多同学学完 Servlet 和 JSP 之后单个知识点都能看懂但一让独立写个完整 Web 项目就卡壳——不知道该分几层、请求怎么流转、登录状态往哪放。这个「快递e栈」综合项目就是冲着这个痛点来的它用最朴素的 Servlet Filter Session 技术栈把 MVC 三层结构完整跑通一遍没有 Spring 帮你兜底所有转发、拦截、状态管理都得自己写。适合刚学完 JavaWeb 基础、想找一个能从头到尾复现的练手项目的人也适合工作后想回头补一补底层请求链路的老手。它解决的不是业务复杂度而是「一个请求从浏览器发出到数据库返回中间到底经过了哪些组件」这个黑匣子问题。2. 环境搭建与 MVC 分层从零把项目跑起来2.1 技术选型与依赖清单这个项目没有用 Maven 做依赖管理属于比较传统的 JavaWeb 工程结构常见做法是手动把 jar 包丢进WEB-INF/lib目录。核心依赖就三块Servlet API容器提供编译期需要、MySQL 驱动、以及可选的 JSTL 标签库。如果你用 IDEA 新建项目记得选 Java Enterprise 而不是普通 Java 项目否则 webapp 目录结构不会自动生成。组件版本建议作用JDK8 或 11编译与运行环境Tomcat8.5 / 9.0Servlet 容器MySQL5.7 / 8.0数据存储servlet-api容器自带编译期依赖mysql-connector-java5.1.x 或 8.0.x数据库驱动jstl1.2JSP 页面标签选 Tomcat 8.5 还是 9.0 有个坑9.0 之后javax.servlet包名没变但如果你后面想迁移到 Jakarta EE包名会变成jakarta.servlet所有 import 都得改。练手阶段建议锁死 8.5别折腾。2.2 标准目录结构与分层职责MVC 分层的核心是职责边界清晰。这个项目里controller包只负责接收参数、调用 service、决定转发到哪个页面service包写业务逻辑和事务控制dao包只跟数据库打交道。实体类放entity工具类放util。目录结构大概是这样src/ com.express.controller/ // Servlet 控制器 com.express.service/ // 业务逻辑 com.express.dao/ // 数据访问 com.express.entity/ // 实体类 com.express.util/ // DBUtil 等工具 webapp/ WEB-INF/ web.xml lib/ css/ js/ images/ index.jsp我一般会在util包里放一个DBUtil用静态代码块加载驱动、用ThreadLocal管理 Connection这样 service 层拿连接不用每次 new。下面是一个最简版本public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/express?useSSLfalseserverTimezoneUTC; private static final String USER root; private static final String PWD 123456; private static ThreadLocalConnection tl new ThreadLocal(); static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(驱动加载失败, e); } } // 获取连接同一个线程复用同一个 Connection public static Connection getConn() throws SQLException { Connection conn tl.get(); if (conn null || conn.isClosed()) { conn DriverManager.getConnection(URL, USER, PWD); tl.set(conn); } return conn; } // 事务提交后必须关闭并移除否则线程池复用时会拿到脏连接 public static void closeConn() throws SQLException { Connection conn tl.get(); if (conn ! null !conn.isClosed()) { conn.close(); tl.remove(); } } }逻辑说明ThreadLocal保证同一线程内多次getConn()拿到的是同一个连接这样 service 层做事务时不用把 Connection 当参数传来传去。参数说明URL里的serverTimezoneUTC在 MySQL 8.0 驱动下必须加否则会报时区错误useSSLfalse是本地开发关掉 SSL 警告。closeConn()里的tl.remove()是血泪经验——Tomcat 线程池会复用线程不 remove 的话下一个请求可能拿到已经关闭的连接。2.3 数据库建表与初始化数据建表脚本建议单独存一个init.sql方便反复重建。核心表就三张用户表、快递单表、取件记录表。字段别贪多够跑通流程就行。CREATE DATABASE express DEFAULT CHARSET utf8mb4; USE express; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, phone VARCHAR(20), role TINYINT DEFAULT 0 -- 0普通用户 1管理员 ); CREATE TABLE express ( id INT PRIMARY KEY AUTO_INCREMENT, number VARCHAR(32) UNIQUE NOT NULL, -- 快递单号 company VARCHAR(32), status TINYINT DEFAULT 0, -- 0待取 1已取 code VARCHAR(10), -- 取件码 create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); INSERT INTO user(username, password, role) VALUES(admin, 123456, 1);参数说明number加唯一索引防止重复录入code是取件码实际业务里一般 6 位随机数这里为了演示用短码。status用 TINYINT 而不是布尔方便以后扩展「已退回」等状态。初始化一个管理员账号方便你第一次登录后台。3. Servlet 请求流转从 URL 到 JSP 的完整链路3.1 用注解还是 web.xml 配置映射Servlet 3.0 之后可以用WebServlet注解比 web.xml 清爽很多。但有个坑注解的urlPatterns如果写成/*会把静态资源也拦下来导致 css、js 加载 404。正确做法是给每个 Servlet 配具体路径比如/express/*或者/user/login。WebServlet(/express/*) public class ExpressServlet extends HttpServlet { private ExpressService service new ExpressServiceImpl(); Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getPathInfo(); // 拿到 /* 后面的部分 if (/list.equals(action)) { ListExpress list service.findAll(); req.setAttribute(list, list); req.getRequestDispatcher(/WEB-INF/jsp/express_list.jsp).forward(req, resp); } else if (/add.equals(action)) { // 处理新增 } } }逻辑说明用getPathInfo()拿到通配符匹配到的剩余路径一个 Servlet 处理一组相关操作减少类数量。参数说明req.getRequestDispatcher(...).forward()是服务器内部转发浏览器地址栏不变如果换成resp.sendRedirect()就是重定向地址栏会变且 request 域数据丢失。新手最容易在这两个之间翻车。3.2 请求参数封装与中文乱码POST 请求中文乱码是老生常谈但每次新建项目还是会忘。Tomcat 8 之后 GET 请求默认用 UTF-8但 POST 请求体还是 ISO-8859-1。最省事的做法是在 Filter 里统一设req.setCharacterEncoding(UTF-8)而不是每个 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;charsetUTF-8); chain.doFilter(request, response); // 放行交给下一个过滤器或 Servlet } }逻辑说明chain.doFilter()是过滤器链的核心不调用它请求就被截断了。参数说明setCharacterEncoding必须在读取任何参数之前调用否则无效——这是很多人写了 Filter 还是乱码的原因因为他们在 Filter 之前就getParameter了。3.3 转发与重定向的选型边界什么时候用转发、什么时候用重定向判断标准很简单需要保留 request 域数据就用转发需要防止表单重复提交就用重定向。查询列表用转发登录成功后跳首页用重定向。下面这个对比表可以贴在显示器边上维度forward 转发redirect 重定向地址栏不变改变请求次数1 次2 次request 域数据保留数据丢失目标范围只能站内可跨站典型场景查询结果展示登录成功跳转我一般会在登录成功后用resp.sendRedirect(req.getContextPath() /index.jsp)加getContextPath()是为了防止项目部署路径不是根路径时 404。4. Filter 与 Session登录拦截和状态保持的实战写法4.1 登录拦截器的实现与放行白名单登录拦截是 Filter 最典型的用法。核心逻辑判断 session 里有没有用户对象没有就踢回登录页。但要注意放行登录页本身和静态资源否则会死循环。WebFilter(/*) public class LoginFilter implements Filter { // 不需要登录就能访问的路径 private static final String[] WHITE_LIST {/login.jsp, /user/login, /css/, /js/}; Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String path uri.substring(request.getContextPath().length()); for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(req, resp); return; } } Object user request.getSession().getAttribute(user); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); } else { chain.doFilter(req, resp); } } }逻辑说明先算出去掉项目名的相对路径再跟白名单前缀比对。参数说明request.getContextPath()返回项目部署路径比如/expresssubstring之后得到/css/style.css这样的相对路径。白名单用startsWith而不是equals是为了把整个 css 目录都放行。4.2 Session 生命周期与超时配置Session 默认 30 分钟超时可以在web.xml里改。但要注意Session 超时后用户再操作会被 LoginFilter 拦截体验上就是「突然退出登录」。如果项目对体验要求高可以在前端加个定时心跳或者把超时时间调长。session-config session-timeout60/session-timeout !-- 单位分钟 -- /session-config参数说明60表示 60 分钟无操作后失效。设太长会占服务器内存设太短用户频繁掉线。我一般练手项目设 60生产环境看业务定。另外session.invalidate()是注销时手动销毁别忘了在退出登录的 Servlet 里调用。4.3 取件码生成与 Session 存储的配合快递e栈的核心业务是「录入快递生成取件码」和「凭取件码取件」。取件码可以存在数据库里也可以临时存 Session。如果存 Session用户关掉浏览器就丢了所以还是落库更稳妥。生成逻辑用Random或SecureRandompublic String generateCode() { SecureRandom random new SecureRandom(); int code 100000 random.nextInt(900000); // 6位 return String.valueOf(code); }逻辑说明100000 nextInt(900000)保证结果落在 100000~999999 之间永远是 6 位。参数说明用SecureRandom而不是Random是为了避免取件码被预测——虽然练手项目无所谓但养成习惯没坏处。生成后要查库确认没重复重复就重新生成。5. 避坑排查那些让项目跑不起来的常见问题5.1 现象访问任何页面都跳登录页包括 login.jsp原因LoginFilter 的urlPatterns配了/*但白名单判断逻辑写错比如用了equals而不是startsWith或者路径没去掉 contextPath 导致比对失败。 解决在 Filter 里打印path变量确认它长什么样。白名单前缀一定要跟打印出来的格式一致。另外检查 Filter 的WebFilter有没有配dispatcherTypes默认只拦 REQUEST如果用了 forward 跳转可能绕过。5.2 现象数据库连接池报「Too many connections」原因DBUtil里每次getConn()都 new 一个 Connection用完没关或者关了但没从 ThreadLocal 移除导致连接泄漏。 解决确保每个 dao 方法执行完调用closeConn()并且closeConn()里必须tl.remove()。更稳妥的做法是用 try-with-resources 或者引入 Druid 连接池但练手阶段先把手动管理写对。5.3 现象JSP 页面 EL 表达式不解析直接显示${user.name}原因JSTL 依赖没加或者 web.xml 的版本声明太低2.3 之前 EL 默认不启用。 解决确认WEB-INF/lib下有jstl.jar和standard.jarweb.xml 的web-app版本至少 2.4。如果用的是 Tomcat 10JSTL 要换成 Jakarta 版本包名从javax变jakarta。5.4 现象POST 提交中文参数乱码Filter 明明配了原因Filter 的执行顺序问题。如果 EncodingFilter 在 LoginFilter 之后执行而 LoginFilter 里调用了getParameter那编码设置就晚了。 解决在 web.xml 里用filter-mapping的顺序控制或者用注解时注意类名的字母顺序注解方式顺序不可控所以编码 Filter 建议用 web.xml 配在最前面。最稳的办法是 EncodingFilter 里不要读任何参数。5.5 现象修改 JSP 后刷新浏览器没变化原因Tomcat 缓存了编译后的 JSP或者 IDEA 没有自动热部署。 解决在 Tomcat 配置里勾选「On frame deactivation: Update classes and resources」或者手动点 Redeploy。生产环境不会频繁改 JSP但开发阶段这个设置能省很多重启时间。6. 进阶技巧用 BaseServlet 消灭重复的 if-else写到后面你会发现每个 Servlet 里都是一堆if (/xxx.equals(action))又臭又长。常见做法是抽一个BaseServlet用反射根据 action 名调用对应方法子类只写业务方法不写分发逻辑。public class BaseServlet extends HttpServlet { Override protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getPathInfo(); // 例如 /login if (action null || /.equals(action)) { action /index; } String methodName action.substring(1); // 去掉斜杠得到 login try { Method method this.getClass().getMethod(methodName, HttpServletRequest.class, HttpServletResponse.class); method.invoke(this, req, resp); } catch (NoSuchMethodException e) { resp.sendError(404, 方法不存在: methodName); } catch (Exception e) { throw new ServletException(e); } } }逻辑说明service()是 Servlet 的入口重写它就能在doGet/doPost之前做统一分发。getMethod按方法名和参数类型查找所以子类的方法签名必须固定为(HttpServletRequest, HttpServletResponse)。参数说明action.substring(1)去掉开头的/这样 URL 里的/user/login就映射到login()方法。子类继承 BaseServlet 后只需要写public void login(...)和public void register(...)不用再写 if-else。用反射的代价是性能略低每次请求多一次方法查找但练手项目完全够用。如果在意性能可以在类加载时把方法缓存到 Map 里。另外要注意反射调用时如果方法抛异常会被包成InvocationTargetException排查时要e.getCause()才能看到真实异常。从那以后我每次写 Servlet 项目都会先把 BaseServlet 和 EncodingFilter 搭好再写业务——这两个东西不提前做后面改起来就是灾难。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑