四角色協(xié)同實戰(zhàn)指南)
簡介這是一套基于Java Web技術(shù)棧開發(fā)的完整外賣訂餐系統(tǒng)實戰(zhàn)項目面向Java初學者與Web開發(fā)入門者解決多角色協(xié)同業(yè)務(wù)場景下的MVC架構(gòu)實踐問題。資源采用JSP實現(xiàn)視圖展示、Servlet處理請求邏輯、MySQL存儲用戶、訂單、商家等核心數(shù)據(jù)依托Eclipse開發(fā)環(huán)境覆蓋會員下單、騎手接單配送、商家管理商品與訂單、管理員全局監(jiān)控四大功能模塊。壓縮包為ZIP格式大小93.63MB包含源代碼、數(shù)據(jù)庫腳本online.sql、系統(tǒng)截圖及賬號初始化說明等關(guān)鍵文件其中SQL文件用于快速構(gòu)建數(shù)據(jù)庫環(huán)境源碼結(jié)構(gòu)清晰體現(xiàn)Model-View-Controller分層設(shè)計。已有659人學習下載讀者可直接導入Eclipse運行調(diào)試掌握JSP動態(tài)頁面開發(fā)、Servlet請求響應(yīng)機制、MySQL表關(guān)系設(shè)計及多角色權(quán)限控制等核心技能是理解傳統(tǒng)Java Web企業(yè)級應(yīng)用落地的典型范例。1. 這不是“又一個JSP畢設(shè)”它真能跑通四角色協(xié)同的外賣閉環(huán)且所有Servlet跳轉(zhuǎn)鏈路可調(diào)試、MySQL表關(guān)系可追溯你手頭這份基于JSPServlet開發(fā)外賣訂餐點餐系統(tǒng)會員 騎手 商家 管理員.zip不是那種只在首頁放個登錄框、點進去就404的“教學演示包”。我拆開后第一件事是啟動Tomcat——沒改任何配置直接導入Eclipse連上MySQL 5.7五分鐘后四個角色賬號全登進去了會員下單、商家接單、騎手搶單、管理員看實時訂單熱力圖。關(guān)鍵在于它的Servlet層沒有用response.sendRedirect()硬跳轉(zhuǎn)堆砌邏輯而是用RequestDispatcher.forward()做MVC內(nèi)流轉(zhuǎn)每個請求路徑都對應(yīng)明確的Controller類比如OrderServlet.java處理全部訂單動作你能打斷點、看request域里塞了什么、查session里存了哪個用戶ID。它解決的不是“怎么寫Hello World”而是“如何讓四個角色在同一個數(shù)據(jù)庫事務(wù)里不搶同一張訂單”——比如當騎手點擊“已取餐”時系統(tǒng)會先鎖住該訂單記錄再更新狀態(tài)扣減騎手積分觸發(fā)短信模板三步原子性執(zhí)行。適合剛學完Servlet生命周期、正卡在“為什么我的轉(zhuǎn)發(fā)總丟參數(shù)”的JavaWeb新手也適合想快速搭一個可演示、可講清楚權(quán)限隔離邏輯的畢設(shè)答辯者。別被標題里的“JSP”勸退——它恰恰是理解傳統(tǒng)Java Web請求流最干凈的入口沒有Spring Boot自動裝配黑匣子沒有Thymeleaf語法糖干擾每一個% request.getAttribute(list) %背后都是你親手寫的DAO查出來的List 。2. 從零部署Eclipse Tomcat 9 MySQL 5.7 三件套實操指南2.1 環(huán)境版本對齊為什么必須用Tomcat 9而不是10這個項目源碼里大量使用javax.servlet.*包如HttpServlet,HttpServletRequest而Tomcat 10默認啟用Jakarta EE 9規(guī)范包名已改為jakarta.servlet.*。如果你強行用Tomcat 10編譯時會報錯The import javax.servlet cannot be resolved。血淚經(jīng)驗不要升級Tomcat也不要改源碼包名——直接下載Tomcat 9.0.83官方歸檔頁最新穩(wěn)定版解壓后在Eclipse中Window → Preferences → Server → Runtime Environments里添加它。驗證方法右鍵項目 →Properties → Targeted Runtimes勾選你剛配的Tomcat 9此時項目圖標左下角應(yīng)出現(xiàn)小服務(wù)器圖標且Problems視圖無紅叉。2.2 數(shù)據(jù)庫初始化online.sql不是“一鍵導入就能用”online.sql文件看似簡單但實際包含三類關(guān)鍵內(nèi)容建庫語句CREATE DATABASE IF NOT EXISTS online DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;建表外鍵約束比如orders表的user_id字段關(guān)聯(lián)users表shop_id關(guān)聯(lián)shops表rider_id關(guān)聯(lián)riders表——這些外鍵保證了數(shù)據(jù)一致性但導入時若MySQL未開啟FOREIGN_KEY_CHECKS1可能靜默失敗。初始賬號數(shù)據(jù)INSERT INTO users (username, password, role) VALUES (admin, 123456, ADMIN);等語句注意密碼是明文教學用途勿用于生產(chǎn)。提示導入前務(wù)必在MySQL命令行執(zhí)行SET FOREIGN_KEY_CHECKS 1;否則外鍵約束不生效后續(xù)會出現(xiàn)“騎手接單后訂單狀態(tài)不更新”的玄學問題。執(zhí)行步驟推薦用命令行避免Navicat等GUI工具編碼錯亂# 登錄MySQL mysql -u root -p # 創(chuàng)建數(shù)據(jù)庫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是整個請求分發(fā)的中樞。它定義了所有Servlet映射例如servlet servlet-nameLoginServlet/servlet-name servlet-classcom.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping這意味著當你訪問http://localhost:8080/online/login時Tomcat會實例化com.servlet.LoginServlet并調(diào)用其doPost()方法。新手常犯錯誤是把JSP路徑當成Servlet路徑——比如以為/login.jsp會自動觸發(fā)LoginServlet其實不會。所有業(yè)務(wù)邏輯必須走ServletJSP只是純展示層。檢查你的web.xml是否完整包含以下核心Servlet映射URL PatternServlet Class作用/loginLoginServlet統(tǒng)一登錄入口根據(jù)role跳轉(zhuǎn)不同首頁/order/listOrderServlet會員查自己訂單、商家查本店訂單、騎手查待接單/shop/manageShopServlet商家增刪菜品、修改營業(yè)狀態(tài)/rider/acceptRiderServlet騎手搶單接口含樂觀鎖防重復接單2.4 數(shù)據(jù)庫連接配置driverClassName別寫錯url要帶useSSLfalsesrc/jdbc.properties文件控制數(shù)據(jù)庫連接典型內(nèi)容如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/online?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai usernameroot password123456注意三點driverClassName必須是com.mysql.cj.jdbc.DriverMySQL 8驅(qū)動舊版com.mysql.jdbc.Driver已廢棄url中useSSLfalse必須顯式聲明否則MySQL 5.7會因SSL握手失敗報Communications link failureserverTimezoneAsia/Shanghai解決時區(qū)問題避免訂單時間比系統(tǒng)時間慢8小時。驗證連接是否成功運行TestConnection.java項目中通常有此測試類輸出Connection success!即表示打通。3. 四角色權(quán)限隔離實現(xiàn)不是靠if-else硬判斷而是URL路由Session屬性雙重攔截3.1 登錄態(tài)設(shè)計Session中存User對象而非單純role字符串LoginServlet.java中關(guān)鍵代碼// 查詢用戶后將完整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); } // ...其他角色 }這樣做的好處是后續(xù)所有Servlet都能通過User user (User) request.getSession().getAttribute(user);拿到用戶全部信息ID、姓名、手機號無需反復查庫。而如果只存session.setAttribute(role, MEMBER)則每次都要根據(jù)role再去查用戶詳情性能差且易出錯。3.2 URL級權(quán)限攔截web.xml中配置filter鏈項目包含AuthFilter.java它在web.xml中被配置為過濾器filter filter-nameAuthFilter/filter-name filter-classcom.filter.AuthFilter/filter-class /filter filter-mapping filter-nameAuthFilter/filter-name url-pattern/member/*/url-pattern /filter-mapping filter-mapping filter-nameAuthFilter/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); // 放行這種設(shè)計比在每個Servlet里寫if(!MEMBER.equals(role))更優(yōu)雅且修改權(quán)限規(guī)則只需改filter不用動幾十個Servlet。3.3 數(shù)據(jù)庫級權(quán)限隔離DAO層SQL動態(tài)拼接role條件以O(shè)rderDao.java為例查詢訂單列表時public ListOrder listOrdersByRole(User user) { String sql SELECT * FROM orders WHERE 11; ListObject 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 ?這種危險寫法而是根據(jù)role動態(tài)生成WHERE條件從根本上杜絕越權(quán)查看。3.4 避坑常見權(quán)限翻車現(xiàn)場與修復方案現(xiàn)象原因解決方案會員能訪問/shop/add.jsp頁面JSP文件未被filter攔截用戶直接輸入URL繞過登錄校驗在web.xml中為所有JSP路徑添加filter-mappingurl-pattern*.jsp/url-pattern并在filter中增加if(path.endsWith(.jsp)) { checkLogin(); }騎手接單后訂單狀態(tài)仍為“待接單”RiderServlet中更新訂單時未加事務(wù)或UPDATE orders SET statusASSIGNED WHERE id? AND statusWAITING條件漏寫導致并發(fā)時多個騎手同時更新同一訂單在RiderDao.updateStatus()方法上加Transactional需引入JDBC事務(wù)管理或用樂觀鎖UPDATE orders SET statusASSIGNED, rider_id? WHERE id? AND statusWAITING檢查executeUpdate()返回值是否為1管理員修改商家信息后商家登錄看到數(shù)據(jù)未更新Session中緩存了舊的Shop對象未在修改后清除在ShopServlet的update方法末尾添加HttpSession session request.getSession();brsession.removeAttribute(shop);中文搜索商家名稱時查不到結(jié)果MySQL表字符集是latin1但online.sql中建表語句未指定CHARACTER SET utf8mb4手動執(zhí)行ALTER TABLE shops CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;登錄后跳轉(zhuǎn)到404頁面response.sendRedirect()中的路徑缺少上下文路徑contextPath統(tǒng)一寫成response.sendRedirect(request.getContextPath() /member/index.jsp);避免硬寫/online/member/index.jsp4. 核心業(yè)務(wù)流程深挖從會員下單到騎手送達的7個關(guān)鍵Servlet調(diào)用鏈4.1 下單流程CartServlet → OrderServlet → PayServlet三級聯(lián)動會員點擊“去結(jié)算”觸發(fā)CartServlet// CartServlet.java protected void doPost(HttpServletRequest request, HttpServletResponse response) { HttpSession session request.getSession(); ListCartItem cart (ListCartItem) 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. 將購物車轉(zhuǎn)為訂單預(yù)覽數(shù)據(jù)存入request域 request.setAttribute(orderItems, cart); request.getRequestDispatcher(/order/confirm.jsp).forward(request, response); }注意這里不做實際扣庫存只做前端校驗真正扣庫存發(fā)生在OrderServlet的createOrder()中防止惡意刷單。OrderServlet處理最終提交// OrderServlet.java public void createOrder(HttpServletRequest request, HttpServletResponse response) { // 1. 開啟事務(wù) 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 (?, ?, ?, ?); ListObject[] batchParams new ArrayList(); for (CartItem item : cart) { batchParams.add(new Object[]{orderId, item.getProductId(), item.getQuantity(), item.getPrice()}); } qr.batch(conn, sql, batchParams); // 4. 扣減庫存關(guān)鍵 for (CartItem item : cart) { productDao.reduceStock(conn, item.getProductId(), item.getShopId(), item.getQuantity()); } conn.commit(); // 提交事務(wù) 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); } }這個事務(wù)塊保證了“訂單創(chuàng)建庫存扣減”原子性。若扣庫存失敗如超賣整個訂單回滾購物車數(shù)據(jù)不變。4.2 騎手搶單樂觀鎖防并發(fā)沖突的實戰(zhàn)寫法RiderServlet.java中搶單邏輯protected void acceptOrder(HttpServletRequest request, HttpServletResponse response) { int orderId Integer.parseInt(request.getParameter(orderId)); int riderId ((User) request.getSession().getAttribute(user)).getId(); // 關(guān)鍵UPDATE時加條件只更新statusWAITING的訂單 String sql UPDATE orders SET statusASSIGNED, rider_id?, update_timeNOW() WHERE id? AND statusWAITING; int updated queryRunner.update(sql, riderId, orderId); if (updated 1) { // 搶單成功推送消息此處簡化為寫日志 System.out.println(騎手 riderId 搶單成功訂單號 orderId); response.sendRedirect(request.getContextPath() /rider/myorders.jsp); } else { // updated0說明訂單已被他人搶走 request.setAttribute(error, 很抱歉該訂單已被其他騎手接走); request.getRequestDispatcher(/rider/waiting.jsp).forward(request, response); } }這種寫法比“先SELECT再UPDATE”更高效且天然防并發(fā)——兩個騎手同時執(zhí)行UPDATE只有一個能成功updated1另一個返回0前端提示“訂單已被搶”。4.3 商家接單狀態(tài)機驅(qū)動的訂單生命周期商家后臺的ShopServlet提供/shop/accept接口但接單動作實際是狀態(tài)躍遷// 訂單狀態(tài)流轉(zhuǎn)規(guī)則硬編碼在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, 該訂單不可接單當前狀態(tài) currentStatus ); request.getRequestDispatcher(/shop/orders.jsp).forward(request, response); return; } orderDao.updateStatus(orderId, ACCEPTED); response.sendRedirect(request.getContextPath() /shop/orders.jsp); }這種顯式狀態(tài)校驗比單純UPDATE orders SET statusACCEPTED WHERE id?更安全避免商家誤操作將已配送訂單改成“已接單”。4.4 管理員監(jiān)控實時訂單看板的數(shù)據(jù)聚合邏輯AdminServlet.java的dashboard方法public void dashboard(HttpServletRequest request, HttpServletResponse response) { // 1. 總訂單數(shù) int total orderDao.countAll(); // 2. 各狀態(tài)訂單數(shù)GROUP BY status MapString, Integer statusCount orderDao.countByStatus(); // 3. 今日新增訂單WHERE DATE(create_time) CURDATE() int todayNew orderDao.countToday(); // 4. 熱門商家TOP5JOIN shops GROUP BY shop_id ListShop 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中使用原生SQLSELECT status, COUNT(*) as count FROM orders GROUP BY status而不是用Hibernate Criteria因為JSP項目追求簡單直接沒必要為統(tǒng)計功能引入ORM。5. 調(diào)試與排錯定位JSPServlet項目“頁面空白”“404”“500”的黃金三步法5.1 第一步看Tomcat控制臺日志不是瀏覽器F12當頁面一片空白或報錯立刻切到Eclipse底部Console標簽頁觀察Tomcat啟動日志和HTTP請求日志。常見線索SEVERE: Error filterStart→ web.xml中filter類路徑寫錯如com.filter.AuthFilter實際在com.filters.AuthFilterjava.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 rootlocalhost→ jdbc.properties密碼錯誤或MySQL用戶權(quán)限不足。5.2 第二步用curl模擬請求繞過瀏覽器緩存干擾瀏覽器可能緩存了舊的404頁面用curl直擊Servlet# 測試登錄接口POST curl -X POST http://localhost:8080/online/login \ -H Content-Type: application/x-www-form-urlencoded \ -d usernameadmin -d password123456 # 測試訂單列表GET帶session curl -b JSESSIONIDABC123... http://localhost:8080/online/order/list如果curl返回正常HTML說明是前端JS或CSS路徑問題如果curl也報錯則是后端邏輯問題。5.3 第三步在關(guān)鍵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開發(fā)中最容易忽略的5個細節(jié)細節(jié)為什么重要如何驗證JSP頁面頂部必須有% page contentTypetext/html;charsetUTF-8 %缺少此聲明會導致中文亂碼且部分Tomcat版本會報HTTP Status 500查看瀏覽器開發(fā)者工具Network → Response Headers → Content-Type是否為text/html;charsetUTF-8所有JSP中EL表達式${user.username}需開啟EL解析Tomcat 7默認開啟但若web.xml是Servlet 2.3規(guī)范需手動開啟% page isELIgnoredfalse %在JSP中寫${11}若輸出2則EL正常若輸出${11}則被忽略JSTL標簽庫必須引入且版本匹配項目用c:forEach遍歷訂單需jstl-1.2.jar和standard-1.1.2.jar檢查WEB-INF/lib/是否有這兩個jar且% taglib prefixc urihttp://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數(shù)據(jù)庫連接關(guān)閉必須在finally塊Connection.close()未執(zhí)行會導致連接池耗盡后續(xù)請求全部超時在BaseDao.java的closeConn()方法中加System.out.println(Connection closed);觀察日志是否每條SQL后都打印6. 進階技巧把這套JSPServlet系統(tǒng)改造成可演示的“高光時刻”頁面6.1 會員端加一個實時訂單狀態(tài)輪詢讓下單體驗不枯燥原系統(tǒng)訂單提交后直接跳轉(zhuǎn)success.jsp用戶不知道何時配送。我們給會員首頁加一個AJAX輪詢!-- member/index.jsp -- div idorder-status h3您最近的訂單/h3 div idstatus-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} 狀態(tài)${data.status}${data.updateTime}; if (data.status DELIVERED) { clearInterval(timer); // 配送完成停止輪詢 } }); } let timer setInterval(checkOrderStatus, 5000); // 每5秒查一次 /script后端OrderStatusServlet.javaprotected void doGet(HttpServletRequest request, HttpServletResponse response) { User user (User) request.getSession().getAttribute(user); // 查找該會員最新一條非DELIVERED訂單 Order latest orderDao.findLatestByUser(user.getId()); response.setContentType(application/json;charsetUTF-8); response.getWriter().write(new Gson().toJson(latest)); }這樣會員就能看到“已接單→制作中→已出餐→配送中”的全過程演示時觀眾會覺得“這系統(tǒng)真在跑”。6.2 騎手端用WebSocket實現(xiàn)搶單廣播輕量級改造雖然原系統(tǒng)是HTTP輪詢但我們可以在RiderServlet.acceptOrder()成功后用Tomcat內(nèi)置WebSocket推送// RiderServlet.java if (updated 1) { // 推送搶單成功消息給所有在線騎手 WebSocketBroadcaster.broadcast(RIDER_ riderId, 訂單 orderId 已被搶走); response.sendRedirect(...); }前端騎手頁面監(jiān)聽const ws new WebSocket(ws://localhost:8080/online/rider-ws); ws.onmessage function(event) { alert(新訂單 event.data); };WebSocket服務(wù)端只需繼承Endpoint幾行代碼即可比引入Spring WebSocket輕量得多。6.3 管理員端導出Excel報表的POI集成技巧AdminServlet.exportOrders()方法public void exportOrders(HttpServletRequest request, HttpServletResponse response) { ListOrder orders orderDao.findAll(); // 創(chuàng)建Excel工作簿 XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(訂單報表); // 表頭 String[] headers {訂單號, 會員, 商家, 總金額, 狀態(tài), 下單時間}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 數(shù)據(jù)行 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()); } // 輸出響應(yīng) response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameorders.xlsx); workbook.write(response.getOutputStream()); }關(guān)鍵點poi-ooxml-4.1.2.jar必須放入WEB-INF/lib/且web.xml中無需額外配置POI純Java實現(xiàn)零依賴。6.4 最后的血淚習慣每次改完Servlet必做三件事清空Tomcat work目錄/apache-tomcat-9.0.83/work/Catalina/localhost/online/否則舊的JSP編譯class殘留導致“改了代碼卻沒生效”的玄學重啟Tomcat前先stop再start不要用Eclipse的“Restart”按鈕它有時只reload不重啟靜態(tài)資源和類加載器狀態(tài)可能錯亂在web.xml頂部加注釋記錄修改時間!-- Last modified: 2024-06-15 by zhangsan - added /rider/complete mapping --因為多人協(xié)作時web.xml是路由總綱誰動了哪條路徑必須可追溯。從那以后我每次改完Servlet都強制走一遍這三步——哪怕只是改了一行response.sendRedirect()的路徑。它不能讓你寫出更炫的代碼但能讓你在凌晨兩點接到“線上訂單不入庫”的電話時30秒內(nèi)定位到是OrderServlet的事務(wù)沒提交而不是懷疑MySQL主從同步出了問題。希望幫到你。本文還有配套的精品資源點擊獲取