SSM框架学习之高并发秒杀业务--笔记4-- web层

时间:2021-07-31 04:59:10

在前面几节中已经完成了service层和dao层,到目前为止只是后端的设计与编写,这节就要设计到前端的设计了。下面开始总结下这个秒杀业务前端有哪些要点:

1. 前端页面的流程

首先是列表页,点某个商品进入详情页,在这里会有个判断是否用户已登录的逻辑。如果已登录则进入详情页展示逻辑,如果用户未登录则让用户登录,将用户的信息写入cookie后再进入展示逻辑。对于详情页,它首先拿到系统的当前时间,将其与当前秒杀单的秒杀开始时间和秒杀结束时间作比较。若大于秒杀结束结束时间则显示秒杀已结束,若小于秒杀开始时间则显示秒杀未开始和倒计时。若在秒杀时间之内或者倒计时结束则显示商品秒杀地址,用户点击,执行秒杀,返回执行结果。

2. Restful接口设计

 具体什么是Restful呢?他是一种url设计规范,一种资源状态和资源状态的转移,关于Restful知识的具体讲解可以看这篇博文:我所理解的RESTful Web API

 业务的秒杀API设计如下:

Get/seckill/list     秒杀列表

Get/seckill/{id}/detail   详情页

Get/seckill/time/now  系统时间

Post/seckill/{id}/exposer  暴露秒杀

Post/seckill/{id}/execution  执行秒杀

其中:Get表示查询操作,Post表示添加/修改操作, Put表示修改操作,DELETE表示删除操作。通过不同的提交方式来表示它的动作,而后面的url设计遵循的规范是:/模块/资源/{标识}/集合1/...

3. SpringMVC框架

   这个应该是这个案例的重点知识点了吧,前面介绍了Mybatis和Spring的整合,现在来介绍Spring如何整合SpringMVC。首先是在web.xml中注册SpringMVC的核心控制器DispatcherServlet,并配置DispatcherServlet需要加载的配置文件。

web.xml

 <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">
<!--修改servlet版本为3.0--> <!-- 配置DispatcherServlet-->
<servlet>
<servlet-name>seckill-servlet</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-servlet</servlet-name>
<!--默认匹配所有的请求-->
<url-pattern>/</url-pattern>
</servlet-mapping> </web-app> 

可以看到SpringMVC需要加载的文件分别是Spring的三个配置文件,spring-dao.xml,spring-service.xml,spring-web.xml。这三个配置文件分别配置了不同层上的东西,dao层,service层和web层。现在还没有spring-web.xml这个文件,在resource目录下的spring目录下新建这个文件用来配置web层的有关配置。

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-3.0.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置springMVC -->
<!--1:开启springMVC注解模式-->
<!--简化配置:
(1)自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
(2)提供一系列:数据绑定,数字和日期format @NumberFormat,@DataTimeFormat,
xml,json默认读写支持-->
<mvc:annotation-driven/> <!-- 2:servlet-mapping 映射路劲:"/" -->
<!-- 静态资源默认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相关的bean-->
<context:component-scan base-package="org.seckill.web"/>
</beans>

SpringMVC框架配置完成之后便开始编写业务的Controller,分发调度请求并调用Service层中相应的service来处理这些请求并返回模型和视图。在org.seckill目录下新建文件目录web用来存放Controller。新建SeckillController,负责对于Seckill这个资源模块的请求调度,代码如下:

SeckillController

 package org.seckill.web;

 import org.seckill.dao.SeckillResult;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.enums.SeckillStatEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*; import java.util.Date;
import java.util.List; /**
* Created by yuxue on 2016/10/17.
*/
@Controller
@RequestMapping("/seckill")// url:/模块/资源/{id}/细分 / seckill/list
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);
//list.jsp+model=ModelAndView
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";
} //ajax json
/*
@ResponseBody表示该方法的返回结果直接写入HTTP response body中
一般在异步获取数据时使用,在使用@RequestMapping后,返回值通常解析为跳转路径,
加上@ResponseBody后返回结果不会被解析为跳转路径,而是直接写入HTTP response body中。 */
@RequestMapping(value = "/{seckillId}/exposer",method=RequestMethod.GET,
produces={"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable 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;
} /*
从客户端的Cookie中获得用户手机号码,将required属性设置为false,否则若浏览器的cookie中未保存
手机号码的话Spring会报错,这里设置为false,将对用户手机号的验证写在我们自己的判断逻辑中
*/
@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="killphone",required=false)Long phone){
if(phone==null){
return new SeckillResult<SeckillExecution>(false,"未注册");
}
SeckillResult<SeckillExecution> result;
try {
94 SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult<SeckillExecution>(true,execution);
}catch (RepeatKillException e){
//logger.error(e.getMessage(),e);没有必要打印日志,因为是系统允许的异常
SeckillExecution seckillExecution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(false,seckillExecution);
}catch(SeckillCloseException e){
SeckillExecution seckillExecution=new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(false,seckillExecution);
}catch (Exception e){
logger.error(e.getMessage(),e);
SeckillExecution seckillExecution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(false,seckillExecution);
}
} @RequestMapping(value="/time/now", method = RequestMethod.GET)
    public SeckillResult<Long> time(){
Date now=new Date();
return new SeckillResult<Long>(true,now.getTime());
} }

分析:

1.return "redirect:/seckill/list"和return "forward:/seckill/list"  分别实现了请求重定向和请求转发。

2.关于注解@ResponseBody和@RequestMapping中的produces

 1)@ResponseBody

   该注解用于将Controller的方法返回的对象,通过适当的HttpMessageConverter转换为指定格式后,写入到Response对象的body数据区。即我们的Controller执行后不需要跳转到别的什么页面,只需要返回某种格式(json,xml)的数据时候就可以用使用这个注解啦!而@RequestMapping中的produces="application/json;charset=UTF-8"便指定了返回的内容类型为json,编码格式为UTF-8。具体解释见这篇博文:Spring MVC之@RequestMapping 详解以及这篇@RequestBody, @ResponseBody 注解详解

3.@CookieValue注解: 从Http请求头中的Cookie提取指定的某个Cookie,这里required=false表示如果没有这个cookie的话不让SpringMVC报错,而是在我们自己写的逻辑中处理。

4.public SeckillResult<SeckillExecution> execute() 这个方法中用到了个泛型SeckillResult<T>,这个类也是个数据传输对象,在dto包下新建类SeckillResult

SeckillResult

 package org.seckill.dao;

 /**
* Created by yuxue on 2016/10/18.
*/
//所有ajax请求返回类型,封装json结果
public class SeckillResult<T> { private boolean success; private T data; private String error; 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;
}
}

分析:

一开始我不明白为什么要用这个类,打完代码后豁然醒悟:这个类其实封装了所有的请求返回类型!!!对于与Seckill有关的业务SeckillService,它里面有多个方法,一些方法要返回给前端数据,这里的问题便是数据类型可能会有很多种,比如暴露秒杀地址方法返回的是Exposer对象而execute方法返回的是SeckillExecution, 那么便用SeckillResult这个泛型来统一封装来自SeckillService的返回类型。前端要用到后端传来的数据时直接就从这里面去取相应的类型就好。这里是个很重要的设计思路:对于上层只提供统一的一致的接口,而底层复杂的细节则由这个接口封装起来,这些是我个人的理解,不知道对不对。

4. 基于Bootstrap开发页面

终于到了开发前端页面的时候了,老实说的话前端真的是不是很明白啊!感觉前端要记的东西太多了,各种杂七杂八的东西。这个案例是基于Bootstrap这个框架来开发前端页面的,Bootstrap其实帮你写好了前端控件的样式,拿过来直接用就可以开发出显示效果不错的前端页面了,自己不需要去写css样式。总共要写的页面有两个:list.jsp秒杀商品列表页和detail.jsp秒杀上屏详情页。

首先是编写list.jsp页面,为了使用Bootstrap框架,这个业务采用的是在线引入的方式所以必须要联网,总共要引入的文件有3个:Bootstrap的样式css文件,依赖的jQuery文件和核心javascript文件。关于Bootstrap的安装使用方式可以去Bootstrap中文网去查看一下。在webapp/WEB-INF目录下新建jsp目录来存放我们写的jsp文件

list.jsp

 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--引入jstl--%>
<%@include file="common/tag.jsp"%>
<!DOCTYPE html>
<head>
<title>秒杀列表页</title>
<%--jsp页面静态包含--%>
<%@include file="common/head.jsp"%>
</head>
<body>
<%--页面显示部分--%>
<div class="container">
<div class="panel panel-default">
<div clas="panel-heading text-center">
<h2>秒杀列表</h2>
</div>
<div class="panel-body">
<table class="table table-hover">
<thead>
<tr>
<th>名称</th>
<th>库存</th>
<th>开始时间</th>
<th>结束时间</th>
<th>创建时间</th>
<th>详情页</th>
</tr>
</thead>
<tbody>
<c:forEach var="sk" items="${list}">
<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>
<%--target="_blank" 使得点击超链接后弹出的是新的页面--%>
<a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">link</a>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.bootcss.com/jquery/1.11.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.bootcss.com/bootstrap/3.3.0/js/bootstrap.min.js"></script>
</html>

这里将页面要用到的jstl标签单独放到tag.jsp中

 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

还有引入的head.jsp,作用是兼容平板,手机的页面显示,这也是bootstrap作为响应式布局的特点

 <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Bootstrap -->
<link rel="stylesheet" href="http://cdn.bootcss.com/bootstrap/3.3.0/css/bootstrap.min.css">

在webapp/WEB-INF目录下新建详情页面detail.jsp

detail.jsp

 <%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html>
<html lang="zh-cn">
<head>
<title>秒杀详情页</title>
<%--jsp页面静态包含--%>
<%@include file="common/head.jsp"%>
</head>
<body>
<div class="container">
<div class="panel panel-default text-center">
<div class="pannel-heading">
<h1>${seckill.name}</h1>
</div>
<div class="panel-body">
<h2 class="text-danger">
<%--显示time图标--%>
<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 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>
</button>
</div>
</div>
</div>
</div>
</body>
<!-- jQuery文件。务必在bootstrap.min.js 之前引入 -->
<script src="http://cdn.bootcss.com/jquery/1.11.1/jquery.min.js"></script>
<!-- 最新的 Bootstrap 核心 JavaScript 文件 -->
<script src="http://cdn.bootcss.com/bootstrap/3.3.0/js/bootstrap.min.js"></script> <%--使用CDN获取公共js http://www.bootcdn.cn/ --%>
<%--jQuery cookie操作插件--%>
<script src="http://cdn.bootcss.com/jquery-cookie/1.4.1/jquery.cookie.min.js"></script>
<%--jQuery countdown倒计时插件--%>
<script src="http://cdn.bootcss.com/jquery.countdown/2.1.0/jquery.countdown.min.js"></script>
<%--开始编写交互逻辑--%>
<script src="/resources/script/seckill.js" type="text/javascript"></script><%--老师说这里有个小坑,结尾必须写</script>这种形式,不能是/>,否则下面的js不会加载--%>
<script type="text/javascript">
$(function () {
//使用El表达式传入参数
seckill.detail.init({
seckillId:${seckill.seckillId},
startTime:${seckill.startTime.time},
endTime:${seckill.endTime.time}
});
});
</script>
</html> 

1.开启tomacat服务器,测试网页能否正常访问,跳转。这里出现了个小问题:list.jsp中的跳转的detail.jsp的超链接<a class="btn btn-info" href="/seckill/${sk.seckillId}/detail" target="_blank">link</a>我把其中的detail写成了detail.jsp结果Controller不能拦截到路径,查了下Controller中RequestMapping的配置路径为@RequestMapping(value = "/{seckillId}/detail",method=RequestMethod.GET),明白了,原来这个路径要“一模一样”,我之前认为只要在SpringMVC的DispatcherServlet里配置拦截路径为所有路径的话,那么就是拦截所有的html和jsp,所以后缀名什么的应该不重要,只要名字相同就可......傻叉了一回....,这个细节以后要注意。

2.关于<div id="killPhoneModal" class="modal fade">,这是个Bootstrap中的模态框插件,模态框(Modal)是覆盖在父窗体上的子窗体。通常,目的是显示来自一个单独的源的内容,可以在不离开父窗体的情况下有一些互动。子窗体可提供信息、交互等。在模态框中需要注意两点:第一是 .modal,用来把 <div> 的内容识别为模态框。第二是 .fade class。当模态框被切换时,它会引起内容淡入淡出。具体的使用见:http://www.runoob.com/bootstrap/bootstrap-modal-plugin.html

在webapp/resources目录下新建script目录,用来存放我们写的的页面交互脚本。新建seckill.js

 //存放主要交互逻辑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';
}
},
handleSeckillkill: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');
//2.发送秒杀请求执行秒杀
$.post(killUrl,{},function (result) {
if(result&&result['success']){
var killResult=result['data'];
var state=killResult['data'];
var stateInfo=killResult['stateInfo'];
//3.显示秒杀结果
node.html('<span class="label label-success">'+stateInfo+'</span>');
}
});
});
node.show();
}else{
//未开启秒杀
var now=exposer['now'];
var start=exposer['start'];
var end=exposer['end'];
//重新计算计时逻辑
seckillId.countdown(seckillId,now,start,end);
}
}else{
console.log('result:'+result);
}
});
},
//验证手机号
validatePhone: function (phone) {
if (phone && phone.length == 11 && !isNaN(phone)) {
return true;
} else {
return false;
}
},
countdown:function (seckillId,nowTime,startTime,endTime) {
var seckillBox=$('#seckill-box');
//时间判断
if(nowTime>endTime){
//秒杀结束
seckillBox.html('秒杀结束!');
}else if(nowTime<startTime){
//秒杀未开始,计时事件绑定
var killTime=new Date(startTime+1000);
seckillBox.countdown(killTime,function(event){
//时间格式
var format=event.strftime('秒杀倒计时:%D天 %H时 %M分 %S秒');
seckillBox.html(format);
/*时间完成后回调事件*/
}).on('finish.countdown',function () {
//获取秒杀地址,控制现实逻辑,执行秒杀
seckill.handleSeckillkill(seckillId,seckillBox);
});
}else{
//秒杀开始
seckill.handleSeckillkill(seckillId,seckillBox);
}
},
//详情秒杀页逻辑
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)){
//绑定phone
//控制输出
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
$.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'});
//刷新页面
window.location.reload();
}else{
$('#killPhoneMessage').hide().html('<label class="label label-danger">手机号错误!</label>').show(300);
}
});
}
//已经登录
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);
}
}
);
}
}
}

还是吐槽一句:前端真心不熟啊。。。。js,jQuery什么的,要学的东西还有很多啊。

到现在前端的编写算是完成了,下一节讲的是怎么优化这个业务让其能承受更多的并发量,呼呼呼,写到这里不容易啊,哪里搞错了的望指正,谢谢。