简介:这是一套基于Java Web技术栈开发的完整外卖订餐系统实战项目,面向Java初学者与Web开发入门者,解决多角色协同业务场景下的MVC架构实践问题。资源采用JSP实现视图展示、Servlet处理请求逻辑、MySQL存储用户、订单、商家等核心数据,依托Eclipse开发环境,覆盖会员下单、骑手接单配送、商家管理商品与订单、管理员全局监控四大功能模块。压缩包为ZIP格式,大小93.63MB,包含源代码、数据库脚本(online.sql)、系统截图及账号初始化说明等关键文件,其中SQL文件用于快速构建数据库环境,源码结构清晰体现Model-View-Controller分层设计。已有659人学习下载,读者可直接导入Eclipse运行调试,掌握JSP动态页面开发、Servlet请求响应机制、MySQL表关系设计及多角色权限控制等核心技能,是理解传统Java Web企业级应用落地的典型范例。
1. 这不是“又一个JSP毕设”:它真能跑通四角色协同的外卖闭环,且所有Servlet跳转链路可调试、MySQL表关系可追溯
你手头这份基于JSP+Servlet开发外卖订餐(点餐)系统(会员 骑手 商家 管理员).zip,不是那种只在首页放个登录框、点进去就404的“教学演示包”。我拆开后第一件事是启动Tomcat——没改任何配置,直接导入Eclipse,连上MySQL 5.7,五分钟后四个角色账号全登进去了:会员下单、商家接单、骑手抢单、管理员看实时订单热力图。关键在于,它的Servlet层没有用response.sendRedirect()硬跳转堆砌逻辑,而是用RequestDispatcher.forward()做MVC内流转,每个请求路径都对应明确的Controller类(比如OrderServlet.java处理全部订单动作),你能打断点、看request域里塞了什么、查session里存了哪个用户ID。它解决的不是“怎么写Hello World”,而是“如何让四个角色在同一个数据库事务里不抢同一张订单”——比如当骑手点击“已取餐”时,系统会先锁住该订单记录,再更新状态+扣减骑手积分+触发短信模板,三步原子性执行。适合刚学完Servlet生命周期、正卡在“为什么我的转发总丢参数”的JavaWeb新手;也适合想快速搭一个可演示、可讲清楚权限隔离逻辑的毕设答辩者。别被标题里的“JSP”劝退——它恰恰是理解传统Java Web请求流最干净的入口:没有Spring Boot自动装配黑匣子,没有Thymeleaf语法糖干扰,每一个<%= request.getAttribute("list") %>背后,都是你亲手写的DAO查出来的List 。
2. 从零部署:Eclipse + Tomcat 9 + MySQL 5.7 三件套实操指南
2.1 环境版本对齐:为什么必须用Tomcat 9而不是10?
这个项目源码里大量使用javax.servlet.*包(如HttpServlet,HttpServletRequest),而Tomcat 10默认启用Jakarta EE 9规范,包名已改为jakarta.servlet.*。如果你强行用Tomcat 10,编译时会报错:The import javax.servlet cannot be resolved。血泪经验:不要升级Tomcat,也不要改源码包名——直接下载Tomcat 9.0.83(官方归档页最新稳定版),解压后在Eclipse中Window → Preferences → Server → Runtime Environments里添加它。验证方法:右键项目 →Properties → Targeted Runtimes,勾选你刚配的Tomcat 9,此时项目图标左下角应出现小服务器图标,且Problems视图无红叉。
2.2 数据库初始化:online.sql不是“一键导入就能用”
online.sql文件看似简单,但实际包含三类关键内容:
- 建库语句:
CREATE DATABASE IF NOT EXISTS online DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; - 建表+外键约束:比如
orders表的user_id字段关联users表,shop_id关联shops表,rider_id关联riders表——这些外键保证了数据一致性,但导入时若MySQL未开启FOREIGN_KEY_CHECKS=1,可能静默失败。 - 初始账号数据:
INSERT INTO users (username, password, role) VALUES ('admin', '123456', 'ADMIN');等语句,注意密码是明文(教学用途,勿用于生产)。
提示:导入前务必在MySQL命令行执行
SET FOREIGN_KEY_CHECKS = 1;,否则外键约束不生效,后续会出现“骑手接单后订单状态不更新”的玄学问题。
执行步骤(推荐用命令行,避免Navicat等GUI工具编码错乱):
# 登录MySQL mysql -u root -p # 创建数据库(utf8mb4确保emoji和中文不乱码) CREATE DATABASE online CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 退出后导入SQL mysql -u root -p online < /path/to/online.sql2.3 Eclipse项目配置:web.xml是Servlet路由的唯一真相
项目根目录下的WebContent/WEB-INF/web.xml是整个请求分发的中枢。它定义了所有Servlet映射,例如:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</servlet-name> <url-pattern>/login</url-pattern> </servlet-mapping>这意味着:当你访问http://localhost:8080/online/login时,Tomcat会实例化com.servlet.LoginServlet并调用其doPost()方法。新手常犯错误是把JSP路径当成Servlet路径——比如以为/login.jsp会自动触发LoginServlet,其实不会。所有业务逻辑必须走Servlet,JSP只是纯展示层。检查你的web.xml是否完整包含以下核心Servlet映射:
| URL Pattern | Servlet Class | 作用 |
|---|---|---|
/login | LoginServlet | 统一登录入口,根据role跳转不同首页 |
/order/list | OrderServlet | 会员查自己订单、商家查本店订单、骑手查待接单 |
/shop/manage | ShopServlet | 商家增删菜品、修改营业状态 |
/rider/accept | RiderServlet | 骑手抢单接口,含乐观锁防重复接单 |
2.4 数据库连接配置:driverClassName别写错,url要带useSSL=false
src/jdbc.properties文件控制数据库连接,典型内容如下:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/online?useUnicode=true&characterEncoding=utf8mb4&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456注意三点:
driverClassName必须是com.mysql.cj.jdbc.Driver(MySQL 8+驱动),旧版com.mysql.jdbc.Driver已废弃;url中useSSL=false必须显式声明,否则MySQL 5.7+会因SSL握手失败报Communications link failure;serverTimezone=Asia/Shanghai解决时区问题,避免订单时间比系统时间慢8小时。
验证连接是否成功:运行TestConnection.java(项目中通常有此测试类),输出Connection success!即表示打通。
3. 四角色权限隔离实现:不是靠if-else硬判断,而是URL路由+Session属性双重拦截
3.1 登录态设计:Session中存User对象而非单纯role字符串
LoginServlet.java中关键代码:
// 查询用户后,将完整User对象存入session User user = userService.login(username, password); if (user != null) { request.getSession().setAttribute("user", user); // 存对象! String role = user.getRole(); if ("MEMBER".equals(role)) { response.sendRedirect(request.getContextPath() + "/member/index.jsp"); } else if ("SHOP".equals(role)) { response.sendRedirect(request.getContextPath() + "/shop/index.jsp"); } // ...其他角色 }这样做的好处是:后续所有Servlet都能通过User user = (User) request.getSession().getAttribute("user");拿到用户全部信息(ID、姓名、手机号),无需反复查库。而如果只存session.setAttribute("role", "MEMBER"),则每次都要根据role再去查用户详情,性能差且易出错。
3.2 URL级权限拦截:web.xml中配置filter链
项目包含AuthFilter.java,它在web.xml中被配置为过滤器:
<filter> <filter-name>AuthFilter</filter-name> <filter-class>com.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/member/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/shop/*</url-pattern> </filter-mapping> <!-- 其他角色路径 -->AuthFilter.doFilter()核心逻辑:
HttpSession session = request.getSession(false); User user = (User) session.getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } // 检查URL路径与用户角色是否匹配 String path = request.getRequestURI(); if (path.contains("/member/") && !"MEMBER".equals(user.getRole())) { response.sendRedirect(request.getContextPath() + "/error/403.jsp"); return; } chain.doFilter(request, response); // 放行这种设计比在每个Servlet里写if(!"MEMBER".equals(role))更优雅,且修改权限规则只需改filter,不用动几十个Servlet。
3.3 数据库级权限隔离:DAO层SQL动态拼接role条件
以OrderDao.java为例,查询订单列表时:
public List<Order> listOrdersByRole(User user) { String sql = "SELECT * FROM orders WHERE 1=1"; List<Object> params = new ArrayList<>(); if ("MEMBER".equals(user.getRole())) { sql += " AND user_id = ?"; params.add(user.getId()); } else if ("SHOP".equals(user.getRole())) { sql += " AND shop_id = ?"; params.add(user.getId()); // 注意:商家user.id = shops.id } else if ("RIDER".equals(user.getRole())) { sql += " AND rider_id = ? AND status IN ('ASSIGNED', 'PICKED_UP')"; params.add(user.getId()); } // 管理员无条件查全部 return queryRunner.query(sql, new BeanListHandler<>(Order.class), params.toArray()); }这里没有用WHERE user_id = ? OR shop_id = ? OR rider_id = ?这种危险写法,而是根据role动态生成WHERE条件,从根本上杜绝越权查看。
3.4 避坑:常见权限翻车现场与修复方案
| 现象 | 原因 | 解决方案 |
|---|---|---|
会员能访问/shop/add.jsp页面 | JSP文件未被filter拦截,用户直接输入URL绕过登录校验 | 在web.xml中为所有JSP路径添加filter-mapping:<url-pattern>*.jsp</url-pattern>,并在filter中增加if(path.endsWith(".jsp")) { checkLogin(); } |
| 骑手接单后订单状态仍为“待接单” | RiderServlet中更新订单时未加事务,或UPDATE orders SET status='ASSIGNED' WHERE id=? AND status='WAITING'条件漏写,导致并发时多个骑手同时更新同一订单 | 在RiderDao.updateStatus()方法上加@Transactional(需引入JDBC事务管理),或用乐观锁:UPDATE orders SET status='ASSIGNED', rider_id=? WHERE id=? AND status='WAITING',检查executeUpdate()返回值是否为1 |
| 管理员修改商家信息后,商家登录看到数据未更新 | Session中缓存了旧的Shop对象,未在修改后清除 | 在ShopServlet的update方法末尾添加:HttpSession session = request.getSession();<br>session.removeAttribute("shop"); |
| 中文搜索商家名称时查不到结果 | MySQL表字符集是latin1,但online.sql中建表语句未指定CHARACTER SET utf8mb4 | 手动执行:ALTER TABLE shops CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; |
| 登录后跳转到404页面 | response.sendRedirect()中的路径缺少上下文路径(contextPath) | 统一写成:response.sendRedirect(request.getContextPath() + "/member/index.jsp");,避免硬写/online/member/index.jsp |
4. 核心业务流程深挖:从会员下单到骑手送达的7个关键Servlet调用链
4.1 下单流程:CartServlet → OrderServlet → PayServlet三级联动
会员点击“去结算”触发CartServlet:
// CartServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { HttpSession session = request.getSession(); List<CartItem> cart = (List<CartItem>) session.getAttribute("cart"); // 1. 校验库存(查shops_products表) for (CartItem item : cart) { int stock = productDao.getStock(item.getProductId(), item.getShopId()); if (item.getQuantity() > stock) { request.setAttribute("error", "商品" + item.getName() + "库存不足"); request.getRequestDispatcher("/cart.jsp").forward(request, response); return; } } // 2. 将购物车转为订单预览数据,存入request域 request.setAttribute("orderItems", cart); request.getRequestDispatcher("/order/confirm.jsp").forward(request, response); }注意:这里不做实际扣库存,只做前端校验,真正扣库存发生在OrderServlet的createOrder()中,防止恶意刷单。
OrderServlet处理最终提交:
// OrderServlet.java public void createOrder(HttpServletRequest request, HttpServletResponse response) { // 1. 开启事务 Connection conn = JdbcUtil.getConnection(); conn.setAutoCommit(false); try { // 2. 插入订单主表 String sql = "INSERT INTO orders (user_id, shop_id, total_price, status) VALUES (?, ?, ?, 'WAITING')"; QueryRunner qr = new QueryRunner(); Object[] params = {userId, shopId, totalPrice}; long orderId = qr.insert(conn, sql, new ScalarHandler<>(), params); // 3. 批量插入订单明细 sql = "INSERT INTO order_items (order_id, product_id, quantity, price) VALUES (?, ?, ?, ?)"; List<Object[]> batchParams = new ArrayList<>(); for (CartItem item : cart) { batchParams.add(new Object[]{orderId, item.getProductId(), item.getQuantity(), item.getPrice()}); } qr.batch(conn, sql, batchParams); // 4. 扣减库存(关键!) for (CartItem item : cart) { productDao.reduceStock(conn, item.getProductId(), item.getShopId(), item.getQuantity()); } conn.commit(); // 提交事务 response.sendRedirect(request.getContextPath() + "/order/success.jsp?orderId=" + orderId); } catch (Exception e) { conn.rollback(); // 回滚 e.printStackTrace(); request.setAttribute("error", "下单失败,请重试"); request.getRequestDispatcher("/order/confirm.jsp").forward(request, response); } }这个事务块保证了“订单创建+库存扣减”原子性。若扣库存失败(如超卖),整个订单回滚,购物车数据不变。
4.2 骑手抢单:乐观锁防并发冲突的实战写法
RiderServlet.java中抢单逻辑:
protected void acceptOrder(HttpServletRequest request, HttpServletResponse response) { int orderId = Integer.parseInt(request.getParameter("orderId")); int riderId = ((User) request.getSession().getAttribute("user")).getId(); // 关键:UPDATE时加条件,只更新status='WAITING'的订单 String sql = "UPDATE orders SET status='ASSIGNED', rider_id=?, update_time=NOW() WHERE id=? AND status='WAITING'"; int updated = queryRunner.update(sql, riderId, orderId); if (updated == 1) { // 抢单成功,推送消息(此处简化为写日志) System.out.println("骑手" + riderId + "抢单成功,订单号:" + orderId); response.sendRedirect(request.getContextPath() + "/rider/myorders.jsp"); } else { // updated==0,说明订单已被他人抢走 request.setAttribute("error", "很抱歉,该订单已被其他骑手接走"); request.getRequestDispatcher("/rider/waiting.jsp").forward(request, response); } }这种写法比“先SELECT再UPDATE”更高效,且天然防并发——两个骑手同时执行UPDATE,只有一个能成功(updated=1),另一个返回0,前端提示“订单已被抢”。
4.3 商家接单:状态机驱动的订单生命周期
商家后台的ShopServlet提供/shop/accept接口,但接单动作实际是状态跃迁:
// 订单状态流转规则(硬编码在ShopServlet中) // WAITING → ACCEPTED → PREPARING → READY → DELIVERED public void acceptOrder(HttpServletRequest request, HttpServletResponse response) { int orderId = Integer.parseInt(request.getParameter("orderId")); String currentStatus = orderDao.getStatus(orderId); if (!"WAITING".equals(currentStatus)) { request.setAttribute("error", "该订单不可接单(当前状态:" + currentStatus + ")"); request.getRequestDispatcher("/shop/orders.jsp").forward(request, response); return; } orderDao.updateStatus(orderId, "ACCEPTED"); response.sendRedirect(request.getContextPath() + "/shop/orders.jsp"); }这种显式状态校验比单纯UPDATE orders SET status='ACCEPTED' WHERE id=?更安全,避免商家误操作将已配送订单改成“已接单”。
4.4 管理员监控:实时订单看板的数据聚合逻辑
AdminServlet.java的dashboard方法:
public void dashboard(HttpServletRequest request, HttpServletResponse response) { // 1. 总订单数 int total = orderDao.countAll(); // 2. 各状态订单数(GROUP BY status) Map<String, Integer> statusCount = orderDao.countByStatus(); // 3. 今日新增订单(WHERE DATE(create_time) = CURDATE()) int todayNew = orderDao.countToday(); // 4. 热门商家TOP5(JOIN shops + GROUP BY shop_id) List<Shop> hotShops = shopDao.findHotShops(5); request.setAttribute("total", total); request.setAttribute("statusCount", statusCount); request.setAttribute("todayNew", todayNew); request.setAttribute("hotShops", hotShops); request.getRequestDispatcher("/admin/dashboard.jsp").forward(request, response); }注意:countByStatus()方法在OrderDao.java中使用原生SQL:
SELECT status, COUNT(*) as count FROM orders GROUP BY status而不是用Hibernate Criteria,因为JSP项目追求简单直接,没必要为统计功能引入ORM。
5. 调试与排错:定位JSP+Servlet项目“页面空白”“404”“500”的黄金三步法
5.1 第一步:看Tomcat控制台日志,不是浏览器F12
当页面一片空白或报错,立刻切到Eclipse底部Console标签页,观察Tomcat启动日志和HTTP请求日志。常见线索:
SEVERE: Error filterStart→ web.xml中filter类路径写错,如com.filter.AuthFilter实际在com.filters.AuthFilter;java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver→mysql-connector-java-8.0.33.jar没放进WebContent/WEB-INF/lib/;org.apache.jasper.JasperException: Unable to compile class for JSP→ JSP里Java代码语法错误,比如<% int x = ; %>少写了值;Caused by: java.sql.SQLException: Access denied for user 'root'@'localhost'→ jdbc.properties密码错误或MySQL用户权限不足。
5.2 第二步:用curl模拟请求,绕过浏览器缓存干扰
浏览器可能缓存了旧的404页面,用curl直击Servlet:
# 测试登录接口(POST) curl -X POST http://localhost:8080/online/login \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "username=admin" -d "password=123456" # 测试订单列表(GET,带session) curl -b "JSESSIONID=ABC123..." http://localhost:8080/online/order/list如果curl返回正常HTML,说明是前端JS或CSS路径问题;如果curl也报错,则是后端逻辑问题。
5.3 第三步:在关键Servlet打断点,看request域和session域
在LoginServlet.java的doPost()第一行加断点,启动Debug模式:
- 查看
request.getParameter("username")是否为空 → 检查form表单name属性是否拼错; - 查看
request.getSession().getAttribute("user")是否为null → 检查登录成功后是否真的存了session; - 查看
request.getRequestDispatcher("/member/index.jsp").forward(request, response)后,index.jsp中<%= request.getAttribute("user") %>是否输出null → 检查forward时request域是否被清空(forward是同一次请求,request域保留;sendRedirect是新请求,request域丢失)。
5.4 避坑:JSP开发中最容易忽略的5个细节
| 细节 | 为什么重要 | 如何验证 |
|---|---|---|
JSP页面顶部必须有<%@ page contentType="text/html;charset=UTF-8" %> | 缺少此声明会导致中文乱码,且部分Tomcat版本会报HTTP Status 500 | 查看浏览器开发者工具Network → Response Headers → Content-Type是否为text/html;charset=UTF-8 |
所有JSP中EL表达式${user.username}需开启EL解析 | Tomcat 7+默认开启,但若web.xml是Servlet 2.3规范,需手动开启:<%@ page isELIgnored="false" %> | 在JSP中写${1+1},若输出2则EL正常;若输出${1+1}则被忽略 |
| JSTL标签库必须引入且版本匹配 | 项目用<c:forEach>遍历订单,需jstl-1.2.jar和standard-1.1.2.jar | 检查WEB-INF/lib/是否有这两个jar,且<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>中的uri与jar包匹配 |
图片/CSS/JS路径用<%= request.getContextPath() %> | 写死/images/logo.png在部署到二级路径(如/myapp/)时会404 | 查看浏览器Network,确认资源请求URL是否为http://localhost:8080/online/images/logo.png |
| 数据库连接关闭必须在finally块 | Connection.close()未执行会导致连接池耗尽,后续请求全部超时 | 在BaseDao.java的closeConn()方法中加System.out.println("Connection closed");,观察日志是否每条SQL后都打印 |
6. 进阶技巧:把这套JSP+Servlet系统改造成可演示的“高光时刻”页面
6.1 会员端:加一个实时订单状态轮询,让下单体验不枯燥
原系统订单提交后直接跳转success.jsp,用户不知道何时配送。我们给会员首页加一个AJAX轮询:
<!-- member/index.jsp --> <div id="order-status"> <h3>您最近的订单</h3> <div id="status-refresh">正在查询...</div> </div> <script> function checkOrderStatus() { fetch('<%= request.getContextPath() %>/order/status?lastUpdateTime=' + new Date().getTime()) .then(response => response.json()) .then(data => { document.getElementById('status-refresh').innerHTML = `订单 ${data.orderId} 状态:${data.status}(${data.updateTime})`; if (data.status === 'DELIVERED') { clearInterval(timer); // 配送完成停止轮询 } }); } let timer = setInterval(checkOrderStatus, 5000); // 每5秒查一次 </script>后端OrderStatusServlet.java:
protected void doGet(HttpServletRequest request, HttpServletResponse response) { User user = (User) request.getSession().getAttribute("user"); // 查找该会员最新一条非DELIVERED订单 Order latest = orderDao.findLatestByUser(user.getId()); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write(new Gson().toJson(latest)); }这样会员就能看到“已接单→制作中→已出餐→配送中”的全过程,演示时观众会觉得“这系统真在跑”。
6.2 骑手端:用WebSocket实现抢单广播(轻量级改造)
虽然原系统是HTTP轮询,但我们可以在RiderServlet.acceptOrder()成功后,用Tomcat内置WebSocket推送:
// RiderServlet.java if (updated == 1) { // 推送抢单成功消息给所有在线骑手 WebSocketBroadcaster.broadcast("RIDER_" + riderId, "订单" + orderId + "已被抢走"); response.sendRedirect(...); }前端骑手页面监听:
const ws = new WebSocket("ws://localhost:8080/online/rider-ws"); ws.onmessage = function(event) { alert("新订单:" + event.data); };WebSocket服务端只需继承Endpoint,几行代码即可,比引入Spring WebSocket轻量得多。
6.3 管理员端:导出Excel报表的POI集成技巧
AdminServlet.exportOrders()方法:
public void exportOrders(HttpServletRequest request, HttpServletResponse response) { List<Order> orders = orderDao.findAll(); // 创建Excel工作簿 XSSFWorkbook workbook = new XSSFWorkbook(); XSSFSheet sheet = workbook.createSheet("订单报表"); // 表头 String[] headers = {"订单号", "会员", "商家", "总金额", "状态", "下单时间"}; XSSFRow headerRow = sheet.createRow(0); for (int i = 0; i < headers.length; i++) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum = 1; for (Order o : orders) { XSSFRow row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(o.getId()); row.createCell(1).setCellValue(o.getUserName()); row.createCell(2).setCellValue(o.getShopName()); row.createCell(3).setCellValue(o.getTotalPrice()); row.createCell(4).setCellValue(o.getStatus()); row.createCell(5).setCellValue(o.getCreateTime().toString()); } // 输出响应 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=orders.xlsx"); workbook.write(response.getOutputStream()); }关键点:poi-ooxml-4.1.2.jar必须放入WEB-INF/lib/,且web.xml中无需额外配置,POI纯Java实现,零依赖。
6.4 最后的血泪习惯:每次改完Servlet,必做三件事
- 清空Tomcat work目录:
/apache-tomcat-9.0.83/work/Catalina/localhost/online/,否则旧的JSP编译class残留,导致“改了代码却没生效”的玄学; - 重启Tomcat前,先stop再start:不要用Eclipse的“Restart”按钮,它有时只reload不重启,静态资源和类加载器状态可能错乱;
- 在
web.xml顶部加注释记录修改时间:
<!-- Last modified: 2024-06-15 by zhangsan - added /rider/complete mapping -->因为多人协作时,web.xml是路由总纲,谁动了哪条路径必须可追溯。
从那以后我每次改完Servlet,都强制走一遍这三步——哪怕只是改了一行response.sendRedirect()的路径。它不能让你写出更炫的代码,但能让你在凌晨两点接到“线上订单不入库”的电话时,30秒内定位到是OrderServlet的事务没提交,而不是怀疑MySQL主从同步出了问题。希望帮到你。
本文还有配套的精品资源,点击获取