拆解:從依賴配置到部署優(yōu)化)
簡(jiǎn)介基于SSM與JSP技術(shù)的二手交易平臺(tái)網(wǎng)站項(xiàng)目是一份適合畢業(yè)設(shè)計(jì)、課程設(shè)計(jì)及期末大作業(yè)的完整JavaWeb源碼包面向需要快速掌握SSM框架的在校生和初級(jí)開發(fā)者。壓縮包共2000個(gè)文件整體約53.33MB包含716個(gè)JS腳本、298個(gè)JSP頁面、290個(gè)CSS樣式、119個(gè)Java類以及117個(gè)XML配置文件前后端源碼、數(shù)據(jù)庫腳本、開發(fā)工具均已整理齊全。系統(tǒng)功能覆蓋商品信息展示、交易管理、后臺(tái)操作等常見模塊界面簡(jiǎn)潔、交互流暢代碼注釋清晰新手可按模塊逐步閱讀項(xiàng)目結(jié)構(gòu)層次分明便于理解SSM框架的MVC分層思想。部署時(shí)建議使用MySQL 5.7和Tomcat 7.x或8.x項(xiàng)目已經(jīng)過嚴(yán)格調(diào)試并附有運(yùn)行視頻教學(xué)與部署答疑支持。已有112人參與學(xué)習(xí)下載后可快速參考部署適合作為高分開題或課程設(shè)計(jì)的參考項(xiàng)目藍(lán)本。1. 一套 SSMJSPHTML5 的二手商品交易系統(tǒng)到底值不值得拆拆這套基于 SSM JSP HTML5 的二手交易平臺(tái)時(shí)我先把“238”這個(gè)編號(hào)忽略掉——它更像是課程設(shè)計(jì)里的項(xiàng)目代號(hào)真正決定復(fù)現(xiàn)難度的是三個(gè)框架的版本協(xié)同、JSP 在 WEB-INF 下的路徑規(guī)則以及 HTML5 本地存儲(chǔ)與后端接口的職責(zé)切分。這套系統(tǒng)的價(jià)值在于它完整保留了 Java Web 單體時(shí)代的交往模式Spring 管 BeanSpringMVC 管路由MyBatis 管 SQLJSP 負(fù)責(zé)服務(wù)端渲染HTML5 只在交互層補(bǔ)充體驗(yàn)。如果你正在準(zhǔn)備 Java 課程設(shè)計(jì)或者需要把一個(gè)老項(xiàng)目改成可運(yùn)行系統(tǒng)這篇正文能幫你把啟動(dòng)順序、事務(wù)邊界和部署參數(shù)一次理清。2. 從空項(xiàng)目到 SSM 容器Spring、SpringMVC、MyBatis 三條線的縫合法2.1 先把依賴釘死再談代碼搭建 SSM 二手交易系統(tǒng)最容易翻車的是依賴版本互相打架。我一般按“Servlet 容器 → Spring → MyBatis → JSON 庫”的順序鎖版本。以 Tomcat 8.5 JDK 8 為例Maven 里的關(guān)鍵依賴是這樣一組properties spring.version5.1.18.RELEASE/spring.version mybatis.version3.5.6/mybatis.version /properties dependencies !-- Spring MVC 與上下文必須同版本否則容器會(huì)報(bào) NoSuchMethodError -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis 官方 Spring 整合包版本要和 mybatis 對(duì)齊 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency !-- MySQL 驅(qū)動(dòng)5.x 驅(qū)動(dòng)類名是 com.mysql.jdbc.Driver -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version5.1.49/version scoperuntime/scope /dependency !-- JSP 標(biāo)簽庫與 APIjsp-api 必須 provided避免和 Tomcat 內(nèi)置類沖突 -- dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdjavax.servlet.jsp/groupId artifactIdjsp-api/artifactId version2.2/version scopeprovided/scope /dependency /dependencies為什么要把版本單獨(dú)抽成 properties因?yàn)?Spring 的 webmvc、context、tx 必須同版本MyBatis 3.5.6 與 mybatis-spring 2.0.6 的搭配針對(duì) Spring 5.1 做過官方測(cè)試。MySQL 驅(qū)動(dòng)選 5.1.49 是為了兼容老項(xiàng)目常用的連接串如果數(shù)據(jù)庫是 8.0就把驅(qū)動(dòng)換成 8.0 系列并把 className 改成com.mysql.cj.jdbc.Driver。jstl 1.2 和 jsp-api 2.2 一個(gè)放 classpath、一個(gè)用 provided是為了避免 Tomcat 9 下 jar 沖突出現(xiàn)Unable to compile class for JSP的報(bào)錯(cuò)。2.2 web.xml 中的加載順序決定 DispatcherServlet 會(huì)不會(huì)吞掉 JSPSSM 的入口仍然是 web.xml但不同配置會(huì)讓行為差很多。二手交易這類帶大量 JSP 頁面的項(xiàng)目我建議按下面這種結(jié)構(gòu)寫web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd version3.1 !-- 先創(chuàng)建根容器負(fù)責(zé) service、dao、數(shù)據(jù)源等 -- listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener context-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-context.xml/param-value /context-param !-- 再創(chuàng)建 SpringMVC 子容器負(fù)責(zé) controller 與視圖解析 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring/spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping !-- 必須放在 DispatcherServlet 之后且映射到 /*否則 POST 提交中文會(huì)亂碼 -- filter filter-nameencoding/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencoding/filter-name url-pattern/*/url-pattern /filter-mapping /web-app這里的順序很關(guān)鍵ContextLoaderListener先創(chuàng)建根容器DispatcherServlet再創(chuàng)建子容器子容器能訪問父容器的 Bean但父容器看不到子容器。如果url-pattern寫成*.doJSP 頁面里的靜態(tài)資源路徑會(huì)好理解但 HTML5 頁面想用干凈的/item/123路由就不方便所以這里用/配合mvc:default-servlet-handler/放行靜態(tài)資源。配置項(xiàng)位置作用context-paramweb.xml指定根容器配置文件路徑ContextLoaderListenerweb.xml創(chuàng)建 service/dao 所在的 Spring 容器DispatcherServletweb.xml創(chuàng)建 controller 所在子容器InternalResourceViewResolverspring-mvc.xml決定 JSP 前綴后綴2.3 Controller 和 Service 分兩個(gè)容器掃事務(wù)代理才安全很多課程設(shè)計(jì)的 SSM 項(xiàng)目把context:component-scan base-packagecom.shop/同時(shí)寫在兩個(gè)配置里結(jié)果出現(xiàn)同一個(gè) Service 被兩個(gè)容器各實(shí)例化一次。SpringMVC 子容器會(huì)優(yōu)先用自己容器里的 Service而這個(gè) Service 沒有經(jīng)過 AOP 代理Transactional直接失效。正確做法是在根容器掃排除 Controller!-- spring-context.xml -- context:component-scan base-packagecom.shop context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean iddataSource classorg.apache.commons.dbcp.BasicDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/second_hand?useUnicodetrueamp;characterEncodingUTF-8/ property nameusername valueroot/ property namepassword valueroot/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdataSource/ property nametypeAliasesPackage valuecom.shop.entity/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean mybatis:scan base-packagecom.shop.mapper/子容器只掃 Controller!-- spring-mvc.xml -- mvc:annotation-driven/ mvc:default-servlet-handler/ context:component-scan base-packagecom.shop.controller use-default-filtersfalse context:include-filter typeannotation expressionorg.springframework.stereotype.Controller/ /context:component-scan bean idviewResolver classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean分完容器后JSP 頁面要放在webapp/WEB-INF/jsp下用戶直接訪問該路徑會(huì)被容器拒絕只能通過 controller 里return item_list轉(zhuǎn)發(fā)到/WEB-INF/jsp/item_list.jsp。這樣既保住了 JSP 服務(wù)端渲染能力又避免了和 HTML5 靜態(tài)資源混在一起。提示如果項(xiàng)目在 IDEA 中直接啟動(dòng)Artifact 類型一定要選 war exploded否則 JSP 修改后要重啟才會(huì)生效。3. 二手交易數(shù)據(jù)模型與 MyBatis 持久層從表結(jié)構(gòu)到動(dòng)態(tài) SQL3.1 五張表足夠撐起一個(gè)交易閉環(huán)把這套系統(tǒng)的業(yè)務(wù)收攏一下核心數(shù)據(jù)模型可以壓縮成五張表用戶、商品、分類、收藏、訂單。二手交易的特點(diǎn)是商品狀態(tài)會(huì)從“在售”流轉(zhuǎn)到“已賣出”因此設(shè)計(jì)表結(jié)構(gòu)時(shí)要特意留狀態(tài)字段而不是在商品被打下架后直接刪除記錄。表名職責(zé)關(guān)鍵字段user用戶注冊(cè)與登錄id, username, password_hash, phoneitem商品發(fā)布與檢索id, seller_id, category_id, title, price, stock, statuscategory商品分類樹id, name, parent_idcollect收藏關(guān)系id, user_id, item_id, create_timeorder_info交易訂單id, buyer_id, item_id, amount, status以下是一張可直接建的表字段注釋盡量寫全后面 mapper 寫動(dòng)態(tài) SQL 時(shí)就不用來回翻表結(jié)構(gòu)CREATE TABLE item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, seller_id INT UNSIGNED NOT NULL COMMENT 賣家用戶ID, category_id INT UNSIGNED NOT NULL COMMENT 商品分類ID, title VARCHAR(120) NOT NULL COMMENT 商品標(biāo)題, price DECIMAL(10,2) NOT NULL COMMENT 出售價(jià)格, stock INT UNSIGNED NOT NULL DEFAULT 1 COMMENT 可賣數(shù)量二手商品通常為1, status TINYINT NOT NULL DEFAULT 0 COMMENT 0-在售 1-下架 2-已賣出, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_seller (seller_id), KEY idx_category_status (category_id,status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;價(jià)格必須用DECIMAL(10,2)不能用 FLOAT。float 是近似值訂單金額用浮點(diǎn)會(huì)出現(xiàn) 19.99 變成 19.989999 的金額誤差對(duì)賬時(shí)很難解釋。狀態(tài)字段用 TINYINT 而不是 VARCHAR是為了讓商品列表查詢能用組合索引idx_category_status把分類過濾和狀態(tài)過濾放在同一個(gè)索引里。create_time使用DATETIME而不是TIMESTAMP避免 2038 年問題也方便前端直接用字符串展示。3.2 Mapper 接口與 XML 分離查詢條件怎么拼才安全二手商品列表的搜索條件通常包含標(biāo)題關(guān)鍵字、分類、最低價(jià)、最高價(jià)、狀態(tài)和分頁。如果把這么多條件全塞進(jìn) Controller 再傳給 MapperSQL 會(huì)因?yàn)锳ND和WHERE的位置問題反復(fù)報(bào)錯(cuò)。常見做法是定義一個(gè)ItemQuery查詢對(duì)象只放查詢字段public class ItemQuery { private String title; private Integer categoryId; private BigDecimal minPrice; private BigDecimal maxPrice; private Integer status; private Integer offset; private Integer pageSize; // getter/setter 省略 }接口定義public interface ItemMapper { ListItem selectByCondition(Param(condition) ItemQuery query); int insertItem(Item item); int deductStock(Param(id) Integer id); }對(duì)應(yīng) XML 里用動(dòng)態(tài) SQL 拼接select idselectByCondition resultTypecom.shop.entity.Item SELECT id, seller_id, category_id, title, price, stock, status, create_time FROM item where if testcondition.title ! null and condition.title ! AND title LIKE CONCAT(%, #{condition.title}, %) /if if testcondition.categoryId ! null AND category_id #{condition.categoryId} /if if testcondition.minPrice ! null AND price gt; #{condition.minPrice} /if if testcondition.maxPrice ! null AND price lt; #{condition.maxPrice} /if if testcondition.status ! null AND status #{condition.status} /if /where ORDER BY id DESC LIMIT #{condition.offset}, #{condition.pageSize} /selectwhere標(biāo)簽會(huì)自動(dòng)去掉第一個(gè)AND所以每個(gè)if判斷里的條件都要寫AND這一步是復(fù)現(xiàn)時(shí)最容易抄錯(cuò)的地方。LIKE CONCAT(%, #{title}, %)把百分號(hào)拼進(jìn)參數(shù)里而不是用${condition.title}直接拼接原因是#{}會(huì)走 PreparedStatement 參數(shù)占位從根上避免 SQL 注入${}只適合表名、排序字段這類固定白名單位置絕不能用于用戶輸入。3.3 庫存扣減與事務(wù)邊界誰的 Service 該加注解二手交易和普通電商不同多數(shù)商品庫存只有 1但仍然會(huì)出現(xiàn)兩個(gè)人同時(shí)下單的情況。如果不做控制超賣后訂單表里會(huì)出現(xiàn)兩個(gè)買家都買同一件在售商品。常見做法是事務(wù)加條件更新Service 層這樣寫Service public class OrderService { Autowired private OrderMapper orderMapper; Autowired private ItemMapper itemMapper; Transactional(rollbackFor Exception.class) public Integer createOrder(Order order) { int updated itemMapper.deductStock(order.getItemId()); if (updated 0) { throw new BizException(商品已下架或庫存不足); } orderMapper.insertOrder(order); return order.getId(); } }deductStock的 SQL 是UPDATE item SET stock stock - 1 WHERE id #{id} AND stock 0。影響行數(shù)為 0 表示庫存不足直接拋業(yè)務(wù)異常讓事務(wù)整體回滾。這種方法比先 SELECT 再 UPDATE 更穩(wěn)避免兩個(gè)請(qǐng)求同時(shí)把 stock 讀到 1后一個(gè)提交時(shí)覆蓋前一個(gè)的扣減。Transactional放在 Service 而不是 Controller是因?yàn)槭聞?wù)要同時(shí)覆蓋deductStock和insertOrder兩個(gè) Mapper 方法的調(diào)用范圍。如果在 Controller 層加注解代理對(duì)象無效異常不會(huì)觸發(fā)回滾。注意MyBatis 的if只做動(dòng)態(tài)拼接不做類型轉(zhuǎn)換日期范圍需要現(xiàn)在 Service 里把 Date 轉(zhuǎn)成字符串再傳入。4. JSP 頁面與 HTML5 交互商品搜索、發(fā)布表單和本地緩存4.1 JSP 的標(biāo)簽循環(huán)與 EL 取值SSM 項(xiàng)目里 JSP 負(fù)責(zé)服務(wù)端渲染商品列表頁的數(shù)據(jù)在 Controller 存入Model然后由 JSP 用 JSTL 標(biāo)簽遍歷輸出% page contentTypetext/html;charsetUTF-8 languagejava % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % c:forEach varitem items${page.list} varStatusvs div classcard>form idpublishForm action/item methodpost input typetext nametitle required minlength4 maxlength120 placeholder商品名稱 input typenumber nameprice required min0.01 step0.01 placeholder價(jià)格 input typedate nameexpireDate input typeurl nameimageUrl placeholder圖片地址 input typetext listcategories namecategoryText datalist idcategories option value1手機(jī)數(shù)碼/option option value2家具家電/option option value3圖書教材/option /datalist button typesubmit發(fā)布/button /formtypenumber配合min和step0.01能限制用戶輸入三位小數(shù)typedate在 Chrome、Edge 上會(huì)彈出日歷但 Firefox 桌面端會(huì)退化為普通文本框所以后端要允許非標(biāo)準(zhǔn)格式并做轉(zhuǎn)換。datalist看起來像下拉框但用戶仍然能自由輸入服務(wù)端必須判斷傳入的分類 ID 是否存在于 category 表否則會(huì)出現(xiàn)孤立分類。HTML5元素前端作用服務(wù)端校驗(yàn)重點(diǎn)typenumber限制數(shù)字格式用 BigDecimal 接收不能以 String 存價(jià)格typedate日期選擇處理空值格式化 yyyy-MM-ddtypeurlURL 格式校驗(yàn)防止javascript:alert(1)注入datalist輸入提示分類 ID 必須存在4.3 用 localStorage 做瀏覽歷史把“最近看過”放在前端JSP 頁面每次跳轉(zhuǎn)會(huì)刷新整頁要記錄用戶看過哪些商品最簡(jiǎn)單的方法不是在后端建瀏覽表而是用 HTML5 的 localStorage 在瀏覽器本地存商品 ID。這段代碼可以放在詳情頁底部function saveHistory(itemId) { const key used_item_history; let history []; try { history JSON.parse(localStorage.getItem(key)) || []; } catch (e) { history []; } history [itemId].concat(history.filter(id id ! itemId)).slice(0, 10); localStorage.setItem(key, JSON.stringify(history)); } // 從 URL 取商品 ID例如 /item/123 const match location.pathname.match(/\/item\/(\d)/); if (match) { saveHistory(parseInt(match[1], 10)); }這段代碼做了三件容易被忽略的事JSON.parse 包了 try/catch避免同一域名下其他頁面寫入臟數(shù)據(jù)導(dǎo)致腳本中斷filter(id id ! itemId)先去重再用concat把當(dāng)前商品放到最前面slice(0, 10)只保留 10 條防止 localStorage 被無限撐大。瀏覽歷史只存 ID不存商品標(biāo)題和價(jià)格因?yàn)槎稚唐穬r(jià)格會(huì)變存快照反而會(huì)讓頁面顯示過期數(shù)據(jù)。展示歷史時(shí)再調(diào)用批量接口async function renderHistory() { const key used_item_history; let ids []; try { ids JSON.parse(localStorage.getItem(key)) || []; } catch (e) { return; } if (ids.length 0) return; const params new URLSearchParams(); ids.forEach(id params.append(id, id)); const resp await fetch(/item/batch? params.toString()); const result await resp.json(); // result.data 是商品列表按 ids 順序重新排列 }URLSearchParams會(huì)把數(shù)組編碼成id1id2的格式后端可以用RequestParam(id) ListInteger ids接收。前端把商品 ID 傳給服務(wù)端拿數(shù)據(jù)比直接存儲(chǔ)整個(gè)商品對(duì)象更安全也更容易清理。4.4 用 fetch 提交表單解析統(tǒng)一 JSONJSP 頁面里用原生 HTML5 fetch 也能做無刷新提交關(guān)鍵是后端返回的 JSON 格式要統(tǒng)一const form document.getElementById(publishForm); form.addEventListener(submit, async (e) { e.preventDefault(); const body new FormData(form); const resp await fetch(form.action, { method: POST, body: body }); if (!resp.ok) { alert(請(qǐng)求失敗稍后再試); return; } const result await resp.json(); if (result.code 0) { location.href /item/ result.data.id; } else { alert(result.message); } });這里先檢查resp.ok再解析 JSON否則后端返回 500 錯(cuò)誤頁時(shí)響應(yīng)體是 HTMLresp.json()會(huì)報(bào)Unexpected token 這種讓人摸不著頭腦的錯(cuò)誤。FormData自動(dòng)把表單里的 name 組裝成參數(shù)SpringMVC 用RequestParam或者直接讓Item對(duì)象的字段名與 name 一致就能完成綁定。提示使用 localStorage 記錄瀏覽歷史前先在頁面里做一次 JSON.parse 的 try/catch避免臟數(shù)據(jù)讓整個(gè)腳本失效。5. 部署到 Tomcat 之后連接池參數(shù)、日志定位和頁面緩存優(yōu)化5.1 打包后先看啟動(dòng)日志里的 Deployment 字樣本地開發(fā)通過 IDEA 跑 Spring Boot 的人第一次部署這種 war 包項(xiàng)目往往會(huì)卡在“Tomcat 明明啟動(dòng)了但頁面 404”。正確流程是mvn clean package -DskipTests cp target/shop-web.war $CATALINA_HOME/webapps/ cd $CATALINA_HOME/bin ./startup.sh tail -f $CATALINA_HOME/logs/catalina.out日志里出現(xiàn)Deployment of web application archive [shop-web.war] has finished才代表容器成功識(shí)別應(yīng)用。如果沒有這一行優(yōu)先檢查web.xml里web-app的版本聲名Servlet 3.1 規(guī)范在 Tomcat 8.5 下使用absolute-ordering/可能會(huì)跳過部分注解掃描需要在web.xml里顯式聲明需要的 jar。5.2 Druid 連接池的參數(shù)別照抄項(xiàng)目數(shù)據(jù)庫連接池如果換成 Druid很多課程設(shè)計(jì)喜歡把網(wǎng)上的參數(shù)整段復(fù)制結(jié)果maxWait設(shè)成600000一個(gè)查詢卡了會(huì)話十秒才報(bào)錯(cuò)。二手交易平臺(tái)這種中小流量系統(tǒng)合理初始參數(shù)是bean iddataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/second_hand?useUnicodetrueamp;characterEncodingUTF-8/ property nameusername valueroot/ property namepassword valueroot/ property nameinitialSize value5/ property nameminIdle value5/ property namemaxActive value20/ property namemaxWait value3000/ property namevalidationQuery valueSELECT 1/ property nametestWhileIdle valuetrue/ /beaninitialSize代表啟動(dòng)時(shí)建立 5 個(gè)連接maxActive20要結(jié)合 MySQL 的max_connections一起看。Linux 下登錄 MySQL 執(zhí)行SHOW VARIABLES LIKE max_connections;如果數(shù)據(jù)庫限制 100多個(gè)應(yīng)用共享時(shí)連接池 maxActive 就不能都設(shè)成 50。maxWait3000表示拿連接等待 3 秒超過就拋異常配合日志能快速定位是連接用完還是 SQL 卡住。5.3 JSP 首次訪問慢預(yù)編譯和緩存頭JSP 第一次被訪問時(shí)才編譯成 classTomcat 低配置服務(wù)器上首次請(qǐng)求可能耗時(shí)幾百毫秒。如果不想等運(yùn)行時(shí)編譯可以在部署前用 Jasper 預(yù)編譯java -cp $CATALINA_HOME/lib/*:target/classes org.apache.jasper.JspC \ -uriroot src/main/webapp \ -webxml src/main/webapp/WEB-INF/generated_web.xml \ -d target/jspc預(yù)編譯完成后把生成的 class 文件放到WEB-INF/classes再啟動(dòng) TomcatJSP 首請(qǐng)求時(shí)間會(huì)明顯下降。另一個(gè)容易忽略的優(yōu)化是給靜態(tài)資源加緩存頭HTML5 的 CSS、JS 文件可以在 Filter 里統(tǒng)一設(shè)置response.setHeader(Cache-Control, public, max-age86400);普通商品詳情頁不要設(shè)置長(zhǎng)緩存因?yàn)閮r(jià)格和庫存會(huì)變圖片和 CSS 設(shè)置為 86400 秒再次訪問時(shí)瀏覽器會(huì)直接命中本地緩存Redis 都不用引入。做完這步后打開 Chrome DevTools 的 Network 面板對(duì)比商品列表頁和靜態(tài)資源請(qǐng)求的時(shí)間差能直接看到優(yōu)化前后的差異。本文還有配套的精品資源點(diǎn)擊獲取