四、高并发秒杀API之Web层设计与实现

项目源码:https://download.csdn.net/download/qq_34288630/10467077

Web层涉及到的技术:
前端交互:页面之间的交互和交互细节
Spring MVC:框架整合,以及如何应用设计和实现Restful接口
Bootstrap和jquery:前者负责页面布局和样式控制,后者负责交互的实现。

一、前端分析与设计

1、前端交互设计部分

前端页面流程:

四、高并发秒杀API之Web层设计与实现_第1张图片

根据细致的流程逻辑,前端工程师设计页面,后端工程师开发对应的代码,可以使得前端和后端之间相互碰触,更加容易的协调一致。所以,前端交互流程是系统开发中一个很重要的部分。

2、Restful接口设计

什么是Restful?

它是一种优雅的URL表述方式,用来设计我们资源的访问URL;通过这个URL的设计,我们就可以很自然的感知到这个URL代表的是哪种业务场景或者什么样的数据或资源,基于Restful设计URL,对于我们接口的使用者、
前端、web系统或者搜索引擎甚至我们的用户,都是非常友好的。

URL设计规范:
四、高并发秒杀API之Web层设计与实现_第2张图片

关于Restful的了解不再详细介绍,下面看看我们这个秒杀系统的URL设计:
四、高并发秒杀API之Web层设计与实现_第3张图片

接下来基于上述资源接口来开始我们对Spring MVC框架的使用。

二、整合配置Spring MVC框架

1、Spring MVC运行流程

使用SpringMVC始终是围绕着Handle开发,Handler最终产出就是Model和View,即模型和视图,下面先回顾SpringMVC的运行路程图:
四、高并发秒杀API之Web层设计与实现_第4张图片

工作原理(运行流程)

  1. 客户端请求提交到DispatcherServlet

  2. 由DispatcherServlet控制器查询一个或多个HandlerMapping,找到处理请求的Controller

  3. DispatcherServlet将请求提交到Controller

  4. Controller调用业务逻辑处理后,返回ModelAndView

  5. DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图

  6. 视图负责将结果显示到客户端

DispatcherServlet是整个Spring MVC的核心。它负责接收HTTP请求组织协调Spring MVC的各个组成部分。其主要工作有以下三项:

   1. 截获符合特定格式的URL请求。
   2. 初始化DispatcherServlet上下文对应的WebApplicationContext,并将其与业务层、持久化层的WebApplicationContext建立关联。
   3. 初始化Spring MVC的各个组成组件,并装配到DispatcherServlet中。

2、Http请求的映射原理

四、高并发秒杀API之Web层设计与实现_第5张图片
用户发送的http请求,首先会发送到Servlet容器(Tomact或Jetty),而SpringMVC则使用的是HandlerMapping来映射URL,然后使用Handler方法来执行结果(默认使用 DefaultAnnotationHandlerMapping注解来映射,也可以通过XML配置编程来映射)

说明:

是一种简写形式,可以让初学者快速成应用默认的配置方案,会默认注册 DefaultAnnotationHandleMapping以及AnnotionMethodHandleAdapter 这两个 Bean, 这两个 Bean ,前者对应类级别, 后者对应到方法级别;

上在面的 DefaultAnnotationHandlerMapping和AnnotationMethodHandlerAdapter 是 Spring 为 @Controller 分发请求所必需的。

annotation-driven 扫描指定包中类上的注解,常用的注解有:

复制代码
@Controller 声明Action组件
@Service 声明Service组件 @Service(“myMovieLister”)
@Repository 声明Dao组件
@Component 泛指组件, 当不好归类时.
@RequestMapping(“/menu”) 请求映射
@Resource 用于注入,( j2ee提供的 ) 默认按名称装配,@Resource(name=”beanName”)
@Autowired 用于注入,(srping提供的) 默认按类型装配
@Transactional( rollbackFor={Exception.class}) 事务管理
@ResponseBody
@Scope(“prototype”) 设定bean的作用域

注解的使用技巧:
四、高并发秒杀API之Web层设计与实现_第6张图片
请求方法的细节处理:

  1. 请求参数绑定

  2. 请求方式限制

  3. 请求转发和重定向

  4. 数据模型赋值

  5. 返回Json数据

  6. Cookie访问

