Java秒殺系統:web層詳解
設計Restful接口
根據需求設計前端交互流程。
三個職位:
- 產品:解讀用戶需求,搞出需求文檔
- 前端:不同平臺的頁面展示
- 後端:存儲、展示、處理數據
前端頁面流程:
詳情頁流程邏輯:
標準系統時間從服務器獲取。
Restful:一種優雅的URI表述方式、資源的狀態和狀態轉移。
Restful規范:
- GET 查詢操作
- POST 添加/修改操作(非冪等)
- PUT 修改操作(冪等,沒有太嚴格區分)
- DELETE 刪除操作
URL設計:
/模塊/資源/{標示}/集合/... /user/{uid}/friends -> 好友列表 /user/{uid}/followers -> 關註者列表
秒殺API的URL設計
GET /seckill/list 秒殺列表 GET /seckill/{id}/detail 詳情頁 GET /seckill/time/now 系統時間 POST /seckill/{id}/exposer 暴露秒殺 POST /seckill/{id}/{md5}/execution 執行秒殺
下一步就是如何實現這些URL接口。
SpringMVC
理論
適配器模式(Adapter Pattern),把一個類的接口變換成客戶端所期待的另一種接口, Adapter模式使原本因接口不匹配(或者不兼容)而無法在一起工作的兩個類能夠在一起工作。
SpringMVC的handler
(Controller
,HttpRequestHandler
,Servlet
等)有多種實現方式,例如繼承Controller的,基於註解控制器方式的,HttpRequestHandler方式的。由於實現方式不一樣,調用方式就不確定瞭。
看HandlerAdapter接口有三個方法:
// 判斷該適配器是否支持這個HandlerMethod boolean supports(Object handler); // 用來執行控制器處理函數,獲取ModelAndView 。就是根據該適配器調用規則執行handler方法。 ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception; long getLastModified(HttpServletRequest request, Object handler);
問流程如上圖,用戶訪問一個請求,首先經過DispatcherServlet
轉發。利用HandlerMapping
得到想要的HandlerExecutionChain
(裡面包含handler和一堆攔截器)。然後利用handler,得到HandlerAdapter
,遍歷所有註入的HandlerAdapter,依次使用supports方法尋找適合這個handler的適配器子類。最後通過這個獲取的適配器子類運用handle方法調用控制器函數,返回ModelAndView。
註解映射技巧
- 支持標準的URL
- ?和*和**等字符,如/usr/*/creation會匹配/usr/AAA/creation和/usr/BBB/creation等。/usr/**/creation會匹配/usr/creation和/usr/AAA/BBB/creation等URL。帶{xxx}占位符的URL。
- 如/usr/{userid}匹配/usr/123、/usr/abc等URL.
請求方法細節處理
- 請求參數綁定
- 請求方式限制
- 請求轉發和重定向
- 數據模型賦值
- 返回json數據
- cookie訪問
返回json數據
cookie訪問:
項目整合SpringMVC
web.xml下配置springmvc需要加載的配置文件:
<!--?xml version="1.0" encoding="UTF-8"?--> <web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemalocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd" version="3.1" metadata-complete="true"> <!--修改servlet版本為3.1--> <!--配置DispatcherServlet--> <servlet> <servlet-name>seckill-dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--配置springmvc需要加載的配置文件 spring-dao.xml spring-service.xml spring-web.xml--> <!--整合:mybatis -> spring -> springmvc--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/spring-*.xml</param-value> </init-param> </servlet> <servlet-mapping> <servlet-name>seckill-dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
在resources文件夾下的spring文件夾添加spring-web.xml文件:
<!--?xml version="1.0" encoding="UTF-8"?--> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemalocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd"> <!--配置springmvc--> <!--1. 開啟springmvc註解模式--> <!-- 簡化配置, 自動註冊handlermapping,handleradapter 默認提供瞭一系列功能:數據綁定,數字和日期的format,xml和json的讀寫支持 --> <mvc:annotation-driven> <!--servlet-mapping 映射路徑:"/"--> <!--2. 靜態資源默認servlet配置 靜態資源處理:js,gif,png.. 允許使用/做整體映射 --> <mvc:default-servlet-handler> <!--3. jsp的顯示viewResolver--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"> <property name="prefix" value="/WEB-INF/jsp"> <property name="suffix" value=".jsp"> </property></property></property></bean> <!--4. 掃描web相關的bean--> <context:component-scan base-package="cn.orzlinux.web"> </context:component-scan></mvc:default-servlet-handler></mvc:annotation-driven></beans>
使用SpringMVC實現Restful接口
新建文件:
首先是SeckillResult.java,這個保存controller的返回結果,做一個封裝。
// 所有ajax請求返回類型,封裝json結果 public class SeckillResult<t> { private boolean success; //是否執行成功 private T data; // 攜帶數據 private String error; // 錯誤信息 // getter setter contructor }
在Seckillcontroller.java中,實現瞭我們之前定義的幾個URL:
GET /seckill/list 秒殺列表 GET /seckill/{id}/detail 詳情頁 GET /seckill/time/now 系統時間 POST /seckill/{id}/exposer 暴露秒殺 POST /seckill/{id}/{md5}/execution 執行秒殺
具體代碼如下:
@Controller // @Service @Component放入spring容器 @RequestMapping("/seckill") // url:模塊/資源/{id}/細分 public class SeckillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SecKillService secKillService; @RequestMapping(value = "/list",method = RequestMethod.GET) public String list(Model model) { // list.jsp + model = modelandview List<seckill> list = secKillService.getSecKillList(); model.addAttribute("list",list); return "list"; } @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET) public String detail(@PathVariable("seckillId") Long seckillId, Model model) { if (seckillId == null) { // 0. 不存在就重定向到list // 1. 重定向訪問服務器兩次 // 2. 重定向可以重定義到任意資源路徑。 // 3. 重定向會產生一個新的request,不能共享request域信息與請求參數 return "redrict:/seckill/list"; } SecKill secKill = secKillService.getById(seckillId); if (secKill == null) { // 0. 為瞭展示效果用forward // 1. 轉發隻訪問服務器一次。 // 2. 轉發隻能轉發到自己的web應用內 // 3. 轉發相當於服務器跳轉,相當於方法調用,在執行當前文件的過程中轉向執行目標文件, // 兩個文件(當前文件和目標文件)屬於同一次請求,前後頁 共用一個request,可以通 // 過此來傳遞一些數據或者session信息 return "forward:/seckill/list"; } model.addAttribute("seckill",secKill); return "detail"; } // ajax json @RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF8"}) @ResponseBody public SeckillResult<exposer> exposer(Long seckillId) { SeckillResult<exposer> result; try { Exposer exposer = secKillService.exportSecKillUrl(seckillId); result = new SeckillResult<exposer>(true,exposer); } catch (Exception e) { logger.error(e.getMessage(),e); result = new SeckillResult<>(false,e.getMessage()); } return result; } @RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF8"}) public SeckillResult<seckillexecution> execute( @PathVariable("seckillId") Long seckillId, // required = false表示cookie邏輯由我們程序處理,springmvc不要報錯 @CookieValue(value = "killPhone",required = false) Long userPhone, @PathVariable("md5") String md5) { if (userPhone == null) { return new SeckillResult<seckillexecution>(false, "未註冊"); } SeckillResult<seckillexecution> result; try { SeckillExecution execution = secKillService.executeSeckill(seckillId, userPhone, md5); result = new SeckillResult<seckillexecution>(true, execution); return result; } catch (SeckillCloseException e) { // 秒殺關閉 SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.END); return new SeckillResult<seckillexecution>(false,execution); } catch (RepeatKillException e) { // 重復秒殺 SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.REPEAT_KILL); return new SeckillResult<seckillexecution>(false,execution); } catch (Exception e) { // 不是重復秒殺或秒殺結束,就返回內部錯誤 logger.error(e.getMessage(), e); SeckillExecution execution = new SeckillExecution(seckillId, SecKillStatEnum.INNER_ERROR); return new SeckillResult<seckillexecution>(false,execution); } } @RequestMapping(value = "/time/now",method = RequestMethod.GET) @ResponseBody public SeckillResult<long> time() { Date now = new Date(); return new SeckillResult<long>(true,now.getTime()); } }
頁面
這裡修改數據庫為合適的時間來測試我們的代碼。
點擊後跳轉到詳情頁。
詳情頁涉及到比較多的交互邏輯,如cookie,秒殺成功失敗等等。放到邏輯交互一節來說。
運行時發現jackson版本出現問題,pom.xml修改為:
<dependency> <groupid>com.fasterxml.jackson.core</groupid> <artifactid>jackson-databind</artifactid> <version>2.10.2</version> </dependency>
list.jsp代碼為:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <%--引入jstl--%> <%--標簽通用頭,寫在一個具體文件,直接靜態包含--%> <%@include file="common/tag.jsp"%> <title>Bootstrap 模板</title> <%--靜態包含:會合並過來放到這,和當前文件一起作為整個輸出--%> <%@include file="common/head.jsp"%> <%--頁面顯示部分--%> <div class="container"> <div class="panel panel-default"> <div class="panel panel-heading text-center"> <h1>秒殺列表</h1> </div> <div class="panel-body"> <c:foreach var="sk" items="${list}"> </c:foreach><table class="table table-hover"> <thead> <tr> <th>名稱</th> <th>庫存</th> <th>開始時間</th> <th>結束時間</th> <th>創建時間</th> <th>詳情頁</th> </tr> </thead> <tbody> <tr> <td>${sk.name}</td> <td>${sk.number}</td> <td> <fmt:formatdate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"> </fmt:formatdate></td> <td> <fmt:formatdate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"> </fmt:formatdate></td> <td> <fmt:formatdate value="${sk.createTime}" pattern="yyyy-MM-dd HH:mm:ss"> </fmt:formatdate></td> <td> <a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank"> link </a> </td> </tr> </tbody> </table> </div> </div> </div> <!-- jQuery (Bootstrap 的 JavaScript 插件需要引入 jQuery) --> <script src="https://code.jquery.com/jquery.js"></script> <!-- 包括所有已編譯的插件 --> <script src="js/bootstrap.min.js"></script>
邏輯交互
身份認證
cookie中沒有手機號要彈窗,手機號不正確(11位數字)要提示錯誤:
選擇提交之後要能夠在cookie中看到:
目前為止detail.jsp:
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <title>秒殺詳情頁</title> <%--靜態包含:會合並過來放到這,和當前文件一起作為整個輸出--%> <%@include file="common/head.jsp"%> <link href="https://cdn.bootcdn.net/ajax/libs/jquery-countdown/2.1.0/css/jquery.countdown.css" rel="stylesheet"> <%--<input type="hidden" id="basePath" value="${basePath}">--%> <div class="container"> <div class="panel panel-default text-center"> <h1> <div class="panel-heading">${seckill.name}</div> </h1> </div> <div class="panel-body"> <h2 class="text-danger"> <!-- 顯示time圖標 --> <span class="glyphicon glyphicon-time"></span> <!-- 展示倒計時 --> <span class="glyphicon" id="seckillBox"></span> </h2> </div> </div> <!-- 登錄彈出層,輸入電話 bootstrap裡面的--> <div id="killPhoneModal" class="modal fade bs-example-modal-lg"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <h3 class="modal-title text-center"> <span class="glyphicon glyphicon-phone"></span>秒殺電話: </h3> </div> <div class="modal-body"> <div class="row"> <div class="col-xs-8 col-xs-offset-2"> <input type="text" name="killphone" id="killphoneKey" placeholder="填手機號^O^" class="form-control"> </div> </div> </div> <div class="modal-footer"> <span id="killphoneMessage" class="glyphicon"></span> <button type="button" id="killPhoneBtn" class="btn btn-success"> <span class="glyphicon glyphicon-phone"></span> Submit </button> </div> </div> </div> </div> <!-- jQuery文件。務必在bootstrap.min.js 之前引入 --> <script src="//cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script> <!-- 最新的 Bootstrap 核心 JavaScript 文件 --> <script src="//cdn.bootcss.com/bootstrap/3.3.5/js/bootstrap.min.js"></script> <!-- jQuery cookie操作插件 --> <script src="//cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script> <!-- jQery countDonw倒計時插件 --> <script src="//cdn.bootcss.com/jquery.countdown/2.1.0/jquery.countdown.min.js"></script> <%--開始寫交互邏輯--%> <script src="/resources/script/seckill.js" type="text/javascript"></script> <script type="text/javascript"> $(function () { seckill.detail.init({ seckillId: ${seckill.seckillId}, startTime: ${seckill.startTime.time}, // 轉化為毫秒,方便比較 endTime: ${seckill.endTime.time}, }); }); </script>
我們的邏輯主要寫在另外的js文件中:
seckill.js
// 存放主要交互邏輯js // javascript 模塊化 var seckill={ // 封裝秒殺相關ajax的URL URL:{ }, // 驗證手機號 validatePhone: function (phone) { if(phone && phone.length==11 && !isNaN(phone)) { return true; } else { return false; } }, // 詳情頁秒殺邏輯 detail: { // 詳情頁初始化 init: function (params) { // 手機驗證和登錄,計時交互 // 規劃交互流程 // 在cookie中查找手機號 var killPhone = $.cookie('killPhone'); var startTime = params['startTime']; var endTime = params['endTime']; var seckillId = params['seckillId']; // 驗證手機號 if(!seckill.validatePhone(killPhone)) { // 綁定手機號,獲取彈窗輸入手機號的div id var killPhoneModal = $('#killPhoneModal'); killPhoneModal.modal({ show: true, //顯示彈出層 backdrop: 'static',//禁止位置關閉 keyboard: false, //關閉鍵盤事件 }); $('#killPhoneBtn').click(function () { var inputPhone = $('#killphoneKey').val(); // 輸入格式什麼的ok瞭就刷新頁面 if(seckill.validatePhone(inputPhone)) { // 將電話寫入cookie $.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'}); window.location.reload(); } else { // 更好的方式是把字符串寫入字典再用 $('#killphoneMessage').hide().html('<label class="label label-danger">手機號格式錯誤</label>').show(500); } }); } // 已經登錄 } } }
計時面板
在登錄完成後,處理計時操作:
// 已經登錄 // 計時交互 $.get(seckill.URL.now(),{},function (result) { if(result && result['success']) { var nowTime = result['data']; // 寫到函數裡處理 seckill.countdown(seckillId,nowTime,startTime,endTime); } else { console.log('result: '+result); } });
在countdown函數裡,有三個判斷,未開始、已經開始、結束。
URL:{ now: function () { return '/seckill/time/now'; } }, handleSeckill: function () { // 處理秒殺邏輯 }, countdown: function (seckillId,nowTime,startTime,endTime) { var seckillBox = $('#seckillBox'); if(nowTime>endTime) { seckillBox.html('秒殺結束!'); } else if(nowTime<starttime) {="" 秒殺未開始,計時="" var="" killtime="new" date(starttime="" +="" 1000);="" seckillbox.countdown(killtime,function="" (event)="" 控制時間格式="" format="event.strftime('秒殺開始倒計時:%D天" %h時="" %m分="" %s秒');="" seckillbox.html(format);="" 時間完成後回調事件="" }).on('finish.countdown',="" function="" ()="" 獲取秒殺地址,控制顯示邏輯,執行秒殺="" seckill.handleseckill();="" })="" }="" else="" 秒殺開始="" },="" ```="" 總體就是一個顯示操作,用瞭jquery的countdown倒計時插件。="" <img="" src="https://gitee.com/hqinglau/img/raw/master/img/20211006194407.png" alt="image-20211006194407145" style="zoom:67%;"> ### 秒殺交互 秒殺之前: ![image-20211006202253376](https://img-blog.csdnimg.cn/img_convert/7609c513cb3b64f4e710d879e57c1651.png) 詳情頁: <img src="https://gitee.com/hqinglau/img/raw/master/img/20211006201149.png" alt="image-20211006201149488" style="zoom:80%;"> 點擊開始秒殺: <img src="https://gitee.com/hqinglau/img/raw/master/img/20211006202320.png" alt="image-20211006202320137" style="zoom:80%;"> 列表頁刷新: ![image-20211006202306300](https://img-blog.csdnimg.cn/img_convert/272dac0d7f6d4a2910614551f4580aac.png) 運行時發現controller忘瞭寫`@ResponseBody`瞭,這裡返回的不是jsp是json,需要加上。 ```java @ResponseBody public SeckillResult<seckillexecution> execute( @PathVariable("seckillId") Long seckillId, // required = false表示cookie邏輯由我們程序處理,springmvc不要報錯 @CookieValue(value = "killPhone",required = false) Long userPhone, @PathVariable("md5") String md5)
在seckill.js中,補全秒殺邏輯:
// 封裝秒殺相關ajax的URL URL:{ now: function () { return '/seckill/time/now'; }, exposer: function(seckillId) { return '/seckill/'+seckillId+'/exposer'; }, execution: function (seckillId,md5) { return '/seckill/'+seckillId+'/'+md5+'/execution'; } }, // id和顯示計時的那個模塊 handleSeckill: function (seckillId,node) { // 處理秒殺邏輯 // 在計時的地方顯示一個秒殺按鈕 node.hide() .html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>'); // 獲取秒殺地址 $.post(seckill.URL.exposer(),{seckillId},function (result) { if(result && result['success']) { var exposer = result['data']; if(exposer['exposed']) { // 如果開啟瞭秒殺 // 獲取秒殺地址 var md5 = exposer['md5']; var killUrl = seckill.URL.execution(seckillId,md5); console.log("killurl: "+killUrl); // click永遠綁定,one隻綁定一次 $('#killBtn').one('click',function () { // 執行秒殺請求操作 // 先禁用按鈕 $(this).addClass('disabled'); // 發送秒殺請求 $.post(killUrl,{},function (result) { if(result) { var killResult = result['data']; var state = killResult['state']; var stateInfo = killResult['stateInfo']; // 顯示秒殺結果 if(result['success']) { node.html('<span class="label label-success">'+stateInfo+'</span>'); } else { node.html('<span class="label label-danger">'+stateInfo+'</span>'); } } console.log(result); }) }); node.show(); } else { // 未開始秒殺,這裡是因為本機顯示時間和服務器時間不一致 // 可能瀏覽器認為開始瞭,服務器其實還沒開始 var now = exposer['now']; var start = exposer['start']; var end = exposer['end']; // 重新進入倒計時邏輯 seckill.countdown(seckillId,now,start,end); } } else { console.log('result='+result); } }) },
秒殺成功後再次進行秒殺則不成功:
輸出:
在庫存不夠時也返回秒殺結束:
至此,功能方面已經實現瞭,後面還剩下優化部分。
總結
本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!
推薦閱讀:
- 使用Jquery操作Cookies
- jQuery treeview樹形結構應用
- 關於springmvc報錯404的問題
- 基於SpringBoot解決CORS跨域的問題(@CrossOrigin)
- SpringMVC RESTFul實戰案例訪問首頁