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); } }) },
秒杀成功后再次进行秒杀则不成功:
输出:
在库存不够时也返回秒杀结束:
至此,功能方面已经实现了,后面还剩下优化部分。
总结
本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注的更多内容!
上一篇: 浅析vue.js数组的变异方法
下一篇: vue移动端轻量级的轮播组件实现代码
推荐阅读
-
详解使用Docker搭建Java Web运行环境
-
JAVA构建高并发商城秒杀系统——架构分析
-
JSP学习之Java Web中的安全控制实例详解
-
Docker学习笔记之Docker部署Java web系统
-
记账系统java web
-
荐 Java秒杀系统方案优化 高性能高并发实战,学习手记(三)
-
Java Web层框架比较—— 比较JSF、Spring MVC、Stripes、Struts 2、Tapestry和Wicket
-
在新linux系统上部署Java web 项目服务器
-
关于企业java web系统技术选型的问题 企业应用javaejb3
-
关于企业java web系统技术选型的问题 企业应用javaejb3