简介这份资源是面向JavaWeb初学者与进阶练习者的综合实战项目「快递e栈」以Servlet、Filter、Session与MVC分层为核心串联HTML、CSS、JavaScript、jQuery、Bootstrap、Layui、Ajax、MySQL、Tomcat、JSP、Listener等一整套Web开发知识点适合在完成基础语法学习后通过一个完整业务场景打通前后端协作与请求处理流程。压缩包共831个文件约17.23MB包含52个Java源文件与109个class文件承载控制层与DAO实现58个HTML、72个JS、38个CSS及大量png、gif、jpg图片构成前端页面与静态素材另有36个jar依赖、11个xml与6个properties配置完整呈现项目运行所需的目录结构。目前已有890人学习下载。读者可从中获得可直接导入运行的工程骨架、分层清晰的MVC代码组织方式、过滤器与监听器的实际用法以及数据库访问与页面交互的完整实现便于对照理解快递业务下的增删改查与登录会话管理并在此基础上进行二次开发与调试排错练习。1. 快递e栈一个把 Servlet、Filter、Session、MVC 全串起来的综合项目快递代收点这个场景需求天然就带着一整套 Web 基本功快递员要把包裹入库站点管理员要按取件码核销出库用户要能查到自己那件到没到后台还得有人管账号和权限。这些动作背后对应的是登录态保持、请求拦截、分层解耦、数据持久化正好把 Servlet、Filter、Session、MVC 四个关键词全部压进一个闭环里。快递e栈这套综合项目就是拿这个业务当壳逼着你把「请求从浏览器进来经过过滤器落到 Servlet再调 Service、DAO最后回到 JSP 或 JSON」这条链路完整走一遍。它适合正在做 JavaWeb 课程设计、毕设开题或者想用一个能讲清楚的项目把 MVC 落地的人。下面按需求、设计、实现、避坑、验收的顺序拆开讲能照着搭也能拿去改。2. 需求与总体设计四类角色、三层结构和一张核心表2.1 先把角色和用例钉死快递e栈的业务边界要先收窄否则做着做着就变成「什么都能干」的四不像。我一般会把它切成四类角色快递员、站点管理员、取件用户、系统管理员。快递员只负责入库录入快递单号、收件人手机号、快递公司、货架号站点管理员负责核销出库和异常件登记取件用户凭手机号加取件码查询自己的包裹状态系统管理员管账号、角色和站点信息。每个角色对应一组 Servlet 入口权限用 Session 里存的角色标识在 Filter 里统一拦不在每个 Servlet 里重复写 if。用例收敛之后功能清单大致是登录登出、快递入库、取件码生成、按码查询、核销出库、包裹列表分页、异常件标记、账号管理。这份清单就是后面分层和建表的依据多一个功能就多一条链路课程设计阶段不建议铺太开。2.2 三层结构怎么分职责别串味MVC 在 JavaWeb 里落地最常见的分法是 View 用 JSP 或前端页面Controller 用 ServletModel 拆成 Service 和 DAO。关键在于职责不能串Servlet 只做参数接收、调用 Service、决定跳转或返回 JSON绝不写 SQLService 管业务规则比如取件码校验、状态流转、事务边界DAO 只管单表增删改查。很多同学翻车就翻在 Servlet 里直接连数据库后期加一个「出库要同时改包裹状态和写操作日志」的需求代码就烂成一团。下面这张表是我一般会先画出来的模块职责划分写方案时直接放进去评审时一眼能看懂边界。层次典型类职责不该做的事ViewJSP / 静态页展示数据、提交表单写业务判断、连库ControllerXxxServlet收参、校验、调度、跳转拼 SQL、算业务规则ServiceXxxService业务规则、事务、组合 DAO直接操作 request/sessionDAOXxxDao单表 CRUD、结果集映射决定页面跳转公共Filter / Util登录拦截、编码、连接管理承载具体业务2.3 核心数据表与状态流转包裹表是整个系统的中心字段设计要能支撑「入库—待取—已取—异常」这条状态线。我通常建这几张表用户表存账号、密码摘要、角色包裹表存单号、手机号、取件码、货架号、状态、入库时间、出库时间操作日志表记录谁在什么时候对哪个包裹做了什么。取件码用 6 位数字入库时生成并保证在「待取」集合内唯一这个唯一性校验放在 Service 里做不要指望数据库唯一索引兜底因为已取件的码可以复用。状态字段用整型枚举而不是字符串流转只允许「待取→已取」和「待取→异常」两条边Service 里用一个方法集中判断别让每个 Servlet 自己改状态。这样后面加「超时未取自动转异常」的定时任务时只需要调同一个方法。3. 登录态与拦截实现Session 存什么、Filter 怎么拦3.1 登录流程与 Session 的存法登录是整套权限体系的入口。用户提交账号密码Servlet 调 Service 校验通过后把用户标识和角色写进 Session再重定向到各自首页。这里有个血泪经验Session 里只存 userId 和 role 两个必要字段别把整个用户对象塞进去否则改个昵称都要重新登录而且序列化时容易带出密码字段。密码存储用加盐摘要校验时对输入做同样处理再比对明文存密码是答辩时最容易被追问的点。// LoginServlet 核心逻辑校验通过后写入最小化 Session protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); UserService userService new UserServiceImpl(); User user userService.login(username, password); // 内部做加盐摘要比对 if (user null) { req.setAttribute(msg, 账号或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); return; } HttpSession session req.getSession(); session.setAttribute(userId, user.getId()); // 只存标识 session.setAttribute(role, user.getRole()); // 角色用于拦截判断 session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作失效 resp.sendRedirect(req.getContextPath() /index); }这段代码里getSession()默认会创建新会话登录成功才需要setMaxInactiveInterval按秒算30 分钟是课程设计里比较稳妥的值太短演示时老掉线太长又体现不出会话管理。重定向用sendRedirect而不是转发是为了避免刷新页面重复提交登录表单这个细节答辩时能讲出来是加分项。3.2 Filter 做统一登录拦截拦截逻辑集中在一个实现Filter的类里用WebFilter(/*)或 web.xml 配置拦截所有路径放行登录页、登录接口和静态资源。判断依据就是 Session 里有没有 userId。这里要注意 Filter 的执行顺序和放行写法chain.doFilter之前是请求预处理之后是响应后处理登录校验放在之前。WebFilter(/*) public class AuthFilter implements Filter { // 白名单登录相关和静态资源不拦 private static final String[] WHITE_LIST {/login.jsp, /login, /css/, /js/}; public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String path req.getRequestURI().substring(req.getContextPath().length()); for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(request, response); // 白名单直接放行 return; } } HttpSession session req.getSession(false); // 不新建会话 if (session null || session.getAttribute(userId) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); // 已登录继续后续链路 } }getSession(false)是关键拦截器里绝不能顺手创建会话否则每个未登录请求都会生成一个空 Session内存白白涨。白名单用前缀匹配能同时覆盖目录和具体页面比逐个精确匹配好维护。角色级权限如果要做可以在这个 Filter 里再读一次 role按路径前缀判断但课程设计阶段建议先只做登录拦截把角色判断放到具体 Servlet 或第二个 Filter 里避免一个类越写越臃肿。3.3 编码与异常的统一处理除了登录拦截通常还会配一个编码 Filter 解决 POST 中文乱码req.setCharacterEncoding(UTF-8)必须在读取任何参数之前调用。GET 请求的乱码在 Tomcat 新版本里默认已是 UTF-8但为了可移植性可以在连接串或服务器配置里统一声明。异常处理则建议在 Filter 里包一层 try-catch捕获后转发到统一错误页别让堆栈直接吐到浏览器上这既是体验问题也是安全问题。4. MVC 分层落地Servlet 调度、Service 事务与 DAO 封装4.1 Servlet 只做调度一个入口管一类动作Servlet 层最容易写胖。我的做法是一个业务模块一个 Servlet用action参数区分具体动作内部用 switch 分发到私有方法每个方法只做「取参—调 Service—设属性或写 JSON—跳转」。这样既避免了为每个动作建一个 Servlet 类导致类爆炸又保持了入口清晰。下面以包裹模块为例。WebServlet(/package) public class PackageServlet extends HttpServlet { private final PackageService packageService new PackageServiceImpl(); protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String action req.getParameter(action); switch (action null ? list : action) { case in: doInbound(req, resp); break; // 入库 case out: doOutbound(req, resp); break; // 核销出库 case query: doQuery(req, resp); break; // 按取件码查询 default: doList(req, resp); // 分页列表 } } private void doInbound(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String trackingNo req.getParameter(trackingNo); String phone req.getParameter(phone); String shelf req.getParameter(shelf); // 参数校验交给 ServiceServlet 只负责把结果转成视图 Result result packageService.inbound(trackingNo, phone, shelf); req.setAttribute(result, result); req.getRequestDispatcher(/WEB-INF/jsp/inbound_result.jsp).show req.getRequestDispatcher(/WEB-INF/jsp/inbound_result.jsp).forward(req, resp); } // 其余方法结构类似省略 }service方法重写而不是分别写 doGet/doPost是为了让 GET 和 POST 走同一套分发减少重复。JSP 放在WEB-INF下用户无法直接访问只能通过 Servlet 转发进入这是 MVC 里保护视图层的常规做法。Result是一个简单的返回对象带成功标志、消息和数据Service 返回它而不是抛异常给 Servlet能让 Controller 保持干净。4.2 Service 管业务规则和事务边界Service 是业务规则的唯一落点。入库时要生成取件码并保证待取集合内唯一出库时要校验取件码、判断状态、更新包裹并写日志这些都在一个方法里完成。涉及多表写入时必须控制事务JDBC 下手动管理连接和提交回滚别让每次 DAO 调用各拿各的连接否则回滚不了。public class PackageServiceImpl implements PackageService { public Result inbound(String trackingNo, String phone, String shelf) { if (isBlank(trackingNo) || isBlank(phone)) { return Result.fail(单号和手机号不能为空); } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 PackageDao dao new PackageDaoImpl(conn); String code generateUniqueCode(dao); // 待取集合内唯一 dao.insert(new Package(trackingNo, phone, code, shelf, STATUS_WAITING)); conn.commit(); return Result.ok(入库成功取件码 code); } catch (Exception e) { DBUtil.rollback(conn); // 统一回滚 return Result.fail(入库失败请重试); } finally { DBUtil.close(conn); } } // 出库、查询等方法同理事务边界都在 Service }setAutoCommit(false)到commit之间就是一个事务单元DAO 接收外部传入的Connection而不是自己新建这样多个 DAO 操作才能共享同一事务。取件码生成放在事务内配合「待取集合内唯一」的查询校验冲突时重试几次即可。异常统一 catch 后回滚并返回失败结果不把底层异常抛给上层避免泄露 SQL 细节。4.3 DAO 封装与结果集映射DAO 层用接口加实现的方式接口定义方法签名实现类里写 JDBC。每个方法只操作一张表PreparedStatement占位符传参防注入结果集逐行映射成实体对象。分页查询用LIMIT ?, ?页码和每页条数由 Service 算好偏移量传进来。连接从工具类拿用完在 Service 层统一关闭DAO 不负责关连接否则事务共享的连接会被提前关掉。这套分层写下来一个请求的完整路径是浏览器 → AuthFilter 校验登录 → PackageServlet 分发 → PackageService 校验规则并开事务 → PackageDao 执行 SQL → 返回 Result → Servlet 转发 JSP 渲染。链路清晰任何一层出问题都能定位答辩时顺着这条线讲评委基本能确认你是真做过。5. 避坑与排查五个最容易翻车的点5.1 中文乱码反复出现现象是表单提交后数据库里存的是问号或者页面显示乱码。原因通常是编码设置时机不对setCharacterEncoding在参数已被读取后才调用就无效或者 JSP 页面没声明pageEncoding。解决办法是编码 Filter 放在拦截链最前面确保先于任何参数读取执行JSP 顶部统一写% page contentTypetext/html;charsetUTF-8 %数据库连接串带上字符集参数三处对齐后乱码基本绝迹。5.2 刷新页面重复提交现象是入库成功后按 F5同一条快递被录了两遍。原因是表单提交后用了转发浏览器地址栏还停在提交接口上。解决办法是提交类操作一律「处理完重定向到结果页」也就是 PRG 模式重定向后浏览器地址变了刷新只会重新拉结果页。如果业务上必须防重可以在 Session 里放一个一次性令牌提交时校验并清除。5.3 连接池耗尽或连接泄漏现象是演示到一半页面卡死日志里报连接获取超时。原因多半是 DAO 里拿了连接没关或者异常路径上跳过了关闭逻辑。解决办法是把连接的获取和关闭收敛到 Service 的 try-finally 里DAO 只使用不关闭用连接池时在池配置里开泄漏检测把超时阈值调短开发阶段就能暴露问题。血泪经验是别在 JSP 里直接连库那种写法几乎必然泄漏。5.4 Filter 拦截了登录页导致死循环现象是访问任何页面都跳登录页登录页本身也跳浏览器报重定向次数过多。原因是白名单没覆盖登录相关路径或者路径匹配时把上下文路径算进去了。解决办法是拦截前先去掉getContextPath()再比对白名单同时包含登录页和登录接口调试时在 Filter 里打一行日志输出当前 path一眼就能看出匹配错在哪。5.5 事务没生效导致数据不一致现象是出库时包裹状态改了但日志没写进去或者反过来。原因是 DAO 各自从连接池拿连接不在同一个事务里。解决办法是 Service 开事务后把同一个Connection传给所有 DAO提交和回滚都在 Service 做检查setAutoCommit(false)是否在第一次 SQL 之前调用以及异常分支是否真的走到了 rollback。这个坑排查起来像黑匣子最有效的办法是在提交和回滚处各打一条日志看实际走了哪条。6. 验收与答辩用一条主链路和一张检查表说话验收阶段最忌讳的是功能罗列讲了一堆页面却说不清数据怎么流动。我的建议是准备一条主链路现场走通快递员登录入库拿到取件码用户凭码查询看到待取管理员核销后状态变已取全程配合数据库里状态字段的前后对比。这条链路把 Session、Filter、Servlet 分发、Service 事务、DAO 持久化全串起来了评委顺着问下去你都有话答。答辩前用下面这张检查表过一遍每项都能当场演示或说清实现位置基本不会被打个措手不及。检查项验收标准对应实现位置登录态未登录访问业务页跳登录登录后可访问AuthFilter Session权限不同角色看到的入口不同Session 中 role 页面判断入库生成唯一取件码并落库PackageService.inbound出库状态流转正确且写日志Service 事务 日志 DAO查询按手机号加取件码能查到PackageDao 条件查询分页列表翻页数据正确LIMIT 偏移量计算异常错误输入有提示不白屏统一错误页 Result现场演示时把浏览器开发者工具的网络面板打开点一次入库让评委看到请求经过 Filter、返回重定向、再拉结果页的完整过程比嘴上讲十遍分层都有说服力。被问到「为什么用 Filter 不用拦截器」时答清楚 Filter 是 Servlet 规范层面、在 Servlet 之前执行、能拦静态资源就够课程设计的深度了。我自己的习惯是答辩前把主链路在干净环境里从零跑三遍第一遍正常流程第二遍故意输错密码和空单号看异常处理第三遍清空 Session 直接访问业务页看拦截是否生效。这三遍跑完心里就有底了。从那以后我每次交项目都强制走一遍这条主链路加检查表翻车次数明显少了。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?