我爱编程JavaSE

基于SSM实现高并发秒杀Web项目(三)

2018-06-10  本文已影响12人  熊猫读书营

这一部分主要介绍秒杀业务Web层的设计和实现,使用SpringMVC整合spring,实现秒杀restful接⼝。

源码可以在微信公众号【程序员修炼营】中获取哦,文末有福利

一、设计Restful接口

1)前端交互流程设计
①前端页面流程如下: ②详情页流程逻辑如下:

2)Restful接口设计

①Restful接口是一种优雅的url表述方式,它表示着一种资源的状态或者状态的转移。

②几个Restful规范:

  1. GET -> 查询操作
  2. POST -> 添加/修改操作
  3. PUT -> 修改操作
  4. 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部分已经全部完成,下一节将简单介绍一下关于项目的高并发优化部分。

上一篇 下一篇

猜你喜欢

热点阅读