看下面的一个例子:
四、高并发秒杀API之Web层设计与实现_第7张图片
主要应用了URL书写方式,限制了数据提交方式,判定使用url转发还是重定向,使用model承载数据,返回字符串修改URL链接。
3、如何返回Json数据

四、高并发秒杀API之Web层设计与实现_第8张图片

4、Cookie访问
四、高并发秒杀API之Web层设计与实现_第9张图片

三、Spring MVC整合

在WEB-INF项目中配置文件web.xml中配置我们的前端中央控制器DispatchServlet的配置,也是SpringMVC的核心所在,并加载mybatis配置文件

spring-dao.xml,spring配置文件spring-service.xml、spring-web.xml文件,将三个框架整合起来,文件内容如下:
web.xml:


<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
         id="WebApp_ID" version="3.0">

  
  
  <servlet>
    <servlet-name>dispatcherServletservlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServletservlet-class>
    
    <init-param>
      <param-name>contextConfigLocationparam-name>
      <param-value>classpath:spring/spring-*.xmlparam-value>
    init-param>

  servlet>
  <servlet-mapping>
    <servlet-name>dispatcherServletservlet-name>
    
    <url-pattern>/url-pattern>
  servlet-mapping>
  <welcome-file-list>
    <welcome-file>index.jspwelcome-file>
  welcome-file-list>
web-app>


然后在Spring容器中进行web层相关的配置bean,即Controller的配置,在Spring包下创建一个spring-web.xml,配置内容:


<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans.xsd
         http://www.springframework.org/schema/mvc
         http://www.springframework.org/schema/mvc/spring-mvc.xsd
         http://www.springframework.org/schema/context
         http://www.springframework.org/schema/context/spring-context.xsd " >

       
        <mvc:annotation-driven/>

        

        <mvc:default-servlet-handler/>
        
        <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>
        
        <context:component-scan base-package="web"/>
beans>

如此,便完成了SpringMVC框架的配置,将框架整合到了项目当中,接下来则是开发Controller,这里基于Restful接口进行我们的项目的Controller开发

四、基于Restful的Controller开发

这里Restful接口使用Spring MVC实现的,Controller中的每一个方法都对应我们系统中的一个资源URL,其设计应该遵循Restful接口的设计风格。
1、
创建一个web包用于放web层Controller开发的代码,在该包下创建一个SeckillController.java :

package web;

import com.alibaba.fastjson.JSON;
import dto.Exposer;
import dto.SeckillExecution;
import dto.SeckillResult;
import entity.Seckill;
import enums.SeckillStateEnum;
import exception.RepeatKillException;
import exception.SeckillCloseException;
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 javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.*;
import service.SeckillService;

import java.util.Date;
import java.util.List;

/**
 * Controller中的方法完全按照Service接口中的方法进行开发的:
 * 第一个方法:用于访问我们商品的列表页
 * 第二个方法:访问商品的详情页
 * 第三个方法:返回Json数据,封装了我们商品的秒杀地址
 * 第四个方法:用于封装用户是否秒杀成功的信息
 * 第五个方法:返回系统当前时间
 * @Author:peishunwu
 * @Description:
 * @Date:Created 2018/6/5
 */
@Controller
@RequestMapping("/seckill")
public class SeckillController {

    private final Logger logger = LoggerFactory.getLogger(SeckillController.class);
    @Autowired
    private SeckillService seckillService;

    /**
     * 获取秒杀列表
     * @param model
     * @return
     */
    @RequestMapping(value = "/list",method = RequestMethod.GET)
    public String list(Model model){
        List list = seckillService.getSeckillList();
        model.addAttribute("list",list);
        System.out.println("======================"+model);
        //list.jsp+model=ModelAndView
        return "list";
    }

