(三)Java高并发秒杀系统API之Web层开发
Restful接口设计
SpringMVC理论
既然是Web层的会肯定要先引入SpringMVC了
- 修改
web.xml
,引入SpringMvc
的DispatcherServlet
:
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<!--用maven创建的web-app需要修改servlet的版本为3.0-->
<!--配置DispatcherServlet-->
<servlet>
<servlet-name>seckill-dispatchServlet</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>
<load-on-startup>
1
</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatchServlet</servlet-name>
<!--默认匹配所有请求-->
<!--直接拦截所有请求,不像strusts1.0的/*或者*.do方式-->
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
在这里的话如果你不配置这一段代码的:
<!--配置springmvc的配置文件-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
SpringMvc默认就会默认去WEB-INF
下查找默认规范的配置文件,像我这里配置的servlet-name
是seckill-dispatchServlet
的话,则默认会寻找WEB-INF
一个名为seckill-dispatchServlet-Servlet.xml
的配置文件
编写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: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
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置springmvc-->
<!--1)开启springmvc注解模式-->
<!--简化配置:
1.自动注册DefaultAnnotationHandlerMapping(采用注解方式进行url到handler映射),
AnnotationMethodHandlerAdapter(基于注解方法的handler适配器)
2.提供一些了:数据绑定,数字日期的format,xml,json默认读写支持-->
<mvc:annotation-driven/>
<!--servlet-mapping 映射路径:"/"-->
<!--2)静态资源默认servlet配置
1.加入对静态资源的处理:js,png
2.运行试验"/"做整体映射-->
<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"/>
</bean>
<!--4)开启注解扫描web相关的bean-->
<context:component-scan base-package="org.seckill.controller"/>
</beans>
接下来编写Controller SeckillController
首先在org.seckill
下建立包为Controller
的包,然后在里面新建一个类SeckillController
:
@Controller
@RequestMapping("/seckill")//url:/模块/资源/{id}/细分 /seckill/list
public class SeckillController {
private Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SeckillService seckillService;
/**
* 进入秒杀列表.
*
* @param model 模型数据,里面放置有秒杀商品的信息
* @return 秒杀列表详情页面
*/
@RequestMapping(value = {"/list", "", "index"}, method = RequestMethod.GET)
public String list(Model model) {
//list.jsp是页面模板 + model是数据 = ModelAndView
List<Seckill> seckillList = seckillService.getSeckillList();
model.addAttribute("list", seckillList);
return "list";
}
@RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)
public String detail(@PathVariable("seckillId") Long seckillId, Model model) {
if (seckillId == null) {
return "redirect:/seckill/list";
}
Seckill seckill = seckillService.getById(seckillId);
if (seckill == null) {
return "forward:/seckill/list";
}
model.addAttribute("seckill", seckill);
return "detail";
}
/**
* 暴露秒杀接口的方法.
*
* @param seckillId 秒杀商品的id
* @return 根据用户秒杀的商品id进行业务逻辑判断, 返回不同的json实体结果
*/
@RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
@ResponseBody//返回json
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId) {
// 查询秒杀商品的结果
SeckillResult<Exposer> result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult<>(true, exposer);
} catch (Exception e) {
e.printStackTrace();
result = new SeckillResult<>(false, e.getMessage());
}
return result;
}
/**
* 用户执行秒杀,在页面点击相应的秒杀连接,进入后获取对应的参数进行判断,返回相对应的json实体结果,前端再进行处理.
*
* @param seckillId 秒杀的商品,对应的时秒杀的id
* @param md5 一个被混淆的md5加密值
* @param userPhone 参与秒杀用户的额手机号码,当做账号密码使用
* @return 参与秒杀的结果, 为json数据
*/
@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "userPhone", required = false) Long userPhone) {
//required = false使得userPhone不是必须的,缺少时不会报错
// 如果用户的手机号码为空的说明没有填写手机号码进行秒杀
if (userPhone == null) {
return new SeckillResult<SeckillExecution>(false, "没有注册");
}
// 根据用户的手机号码,秒杀商品的id跟md5进行秒杀商品,没异常就是秒杀成功
try {
// 这里换成储存过程
SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
} catch (RepeatKillException e1) {
// 重复秒杀
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(false, execution);
} catch (SeckillCloseException e2) {
// 秒杀关闭
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(false, execution);
} catch (SeckillException e) {
// 不能判断的异常
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(false, execution);
}
// 如果有异常就是秒杀失败
}
/**
* 获取服务器端时间,防止用户篡改客户端时间提前参与秒杀
*
* @return 时间的json数据
*/
@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult<LocalDateTime> time() {
Date now = new Date();
return new SeckillResult(true, now.getTime());
}
}
建立一个全局ajax请求返回类,返回json类型
SeckillResult
:
/**
* 封装所有的ajax请求返回类型,方便返回json
*/
public class SeckillResult<T> {
private boolean success;
private T data;
private String error;
public SeckillResult() {
}
public SeckillResult(boolean success, T data) {
this.success = success;
this.data = data;
}
public SeckillResult(boolean success, String error) {
this.success = success;
this.error = error;
}
public boolean isSuccess() {
return success;
}
public void setSuccess(boolean success) {
this.success = success;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
@Override
public String toString() {
return "SeckillResult{" +
"状态=" + success +
", 数据=" + data +
", 错误消息='" + error + '\'' +
'}';
}
}
页面的编写
因为项目的前端页面都是由Bootstrap
开发的,所以我们要先去下载Bootstrap
或者是使用在线的CDN.
-Bootstrap中文官网
-Bootstrap中文文档
使用在线CDN引入的方法:
<!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
<!-- 可选的 Bootstrap 主题文件(一般不用引入) -->
<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap-theme.min.css" integrity="sha384-rHyoN1iRsVXV4nD0JutlnGaslCJuC7uwjduW9SVrLvRYooPp2bWYgmgJQIXwl/Sp" crossorigin="anonymous">
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha384-Tc5IQib027qvyjSMfHjOMaLkfuWVxZxUPnCJA7l2mCWNIpG9mGCD8wGNIcPD7Txa" crossorigin="anonymous"></script>
文档里面写的很详细,然后我这里是使用离线版本的,方便我们本地调试,避免出现什么别的因素干扰我们:
- 首先下载
JQuery
,因为Bootstrap
就是依赖JQuery
的 - 然后下载
Bootstrap
- 然后下载一个倒计时插件
jquery.countdown.min.js
- 再下载一个操作
Cookie
插件jquery.cookie.min.js
- 再下载一个操作
- 首先编写一个公共的头部
jsp
文件,位于WEB-INF
下common
中的head.jsp
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/css/bootstrap.min.css" type="text/css">
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/css/bootstrap-theme.min.css" type="text/css">
- 然后编写一个公共的
jstl
标签库文件,位于WEB-INF
下common
中的tag.jsp
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>
- 编写列表页面,位于
WEB-INF
下common
中的list.jsp
<%@page contentType="text/html; charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp" %>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<title>秒杀列表</title>
<%@include file="common/head.jsp" %>
</head>
<body>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading text-center">
<h2>秒杀列表</h2>
</div>
<div class="panel-body">
<table class="table table-hover">
<thead>
<tr>
<td>名称</td>
<td>库存</td>
<td>开始时间</td>
<td>结束时间</td>
<td>创建时间</td>
<td>详情页</td>
</tr>
</thead>
<tbody>
<c:forEach items="${list}" var="sk">
<tr>
<td>${sk.name}</td>
<td>${sk.number}</td>
<td><fmt:formatDate value="${sk.startTime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td><fmt:formatDate value="${sk.endTime}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td><fmt:formatDate value="${sk.createTIme}" pattern="yyyy-MM-dd HH:mm:ss"/></td>
<td><a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">详情</a></td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</body>
<script src="${pageContext.request.contextPath}/resources/plugins/jquery.js"></script>
<script src="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
</html>
- 编写列表页面,位于
WEB-INF
下common
中的detail.jsp
,秒杀详情页面
<%--
Created by IntelliJ IDEA.
User: jianrongsun
Date: 17-5-25
Time: 下午5:03
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@include file="common/tag.jsp" %>
<html>
<head>
<title>秒杀商品详情页面</title>
<%@include file="common/head.jsp" %>
</head>
<body>
<div class="container">
<div class="panel panel-default">
<div class="panel-heading">
<h1>${seckill.name}</h1>
</div>
<div class="panel-body">
<h2 class="text-danger">
<span class="glyphicon glyphicon-time"></span>
<span class="glyphicon" id="seckill-box"></span>
</h2>
</div>
</div>
</div>
<div id="killPhoneModal" class="modal fade">
<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>
<div class="modal-body">
<div class="row">
<div class="col-xs-8 col-xs-offset-2">
<input type="text" name="killPhone" id="killPhoneKey" placeholder="填写手机号码" 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>
提交
</button>
</div>
</div>
</div>
</body>
<script src="${pageContext.request.contextPath}/resources/plugins/jquery.js"></script>
<script src="${pageContext.request.contextPath}/resources/plugins/bootstrap-3.3.0/js/bootstrap.min.js"></script>
<script src="${pageContext.request.contextPath}/resources/plugins/jquery.cookie.min.js"></script>
<script src="${pageContext.request.contextPath}/resources/plugins/jquery.countdown.min.js"></script>
<script src="${pageContext.request.contextPath}/resources/script/seckill.js"></script>
<script type="text/javascript">
$(function () {
var startTimeVal = "${seckill.startTime.toLocalDate()} " + seckill.cloneZero("${seckill.startTime.toLocalTime()}");
var endTimeVal = "${seckill.endTime.toLocalDate()} " + seckill.cloneZero("${seckill.endTime.toLocalTime()}");
console.log("startTimeVal========" + startTimeVal);
console.log("endTimeVal========" + endTimeVal);
// 传入参数
seckill.detail.init({
seckillId:${seckill.seckillId},
startTime: startTimeVal,
endTime: endTimeVal
})
})
</script>
</html>
- 建立一个模块化的
seckill.js
文件,位于Webapp
下resources
下script
文件夹下,文件内容如下:
/**
* 模块化javaScript
*/
var seckill = {
// 封装秒杀相关的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";
}
},
// 验证手机号码
validatePhone: function (phone) {
if (phone && phone.length === 11 && !isNaN(phone)) {
return true;
} else {
return false;
}
},
// 详情页秒杀业务逻辑
detail: {
// 详情页开始初始化
init: function (params) {
console.log("获取手机号码");
// 手机号验证登录,计时交互,在cookie中查找手机号
var userPhone = $.cookie('userPhone');
// 验证手机号
if (!seckill.validatePhone(userPhone)) {
console.log("未填写手机号码");
// 验证手机控制输出
var killPhoneModal = $("#killPhoneModal");
// 显示了弹出层
killPhoneModal.modal({
show: true, // 显示弹出层
backdrop: 'static', // 禁止位置关闭
keyboard: false // 关闭键盘事件
});
$("#killPhoneBtn").click(function () {
console.log("提交手机号码按钮被点击");
var inputPhone = $("#killPhoneKey").val();
console.log("inputPhone" + inputPhone);
if (seckill.validatePhone(inputPhone)) {
// 把电话写入cookie
$.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'});
// 验证通过 刷新页面
window.location.reload();
} else {
// todo 错误文案信息写到前端
$("#killPhoneMessage").hide().html("<label class='label label-danger'>手机号码错误</label>").show(300);
}
});
// 已经登录
} else {
console.log("在cookie中找到了电话号码,开启计时");
// 已经登录了就开始计时交互
var startTime = params['startTime'];
var endTime = params['endTime'];
var seckillId = params['seckillId'];
console.log("开始秒杀时间=======" + startTime);
console.log("结束秒杀时间========" + endTime);
$.get(seckill.URL.now(), {}, function (result) {
if (result && result['success']) {
var nowTime = result['data'];
console.log("服务器当前的时间==========" + nowTime);
// 进行秒杀商品的时间判断,然后计时交互
seckill.countDown(seckillId, nowTime, startTime, endTime);
} else {
console.log('结果:' + result);
console.log('result' + result);
}
});
}
}
},
handlerSeckill: function (seckillId, node) {
// 获取秒杀地址
node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">开始秒杀</button>');
console.debug("开始进行秒杀地址获取");
$.post(seckill.URL.exposer(seckillId), {}, function (result) {
//在回调函数中,执行交互流程
if (result && result['success']) {
var exposer = result['data'];
if (exposer['exposed']) {
console.log("有秒杀地址接口");
// 开启秒杀,获取秒杀地址
var md5 = exposer['md5'];
var killUrl = seckill.URL.execution(seckillId, md5);
console.log("秒杀的地址为:" + killUrl);
// 绑定一次点击事件,减轻服务器压力
$("#killBtn").one('click', function () {
console.log("开始进行秒杀,按钮被禁用");
// 执行秒杀请求
// 1.先禁用按钮
$(this).addClass("disabled");
// 2.发送秒杀请求
$.post(killUrl, {}, function (result) {
if (result && result['success']) {
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
console.log("秒杀状态" + stateInfo);
// 3.显示秒杀结果
node.html('<span class="label label-success">' + stateInfo + '</span>');
}
});
});
node.show();
} else {
console.warn("还没有暴露秒杀地址接口,无法进行秒杀");
// 未开启秒杀
var now = exposer['now'];
var start = exposer['start'];
var end = exposer['end'];
console.log("当前时间" + now);
console.log("开始时间" + start);
console.log("结束时间" + end);
console.log("开始倒计时");
console.debug("开始进行倒计时");
//重新计算计时逻辑
seckill.countDown(seckillId, now, start, end);
}
} else {
console.error("服务器端查询秒杀商品详情失败");
console.log('result' + result.valueOf());
}
});
},
countDown: function (seckillId, nowTime, startTime, endTime) {
console.log("秒杀的商品ID:" + seckillId + ",服务器当前时间:" + nowTime + ",开始秒杀的时间:" + startTime + ",结束秒杀的时间" + endTime);
// 获取显示倒计时的文本域
var seckillBox = $("#seckill-box");
// 获取时间戳进行时间的比较
if (nowTime < endTime && nowTime > startTime) {
// 秒杀开始
console.log("秒杀可以开始,两个条件符合");
seckill.handlerSeckill(seckillId, seckillBox);
} else if (nowTime > endTime) {
console.log(nowTime + ">" + endTime);
// 秒杀结束
console.warn("秒杀已经结束了,当前时间为:" + nowTime + ",秒杀结束时间为" + endTime);
seckillBox.html("秒杀结束");
} else {
console.log("秒杀还没开始");
// 秒杀未开启
var killTime = new Date(startTime + 1000);//加上1秒防止用户时间偏移
console.log(killTime);
console.log("开始计时效果");
seckillBox.countdown(killTime, function (event) {
// 事件格式
var format = event.strftime("秒杀倒计时: %D天 %H时 %M分 %S秒");
console.log(format);
seckillBox.html(format);
}).on('finish.countdown', function () {
// 时间完成后回调事件,获取秒杀地址,控制业务逻辑
console.log("准备执行回调,获取秒杀地址,执行秒杀");
console.log("倒计时结束");
seckill.handlerSeckill(seckillId, seckillBox);
});
}
}
};
自定义jstl标签参考资料
stackoverflow上的资料1
stackoverflow上的资料2
编写完了就部署运行吧,不出意外的话就是这个样子的: