高并发秒杀API(四)

作者: MOVE1925 | 来源:发表于2017-01-18 02:10 被阅读496次

    前言

    本篇将完成WEB层的设计与开发,包括:

    • Spring MVC与Spring、MyBatis整合
    • 设计并实现Restful接口

    一、Spring MVC与Spring整合

    之前Spring与MyBatis已经进行过整合了,当通过DispatcherServlet加载Spring MVC的时候,DispatcherServlet同时会把Spring相关的配置也会整合到Spring MVC中,这样就实现了三个框架的整合,即MyBatis+Spring+Spring MVC

    打开web.xml,在Eclipse中位置是src/main/webapp/WEB-INF

      <!-- 配置DispatcherServlet -->
      <servlet>
        <servlet-name>seckill-dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        
        <!-- 配置Spring MVC需要加载的配置文件 -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>classpath:spring/spring-*.xml</param-value>
        </init-param>
      </servlet>
    

    首先配置的是Spring MVC中央控制器的Servlet,即DispatcherServlet,所有Spring MVC的请求都由DispatcherServlet来分发

    然后配置Spring MVC需要加载的配置文件,所有在spring目录下的xml配置文件都要加载进来,之前完成的配置文件有spring-dao.xml和spring-service.xml

      <servlet-mapping>
        <servlet-name>seckill-dispatcher</servlet-name>
        <!-- 默认匹配所有请求 -->
        <url-pattern>/</url-pattern>
      </servlet-mapping>
    

    接着是servlet-mapping,默认匹配所有请求,也就是所有请求都会被DispatcherServlet拦截

    在src\main\resources\spring下新建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">
    <beans>
    

    把以上内容复制到spring-web.xml

    开始配置Spring MVC

    <!-- 开启Spring MVC注解模式 -->
    <mvc:annotation-driven/>
    

    开启Spring MVC注解模式,这一步是一个简化配置,提供了以下功能:

    • 自动注册DefaultAnnotationHandlerMapping,也就是默认地URL到Handler的映射是通过注解的方式
    • 自动注册AnnotationMethodHandlerAdapter,这个是基于注解的Handler适配器
    • 数据绑定
    • 数字和日期的format,也就是转换,例如@NumberFormat,@DataTimeFormat
    • 提供xml,json默认读写支持
      总而言之,我们可以通过不同的注解来完成以上的功能,当然这些功能不仅可以使用注解,也可以使用额外的xml配置文件甚至是编程的方式,根据项目的不同采用不同的方式
    <!-- 静态资源默认servlet配置 -->
    <mvc:default-servlet-handler/>
    

    前面配置了servlet-mapping,映射路径为“/”,使用这样配置的话,就需要这个处理方式,有两个作用:

    • 加入对静态资源的处理,即js、png等
    • 允许使用"/"做整体映射

    接着配置jsp

    <!-- 配置输出样式为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>
    

    也就是需要默认的文档输出是jsp和json,不过json不需要我们提供,因为在开始配置Spring MVC注解模式的时候,已经提供了json的读写支持,只要对应到相应的注解就行

    因为可能要用到el表达式或者jstl标签,所以配置一个viewClass

    还要配置一个识别JSP文件前缀的属性,设置jsp文件存放在/WEB-INF/jsp目录下,再加上后缀

    <!-- 扫描WEB相关的bean -->
    <context:component-scan base-package="org.seckill.web"/>
    

    扫描WEB相关的bean

    接着按照我粗浅的理解,简单的说一下Spring MVC的运行流程:


    Spring MVC运行流程图

    1、用户发送的请求,所有的请求都会映射到DispatcherServlet,这是一个中央控制器的Servlet,这个Servlet会拦截所有的请求,对应在项目中应该就是web.xml中配置的servlet-mapping标签
    2、DispatcherServlet默认的会使用DefaultAnnotation HandlerMapping,主要的作用就是映射URL,哪个URL对应哪个handler,对应在项目中就是在spring-web.xml中mvc:annotation-driven,即开启Spring MVC的注解模式
    3、DispatcherServlet默认的会使用DefaultAnnotation HandlerAdapter,用于做Handler适配,对应在项目中就是在spring-web.xml中mvc:annotation-driven,即开启Spring MVC的注解模式
    4、DefaultAnnotation HandlerAdapter最终会衔接这次开发的SeckillController,最终的产生就是ModelAndView
    5、ModelAndView会与中央控制器DispatcherServlet进行交互
    6、通过第五步的交互,DispatcherServlet会发现应用的是InternalResource ViewResolver,这个其实就是jsp默认的View
    7、通过第五步的交互,DispatcherServlet也会把Model和list.jsp相结合,
    8、最终返回给用户
    实际开发的时候只有蓝色的部分,其他的可以使用默认的注解形式,非常方便地映射URL,去对应到相应的逻辑,同时控制输出数据和对应的页面

    二、设计Restful接口

    一种软件架构风格,设计风格而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。--百度百科

    通过这个项目,我对Restful接口的理解是:

    这是一种优雅的URL表达方式,通过这种URL表达式可以明显的感知到这个URL代表的是什么业务场景或者什么的数据、资源

    以下是本项目的URL设计:

    • /seckill/list:秒杀列表,GET方式
    • /seckill/{id}/detail:详情页,GET方式
    • /seckill/time/now:系统时间,通过系统时间为基准,对秒杀操作进行提前的计时的操作逻辑,GET方式
    • /seckill/{id}/exposer:暴露秒杀,通过这个URL才能拿到最后要执行秒杀操作的URL,POST方式
    • /sekcill/{id}/{md5}/execution:执行秒杀,POST方式

    三、使用Spring MVC实现Restful接口

    在org.seckill包下新建一个web包,用于存放所有的controller,新建一个SeckillController类

    @Controller
    @RequestMapping("/seckill")
    public class SeckillController
    

    标注这个类是一个Controller,使用@Controller注解,目的是将这个类放入Spring容器当中

    还要加上一个@RequestMapping注解,代表的是模块,由于我们使用比较规范的URL设计风格,所有的URL应该是:

    /模块/资源/{id}/更加细分
    

    要获取列表页,也就是要调用Service

    //实例化日志对象,导入org.slf4j包
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
        
    @Autowired
    private SeckillService seckillService;
    

    将Service注入到当前的Controller下,SeckillService在Spring容器中只有一个,Spring容器根据类型匹配,会直接找到bean的实例,然后注入到当前的Controller下

    1、秒杀列表页

    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public String list(Model model){
            
        //获取列表页
        List<Seckill> list = seckillService.getSeckillList();
        model.addAttribute("list", list);
        return "list";
    
    }
    

    参数model就是用来存放渲染list.jsp的数据

    @RequestMapping(value = "/list", method = RequestMethod.GET)
    

    这里Spring MVC的注解映射使用的是@RequestMapping注解,其中value的值是二级URL,后面的method属性限制了http请求的方式,这个方法只接收GET方式的http请求,如果是POST请求,Spring MVC将不会做映射

    @RequestMapping注解它支持很多种URL映射:

    • 支持标准的URL
    • 支持Ant风格URL,即?、*、**等字符
      • ?表示匹配一个字符
      • *表示匹配任意字符
      • **表示匹配任意URL路径
    • 带{}占位符的URL

    举个栗子:

    /user/*/creation可以匹配/user/aaa/creation、/user/bbb/creation等URL
    /user/**/creation可以匹配/user/creation、/user/aaa/bbb/creation等URL
    /user/{userId}可以匹配user/213、user/abc等URL  123、abc可以以参数的方式传入
    /company/{companyId}/user/{userId}/detail匹配/company/123/user/456/detail等URL
    

    在list方法中,通过实例化的SeckillService调用其中的方法

    /**
    * 查询所有秒杀商品记录
    * @return
    */
    List<Seckill> getSeckillList();//这是SeckillService接口中的方法
    
    //获取列表页
    List<Seckill> list = seckillService.getSeckillList();
    model.addAttribute("list", list);
    return "list";
    

    model就是用来存放数据的,并把返回的数据通过字符串进行标识,最后返回一个字符串,那么这里为什么返回一个字符串?这个字符串会被怎么处理?

    之前介绍的Spring MVC运行流程


    Spring MVC运行流程

    HandlerAdapter在对Handler,即SeckillController进行处理之后会返回一个ModelAndView对象,在获得了ModelAndView对象之后,Spring就需要把该View渲染给用户,即返回给浏览器。在这个渲染的过程中,发挥作用的就是ViewResolver和View

    <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>
    

    在spring-web.xml文件中使用的ViewResolver是InternalResourceViewResolver

    InternalResourceViewResolver 会把返回的视图名称都解析为 InternalResourceView 对象, InternalResourceView 会把 Controller 处理器方法返回的模型属性都存放到对应的 request 属性中,然后通过 RequestDispatcher 在服务器端把请求 forword 重定向到目标 URL 。比如在 InternalResourceViewResolver 中定义了 prefix=/WEB-INF/ , suffix=.jsp ,然后请求的 Controller 处理器方法返回的视图名称为 test ,那么这个时候 InternalResourceViewResolver 就会把 test 解析为一个 InternalResourceView 对象,先把返回的模型属性都存放到对应的 HttpServletRequest 属性中,然后利用 RequestDispatcher 在服务器端把请求 forword 到 /WEB-INF/test.jsp

    这就是 InternalResourceViewResolver 一个非常重要的特性,我们都知道存放在 /WEB-INF/ 下面的内容是不能直接通过 request 请求的方式请求到的,为了安全性考虑,我们通常会把 jsp 文件放在 WEB-INF 目录下,而 InternalResourceView 在服务器端跳转的方式可以很好的解决这个问题

    2、秒杀详情页

    @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";
            
    }
    

    之前说过,@RequestMapping注解支持多种URL映射,本项目所设计的URL就有带{}占位符的URL

    @RequestMapping(value = "/{seckillId}/detail", method = RequestMethod.GET)
    public String detail(@PathVariable("seckillId") Long seckillId, Model model)
    

    通过@PathVariable注解绑定后面的参数seckilId,然后对应到URL占位符,当用户传递对应的URL时,@RequestMapping注解中占位符{seckillId}的值会传入detail方法中对应的参数,因为不同的秒杀商品有不同的详情页,所以在二级URL上使用占位符标识不同id的秒杀商品

    接着对传进来的seckillId进行判断

    if(seckillId == null){
        return "redirect:/seckill/list";
    }
    Seckill seckill = seckillService.getById(seckillId);
    if(seckill == null){
        return "forward:/seckill/list";
    }
    

    先要判断seckillId有没有传进来,如果没有传进来,就请求转发到list页面,会回到列表页;如果传进来的seckillId的值不属于任何秒杀商品,那么仍然会重定向到列表页

    这里简单说下请求转发与重定向:

    • 从地址栏显示来说:
      • forward:服务器请求资源,服务器直接访问目标地址的URL,把那个URL的响应内容读取过来,然后把这些内容再发给浏览器,浏览器根本不知道服务器发送的内容从哪里来的,所以它的地址栏还是原来的地址
      • redirect:服务端根据逻辑,发送一个状态码,告诉浏览器重新去请求那个地址,所以地址栏显示的是新的URL,redirect等于客户端向服务器端发出两次request,同时也接受两次response
    • 从数据共享来说
      • forward:转发页面和转发到的页面可以共享request里面的数据
      • redirect:不能共享数据
    • 从运用地方来说
      • forward:一般用于用户登陆的时候,根据角色转发到相应的模块
      • redirect:一般用于用户注销登陆时返回主页面和跳转到其它的网站等
    • 从效率来说
      • forward:高
      • redirect:低

    这里使用redirect和forward没有特别的用意

    model.addAttribute("seckill", seckill);
    return "detail";
    

    接着就是使用model存储数据,并返回一个字符串

    3、秒杀地址暴露

    @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注解设置二级URL,限制http请求方式为POST,并且通过produces返回HttpResponse的hanndler,告诉浏览器这是一个application/json,同时设置编码为UTF-8

    使用@ResponseBody注解,Spring MVC会把返回的数据封装成json

    之前说过有个DTO层,主要是用来封装Service层与WEB层之间的数据,这里在dto包下新建一个SeckillResult类,用于封装数据结果

    //封装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;
    }
    

    通过success来判断请求是否成功,成功的话,返回从数据库中取得的数据,如果请求不成功,返回错误信息

    再生成get和set方法

        SeckillResult<Exposer> result;
        try {
            Exposer exposer = seckillService.exportSeckillUrl(seckillId);
            result = new SeckillResult<Exposer>(true, exposer);
        }
    

    实例化一个SeckillResult<Exposer>对象,调用SeckillService的exportSeckillUrl方法

    /**
    * 秒杀开启时输出秒杀接口地址
    * 否则输出系统时间和秒杀时间
    * @param seckillId
    * @return
    */
    Exposer exportSeckillUrl(long seckillId);//SeckillService接口中的方法
    

    可以看到SeckillService的这个方法返回的是Exposer类型,所以实例化SeckillResult对象的时候,泛型中是Exposer类型

    public class Exposer {
        
        //是否开启秒杀
        private boolean exposed;
        
        //加密措施
        private String md5;
        
        //id
        private long seckillId;
        
        //系统当前时间(毫秒)
        private long now;
        
        //秒杀开启时间
        private long start;
        
        //秒杀结束时间
        private long end;
    }
    

    从之前定义好的Exposer类中的属性就可以看到,如果开启秒杀的话,页面会返回通过MD5加密过的秒杀的地址,如果没有开启秒杀,则返回系统当前时间及秒杀开启与结束时间,用于倒计时

    在SeckillController中,如果秒杀开启,通过调用SeckillService中的exportSeckillUrl方法返回Exposer对象,存放的是MD5及seckillId,然后初始化SeckillResult对象,参数为true,成功返回Exposer对象中的数据

    如果这期间出现错误,说明没有请求成功,需要把上面两步try/catch一下

    catch (Exception e) {
        logger.error(e.getMessage(), e);
        result = new SeckillResult<Exposer>(false, e.getMessage());
    }
    return result;
    

    因为没请求成功,所以需要输出错误信息,同时说明不在秒杀活动期内,同样初始化SeckillResult对象,返回Exposer类中的信息,在Exposer中定义的有系统当前时间以及秒杀开启、结束时间,所以如果没有请求成功,在页面返回的是倒计时或者是秒杀结束等字样

    4、执行秒杀

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

    方法上的注解就不多说了,和上面一样,所有的ajax请求返回的都是统一的SeckillResult,之前在dto包中已经定义了SeckillException,用于封装秒杀执行后的结果

    /**
     * 封装秒杀执行后的结果
     * @author Fzero
     *
     */
    public class SeckillExecution {
        
        private long seckillId;
        
        //秒杀结果执行后的状态
        private int state;
        
        //状态信息
        private String stateInfo;
    
        //秒杀成功对象
        private SuccessKilled successKilled;
    }
    

    这里就可以理解DTO作为Service与WEB层之间数据传递

    因为所有的秒杀都要有用户的标识,本项目没有做登录模块,所以使用手机号phone作为用户的标识,可以看到@RequestMapping注解中的请求参数中没有phone,这个参数是由用户浏览器的Request请求的cookie中获取到的,这里Spring MVC处理cookie有个小问题,如果不设置required属性为false的时候,当请求的header中没有一个cookie叫killPhone的时候,Spring MVC会报错,所以在@CookieValue注解中将required设置为false

    if(phone == null){
        return new SeckillResult<SeckillExecution>(false, "未注册");
    }
    try {
        SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
        return new SeckillResult<SeckillExecution>(true, execution);
    } 
    

    这里先使用if简单的判断一下,实际项目中,要验证的参数很多,可以采用Spring MVC的验证信息,所以这里的killPhone不是必须的,验证用户的逻辑放在代码中

    对于执行秒杀操作,可能会出现各种异常和错误,所以这里需要try/catch以下,并且有些特定的异常比如重复秒杀、秒杀结束等,之前单独建立了一个exception包,专门存放与业务相关的异常

    /**
    * 执行秒杀操作
    * @param seckillId
    * @param userPhone
    * @param md5
    * @return
    */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException,RepeatKillException,SeckillCloseException;
    

    这是SeckillService中定义的方法,可以看到抛出了不同的异常,所以对于这些特定的异常,要单独的catch

    catch (RepeatKillException e) {
          SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
          return new SeckillResult<SeckillExecution>(true, execution);
    }
    

    这个是处理重复秒杀的异常,重新初始化SeckillExecution对象,向数据字典传入对象异常的标识,结果是返回初始化的SeckillResult,上面说过SeckillExecution对象是用于封装秒杀执行后的结果,这里的参数为true,因为当初在SeckillResult定义布尔类型的success的时候就说明这是判断请求是否成功,这里的重复秒杀显然是请求成功,所以参数为true

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

    如果不是上述特定的两个异常,其他的异常都视为inner_error

    最后一个方法就是获取系统时间

    @RequestMapping(value = "/time/now", method = RequestMethod.GET)
    @ResponseBody
    public SeckillResult<Long> time(){
        Date now = new Date();
        return new SeckillResult<Long>(true, now.getTime());
    }
    

    至此,WEB层完成了

    相关文章

      网友评论

        本文标题:高并发秒杀API(四)

        本文链接:https://www.haomeiwen.com/subject/liembttx.html