    /**
     * 通过id获取秒杀详情
     * @param seckillId
     * @param model
     * @return
     */
    @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);
        System.out.println("seckill:"+ JSON.toJSONString(seckill));
        if(seckill == null){
            //如果请求对象不存在
            return "forward:/seckill/list";
        }
        model.addAttribute("seckill",seckill);
        return "detail";
    }


    @RequestMapping(value = "/{seckillId}/exposer",method = RequestMethod.POST,produces = {"application/json;charset=UTF-8"})
    @ResponseBody
    public SeckillResult exposer(Long seckillId){
        SeckillResult result;
        try{
            //Exposer:存放是否秒杀的状态信息
            Exposer exposer = seckillService.exportSeckillUrl(seckillId);
            result = new SeckillResult(true,exposer);
        }catch (Exception e){
            e.printStackTrace();
            result = new SeckillResult(false,e.getMessage());
        }
        return result;

    }
    /*
     * md5:验证用户的请求有没有被篡改
     * 默认的ajax输出是Json格式,所以将输出结果都封装成Json格式。
     */
    @RequestMapping(value = "/{seckillId}/{md5}/execution",method = RequestMethod.POST,produces = "application/json;charset=UTF-8")
    @ResponseBody
    public SeckillResult execute(@PathVariable("seckillId") Long seckillId,
                                                   @PathVariable("md5") String md5,
                                                   @CookieValue(value = "killPhone",required = false) Long phone){
        if(phone == null){
            return new SeckillResult(false,"未注册");
        }
        SeckillResult result;
        try{
            SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId,phone,md5);
            result = new SeckillResult(true,seckillExecution);
            logger.info("秒杀成功结果:{}",JSON.toJSONString(result));
        }catch (RepeatKillException e1){
            SeckillExecution seckillExecution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
            result = new SeckillResult(false,seckillExecution);
        }catch (SeckillCloseException e2) {
            SeckillExecution execution = new SeckillExecution(seckillId,
                    SeckillStateEnum.END);
            result = new SeckillResult(false, execution);
        } catch (Exception e) {
            SeckillExecution execution = new SeckillExecution(seckillId,
                    SeckillStateEnum.INNER_ERROR);
            result = new SeckillResult(false, execution);
        }
        logger.info("秒杀结果:{}",JSON.toJSONString(result));
        return  result;
    }

    /**
     * 获取系统时间
     * @return
     */
    @RequestMapping(value = "/time/now",method = RequestMethod.GET)
    @ResponseBody
    public SeckillResult time(){
        Date now = new Date();
        return new SeckillResult(true,now.getTime());
    }
}

Controller中的方法的开发完全是按照Service接口中的方法进行开发的:

第一个方法用于访问我们商品的列表页;

第二个方法访问商品的详情页;

第三个方法用于返回一个json数据,数据中封装了我们商品的秒杀地址;

第四个方法用于封装用户是否秒杀成功的信息;

第五个方法用于返回系统当前时间。

代码中涉及到一个将返回秒杀商品地址封装为json数据的一个Vo类,即SeckillResult.java,在dto包中创建此类:

2、

package dto;

/**
 * 秒杀结果分装泛型类型类
 * //所有ajax请求的返回类型,封装为json结果类型
 * @Author:peishunwu
 * @Description:
 * @Date:Created  2018/6/5
 */
public class SeckillResult {
    private boolean Success;

    private T data;

    private String error;

    public SeckillResult(boolean success, T data) {
        Success = success;
        this.data = data;
    }

    public SeckillResult(boolean success, String error) {
        Success = success;
        this.error = error;
    }

    public boolean isSuccess() {
        return Success;
    }

    public void setSuccess(boolean success) {
        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;
    }
}

3、list.jsp 秒杀列表页

<%--
  Created by IntelliJ IDEA.
  User: psw
  Date: 2018/6/5
  Time: 15:48
  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 text-center">
            <h2 class="panel-title">
                秒杀列表
            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/seckill/${sk.seckillId}/detail">秒杀详情a>td>
                    tr>
                c:forEach>
                tbody>
            table>
        div>
    div>

div>
body>



<%--


--%>
html>

四、高并发秒杀API之Web层设计与实现_第10张图片

4、detail.jsp 秒杀详情页

<%--
  Created by IntelliJ IDEA.
  User: psw
  Date: 2018/6/5
  Time: 15:48
  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 text-center"><%--面板--%>
            <div class="panel-heading">
                <h3>${seckill.name}h3>
            div>

            <div class="panel-body">
                <h2 class="text-primary">
                    <%--显示time图标--%>
                    <span class="glyphicon glyphicon-time">span>
                        <%--展示倒计时--%>
                    <span class="glyphicon" id="seckill-box">span>
                h2>
            div>
        div>

    div>

    
    <button class="btn btn-primary btn-lg" data-toggle="modal" data-target="#killPhoneModal">
        开始演示模态框
    button>
