项目源码:https://download.csdn.net/download/qq_34288630/10467077
Web层涉及到的技术:
前端交互:页面之间的交互和交互细节
Spring MVC:框架整合,以及如何应用设计和实现Restful接口
Bootstrap和jquery:前者负责页面布局和样式控制,后者负责交互的实现。
1、前端交互设计部分
前端页面流程:
根据细致的流程逻辑,前端工程师设计页面,后端工程师开发对应的代码,可以使得前端和后端之间相互碰触,更加容易的协调一致。所以,前端交互流程是系统开发中一个很重要的部分。
2、Restful接口设计
什么是Restful?
它是一种优雅的URL表述方式,用来设计我们资源的访问URL;通过这个URL的设计,我们就可以很自然的感知到这个URL代表的是哪种业务场景或者什么样的数据或资源,基于Restful设计URL,对于我们接口的使用者、
前端、web系统或者搜索引擎甚至我们的用户,都是非常友好的。
关于Restful的了解不再详细介绍,下面看看我们这个秒杀系统的URL设计:
接下来基于上述资源接口来开始我们对Spring MVC框架的使用。
1、Spring MVC运行流程
使用SpringMVC始终是围绕着Handle开发,Handler最终产出就是Model和View,即模型和视图,下面先回顾SpringMVC的运行路程图:
工作原理(运行流程)
客户端请求提交到DispatcherServlet
由DispatcherServlet控制器查询一个或多个HandlerMapping,找到处理请求的Controller
DispatcherServlet将请求提交到Controller
Controller调用业务逻辑处理后,返回ModelAndView
DispatcherServlet查询一个或多个ViewResoler视图解析器,找到ModelAndView指定的视图
视图负责将结果显示到客户端
DispatcherServlet是整个Spring MVC的核心。它负责接收HTTP请求组织协调Spring MVC的各个组成部分。其主要工作有以下三项:
1. 截获符合特定格式的URL请求。
2. 初始化DispatcherServlet上下文对应的WebApplicationContext,并将其与业务层、持久化层的WebApplicationContext建立关联。
3. 初始化Spring MVC的各个组成组件,并装配到DispatcherServlet中。
2、Http请求的映射原理
用户发送的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的作用域
请求参数绑定
请求方式限制
请求转发和重定向
数据模型赋值
返回Json数据
Cookie访问
看下面的一个例子:
主要应用了URL书写方式,限制了数据提交方式,判定使用url转发还是重定向,使用model承载数据,返回字符串修改URL链接。
3、如何返回Json数据
在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接口使用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>
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>
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);
}
}
}
秒杀成功: