基于SSM实现高并发秒杀Web项目(三)
这一部分主要介绍秒杀业务Web层的设计和实现,使用SpringMVC整合spring,实现秒杀restful接⼝。
源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利
一、设计Restful接口
1)前端交互流程设计
①前端页面流程如下: ②详情页流程逻辑如下:2)Restful接口设计
①Restful接口是一种优雅的url表述方式,它表示着一种资源的状态或者状态的转移。
②几个Restful规范:
- GET -> 查询操作
- POST -> 添加/修改操作
- PUT -> 修改操作
- DELETE -> 删除操作
③简单的URL设计规范:
/模块/资源/{标示}/集合1/...
例如:
/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 -> 执行秒杀
二、SpringMVC整合Spring
1)SpringMVC的运行流程
我们使用的SpringMVC始终都是围绕的Handler进行开发 SpringMVC的运行流程如下:具体过程为:
①用户发出请求,所有的请求都会映射到DispatcherServlet中,这相当于一个中央控制器,它会拦截用户的所有请求。
②DispatcherServlet拦截到请求后,首先会用到DefaultAnnotationHandlerMapping,它的主要作用是用来映射URL。
③映射完成后会用到默认的DefaultAnnotationHandlerAdapter,用来作Handler的适配,如果其中用到了拦截器的话,也会将拦截器绑定到流程当中。
④然后会衔接到我们要开发的Controller部分。
⑤上一步的结果产出到了ModelAndView中,用一个字符串表示,相当于一个jsp页面,同时会交付到Servlet中,即中央控制器DispatcherServlet。
⑥DispatcherServlet会发现你应用的是一个InternalResourceViewResolver,这其实就是一个jsp的view。
⑦接下来DispatcherServlet就会把Model和jsp相结合。
⑧最终将结果返回给用户。
2)HTTP请求地址映射原理
发送的HTTP请求首先会到Servlet容器当中,然后通过SpringMVCHandlerMapping来映射URL,分为注解、xml配置、编程等方式,然后会对应到后端我们自己实现的Handler处理方法。
3)整合配置SpringMVC框架
①配置WEB-INF下的web.xml文件,配置springMVC所需要的配置文件spring-*.xml到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.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
Mybites -> 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>
②配置spring-web.xml。resources/spring目录下新建spring-web.xml文件。
SSM框架整合流程为:DAO层(Mybatis)、Service层、Web层 -> Spring ->Spring MVC。Web层、Serivce层的bean、Mybatis中的接口注入(整合)到Spring IOC容器中(通过XML文件注入),再通过Spring MVC的web.xml文件加载Spring的xml文件,把所有bean都注入到Spring IOC容器中,Spring MVC框架中的Controller类等就能使用了。
<?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">
<!--配置spring mvc-->
<!--1,开启springmvc注解模式
a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
c:xml,json的默认读写支持-->
<mvc:annotation-driven/>
<!--2.静态资源默认servlet配置-->
<!--
1).加入对静态资源处理:js,gif,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相关的controller-->
<context:component-scan base-package="web"/>
</beans>
三、实现秒杀相关的Restful接口
①新建Controller包,下面新建SeckillController类
@Controller
@RequestMapping("/seckill")
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<Seckill> list = secKillService.getSeckillList();
model.addAttribute("list", list);
return "list"; //由于spring-web.xml的viewResolver的配置,这里的"list"会解析成/WEB-INF/jsp/list.jsp
}
@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.getSecKillById(seckillId);
if (seckill == null){
return "forward:/seckill/list";
}
model.addAttribute("seckill", seckill);
return "detail";
}
//接下里是几个给Ajax用的接口,返回json
@RequestMapping(value = "/{seckillId}/exposer", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") 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<Exposer>(false, e.getMessage());
}
return result;
}
@RequestMapping(value = "/{seckillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckill") Long seckillId, @PathVariable("md5") String md5, @CookieValue(value = "killPhone", required = false) Long phone){
if (phone == null){
return new SeckillResult<SeckillExecution>(false, "未注册");
}
SeckillResult<SeckillExecution> result;
try {
SeckillExecution execution = secKillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
}catch (RepeatKillException e){
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true, execution);
}catch (SecKillCloseException e){
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
return new SeckillResult<SeckillExecution>(true, execution);
}catch (Exception e){
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true, execution);
}
}
@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult<Long> time(){
Date now = new Date();
return new SeckillResult(true, now.getTime());
}
}
②同时在dto中加入json数据的封装泛型类,注意这种包装json数据的泛型类的使用方法
//所有的Ajax请求返回类型,封装json结果
public class SeckillResult<T> {
private boolean success;
private T Data;
private String error;
public SeckillResult(boolean success, T data) {
this.success = success;
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) {
Data = data;
}
public String getError() {
return error;
}
public void setError(String error) {
this.error = error;
}
}
四、基于Bootstrap开发页面结构
使用 Jsp + Bootstrap 开发前端页面,具体的页面可以在源代码中查看,这里不做记录了。 之后配置tomcat服务器启动项目测试。
访问http://localhost:8080/seckill/list
成功
五、交互逻辑实现
1)cookie登录交互
这里使用到了jQuery cookie的操作插件。
新建webapp/resources/script/seckill.js文件,用于实现cookie登录交互逻辑
//存放主要交互逻辑的js代码
// 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;//直接判断对象会看对象是否为空,空就是undefine就是false; isNaN 非数字返回true
} else {
return false;
}
},
//详情页秒杀逻辑
detail: {
//详情页初始化
init: function (params) {
//手机验证和登录,计时交互
//规划我们的交互流程
//在cookie中查找手机号
var userPhone = $.cookie('userPhone');
//验证手机号
if (!seckill.validatePhone(userPhone)) {
//绑定手机 控制输出
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show: true,//显示弹出层
backdrop: 'static',//禁止位置关闭
keyboard: false//关闭键盘事件
});
$('#killPhoneBtn').click(function () {
var inputPhone = $('#killPhoneKey').val();
console.log("inputPhone: " + inputPhone);//TODO
if (seckill.validatePhone(inputPhone)) {
//电话写入cookie(7天过期)
$.cookie('userPhone', inputPhone, {expires: 7, path: '/seckill'});
//验证通过 刷新页面
window.location.reload();
} else {
//todo 错误文案信息抽取到前端字典里
$('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
}
});
}
}
这里只是简单的验证手机号进行登录,再次启动项目验证登录效果
成功
2)计时交互
这里使用到了jQuery countDown的倒计时插件。
在seckill.js中增加计时交互逻辑
//已经登录
//计时交互
var startTime = params['startTime'];
var endTime = params['endTime'];
var seckillId = params['seckillId'];
$.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);
alert('result: ' + result);
}
});
}
},
countDown: function (seckillId, nowTime, startTime, endTime) {
console.log(seckillId + '_' + nowTime + '_' + startTime + '_' + endTime);
var seckillBox = $('#seckill-box');
if (nowTime > endTime) {
//秒杀结束
seckillBox.html('秒杀结束!');
} else if (nowTime < startTime) {
//秒杀未开始,计时事件绑定
var killTime = new Date(startTime + 1000);//todo 防止时间偏移
seckillBox.countdown(killTime, function (event) {
//时间格式
var format = event.strftime('秒杀倒计时: %D天 %H时 %M分 %S秒 ');
seckillBox.html(format);
}).on('finish.countdown', function () {
//时间完成后回调事件
//获取秒杀地址,控制现实逻辑,执行秒杀
console.log('______fininsh.countdown');
seckill.handlerSeckill(seckillId, seckillBox);
});
} else {
//秒杀开始
seckill.handlerSeckill(seckillId, seckillBox);
}
}
启动项目进行验证
成功
3)秒杀交互
在seckill.js中继续增加最后有关秒杀的交互逻辑
handlerSeckill: 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);
//绑定一次点击事件
$('#killBtn').one('click', function () {
//执行秒杀请求
//1.先禁用按钮
$(this).addClass('disabled');//,<-$(this)===('#killBtn')->
//2.发送秒杀请求执行秒杀
$.post(killUrl, {}, function (result) {
if (result && result['success']) {
var killResult = result['data'];
var state = killResult['state'];
var stateInfo = killResult['stateInfo'];
//显示秒杀结果
node.html('<span class="label label-success">' + stateInfo + '</span>');
}
});
});
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);
}
});
},
启动项目进行秒杀测试
成功
到此整个秒杀系统的Web部分已经全部完成,下一节将简单介绍一下关于项目的高并发优化部分。