<%--登录弹出模态框--%>
<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>
<%--jQery文件,务必在bootstrap.min.js之前引入--%>
<%--
--%>

<%--使用CDN 获取公共js http://www.bootcdn.cn/
    CDN不需要去网站获取插件
    可使WEB加速。
--%>
<%--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="/seckill/resource/js/seckill.js" type="text/javascript">script>
<script type="text/javascript">
    $(function () {//调用函数
        //使用EL表达式传入参数
        seckill.detail.init({
            seckillId:${seckill.seckillId},
            startTime:${seckill.startTime.time},//把Date转换为系统的毫秒时间
            endTime:${seckill.endTime.time}
        });
    })
script>
html>

四、高并发秒杀API之Web层设计与实现_第11张图片
5、一个tag.jsp 标签库(JSTL标签库引用) 和head.jsp(js和css公共引入)

tag.jsp:

<%--
  Created by IntelliJ IDEA.
  User: psw
  Date: 2018/6/5
  Time: 15:48
  To change this template use File | Settings | File Templates.
--%>
<%--JSTL标签库引用--%>
<%@ taglib prefix="c"   uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

head.jsp:

<%--
  Created by IntelliJ IDEA.
  User: psw
  Date: 2018/6/5
  Time: 15:48
  To change this template use File | Settings | File Templates.
--%>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta charset="utf-8">

<link rel="stylesheet" href="https://cdn.bootcss.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://cdn.bootcss.com/jquery/2.1.1/jquery.min.js">script>
<script src="https://cdn.bootcss.com/bootstrap/3.3.7/js/bootstrap.min.js">script>

6、用到的js seckill.js

/*逻辑交互js*/



var seckill = {
    /*封装秒杀相关的ajax的url*/
    URL:{
        now:function () {
            return '/seckill/seckill/time/now';
        },
        exposer:function (secillId) {
            return '/seckill/seckill/'+secillId+'/exposer'
        },
        execution:function (seckillId,md5) {
            return '/seckill/seckill/'+seckillId+'/'+md5+'/execution'
        }
    },

    /*验证手机号码*/
    validatePhone:function (phone) {
        if(phone && phone.length == 11 && !isNaN(phone)){
            return true;
        }else {
            return false;
        }
    },


    /*详情页秒杀逻辑*/

    detail:{
        /*详情页初始化*/
        init:function (params) {
            //手机验证和登录,计时交互
            //规划我们的交互流程
            //cookie中查找手机
            var killPhone = $.cookie('killPhone');
            //验证手机号
            if(!seckill.validatePhone(killPhone)){
                var killPhoneModal = $('#killPhoneModal')
                killPhoneModal.modal({
                    show:true,//显示弹出层
                    backdrop:'static',//禁止位置关闭
                    keyboard:false//关闭键盘事件
                });

                $('#killPhoneBtn').click(function () {
                    var inputPhone = $('#killPhoneKey').val();
                    console.log('inputPhone'+inputPhone);
                    if(seckill.validatePhone(inputPhone)){
                        //电话写入cookie,7天过期
                        $.cookie('killPhone',inputPhone,{expires:7,path:'/seckill'})
                        //验证通过,刷新页面
                        window.location.reload();
                    }else {
                        $('#killPhoneMessage').hide().html('').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);
                    alert('result'+result);
                }
            });
        }
    },


    /*执行秒杀*/
    handlerSeckill:function (seckillId,node) {
        //处理秒杀逻辑
        //获取秒杀地址,控制显示器,执行秒杀
        node.hide().html('');
        var url = seckill.URL.exposer(seckillId);
        console.log('url:'+url);
        $.post(url,{'seckillId':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、先禁用按钮
                        $('#killBtn').addClass('disabled');
                        //2、发送秒杀请求执行秒杀
                        $.post(killUrl,{'seckillId':seckillId,'md5':md5},function(result) {
                            //alert(resultData)
                            //if(result){
                                var killResult = result['data'];
                                var state = killResult['state'];
                                var stateInfo = killResult['stateInfo'];
                                //显示秒杀结果
                                node.html('' + stateInfo + '');
                            //}
                        });
                    });
                    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);
            }
        });
    },


    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);
        }
    }
}

秒杀成功:

四、高并发秒杀API之Web层设计与实现_第12张图片

你可能感兴趣的:(高并发秒杀系统学习)