前言
本篇将完成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
seckill-dispatcher
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath:spring/spring-*.xml
首先配置的是Spring MVC中央控制器的Servlet,即DispatcherServlet,所有Spring MVC的请求都由DispatcherServlet来分发
然后配置Spring MVC需要加载的配置文件,所有在spring目录下的xml配置文件都要加载进来,之前完成的配置文件有spring-dao.xml和spring-service.xml
seckill-dispatcher
/
接着是servlet-mapping,默认匹配所有请求,也就是所有请求都会被DispatcherServlet拦截
在src\main\resources\spring下新建spring-web.xml
把以上内容复制到spring-web.xml
开始配置Spring MVC
开启Spring MVC注解模式,这一步是一个简化配置,提供了以下功能:
- 自动注册DefaultAnnotationHandlerMapping,也就是默认地URL到Handler的映射是通过注解的方式
- 自动注册AnnotationMethodHandlerAdapter,这个是基于注解的Handler适配器
- 数据绑定
- 数字和日期的format,也就是转换,例如@NumberFormat,@DataTimeFormat
- 提供xml,json默认读写支持
总而言之,我们可以通过不同的注解来完成以上的功能,当然这些功能不仅可以使用注解,也可以使用额外的xml配置文件甚至是编程的方式,根据项目的不同采用不同的方式
前面配置了servlet-mapping,映射路径为“/”,使用这样配置的话,就需要这个处理方式,有两个作用:
- 加入对静态资源的处理,即js、png等
- 允许使用"/"做整体映射
接着配置jsp
也就是需要默认的文档输出是jsp和json,不过json不需要我们提供,因为在开始配置Spring MVC注解模式的时候,已经提供了json的读写支持,只要对应到相应的注解就行
因为可能要用到el表达式或者jstl标签,所以配置一个viewClass
还要配置一个识别JSP文件前缀的属性,设置jsp文件存放在/WEB-INF/jsp目录下,再加上后缀
扫描WEB相关的bean
接着按照我粗浅的理解,简单的说一下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 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 getSeckillList();//这是SeckillService接口中的方法
//获取列表页
List list = seckillService.getSeckillList();
model.addAttribute("list", list);
return "list";
model就是用来存放数据的,并把返回的数据通过字符串进行标识,最后返回一个字符串,那么这里为什么返回一个字符串?这个字符串会被怎么处理?
之前介绍的Spring MVC运行流程
HandlerAdapter在对Handler,即SeckillController进行处理之后会返回一个ModelAndView对象,在获得了ModelAndView对象之后,Spring就需要把该View渲染给用户,即返回给浏览器。在这个渲染的过程中,发挥作用的就是ViewResolver和View
在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(@PathVariable("seckillId") Long seckillId){
SeckillResult result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult(true, exposer);
} catch (Exception e) {
logger.error(e.getMessage(), e);
result = new SeckillResult(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 {
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 result;
try {
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
result = new SeckillResult(true, exposer);
}
实例化一个SeckillResult
/**
* 秒杀开启时输出秒杀接口地址
* 否则输出系统时间和秒杀时间
* @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(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 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 execution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult(true, execution);
} catch (RepeatKillException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.REPEAT_KILL);
return new SeckillResult(true, execution);
} catch (SeckillCloseException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
return new SeckillResult(true, execution);
} catch (Exception e) {
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
return new SeckillResult(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(false, "未注册");
}
try {
SeckillExecution execution = seckillService.executeSeckill(seckillId, phone, md5);
return new SeckillResult(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(true, execution);
}
这个是处理重复秒杀的异常,重新初始化SeckillExecution对象,向数据字典传入对象异常的标识,结果是返回初始化的SeckillResult,上面说过SeckillExecution对象是用于封装秒杀执行后的结果,这里的参数为true,因为当初在SeckillResult定义布尔类型的success的时候就说明这是判断请求是否成功,这里的重复秒杀显然是请求成功,所以参数为true
catch (SeckillCloseException e) {
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.END);
return new SeckillResult(true, execution);
}
秒杀关闭异常
catch (Exception e) {
logger.error(e.getMessage(), e);
SeckillExecution execution = new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
return new SeckillResult(true, execution);
}
如果不是上述特定的两个异常,其他的异常都视为inner_error
最后一个方法就是获取系统时间
@RequestMapping(value = "/time/now", method = RequestMethod.GET)
@ResponseBody
public SeckillResult time(){
Date now = new Date();
return new SeckillResult(true, now.getTime());
}
至此,WEB层完成了