一步一步的搭建JAVA WEB项目,采用Maven构建,基于MYBatis+Spring+Spring MVC+Bootstrap技术的秒杀项目
学习的视频:http://www.imooc.com/learn/587
创建Maven项目
-
创建目录,执行Maven命令
mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=local
问题:Maven命令执行到Generating Project in Batch mode 卡住,参考链接
将项目导入到IDEA工具中
-
修改项目配置
- 修改web.xml中的servlet版本,默认是2.3,其不支持JSP的EL表达式。从Tomcat中的示例的web.xml中拷贝3.0的版本配置到项目中
- 补全目录。项目的main目录下创建java目录,在src目录下创建test目录,test目录下创建java和sources目录
-
打开pom.xml,进行依赖的配置
- 单元测试依赖:Junit4
- 日志依赖:slf4j+logback。(lf4j是规范/接口,log4j,common-logging,logback是日志的实现)
- 数据库依赖:mysql-connector-java、c3p0
- DAO框架:mybatis依赖:mybatis
- Servlet web相关依赖:standard、jstl、jackson-databind、servlet-api
-
Spring依赖:spring-core、spring-beans、spring-context、spring-jdbc、spring-tx、spring-web、spring-webmvc、spring-test
<dependencies>
<!---3.0使用编程方式,4.0使用注解方式-->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.12</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-core</artifactId>
<version>1.1.1</version>
</dependency>
<!--实现slf4j接口并进行整合-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.1</version>
</dependency>
<!--数据库相关依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.35</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>c3p0</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.1.2</version>
</dependency>
<!--DAO框架:mybatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<!--mybatis自身实现的spring的整合依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>
<!--servlet web相关依赖-->
<dependency>
<groupId>taglibs</groupId>
<artifactId>standard</artifactId>
<version>1.1.2</version>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.5.4</version>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!--Spring依赖-->
<!--spring核心-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!--spring的DAO层依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!--spring的WEB层依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
<!--spring的单元测试依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
</dependencies>数据库的设计
- 在项目main目录下创建sql目录,新建 schema.sql,作为数据库的创建脚本
- 脚本代码如下:
```
-- 数据库初始化脚本
-- 创建数据库
CREATE DATABASE seckill;-- 使用数据库
use seckill;-- 创建秒杀库存表:使用InnoDB引擎,其支持事务。主键自增设置为从1000开始,字符格式设置为UTF8
CREATE TABLE seckill(
seckill_id bigint NOT NULL AUTO_INCREMENT COMMENT '商品库存id',
name varchar(120) NOT NULL COMMENT '商品名称',
number int NOT NULL COMMENT '库存数量',
start_time timestamp NOT NULL COMMENT '秒杀开启时间',
end_time timestamp NOT NULL COMMENT '秒杀结束时间',
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',PRIMARY KEY (seckill_id),
KEY idx_start_time(start_time),
KEY idx_end_time(end_time),
KEY idx_create_time(create_time)
)ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';-- 秒杀成功明细表
CREATE TABLE success_killed(
seckill_id bigint NOT NULL COMMENT '秒杀商品id',
user_phone int NOT NULL COMMENT '用户手机号',
state tinyint NOT NULL COMMENT '状态标示:-1指无效,0指成功,1指已付款',
create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (seckill_id,user_phone),
KEY idx_create_time(create_time)
)ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';-- 初始化数据
INSERT INTO seckill(name,number,start_time,end_time)
VALUES
('1000元秒杀iphone6',100,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
('500元秒杀iphone5',200,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
('200元秒杀小米4',300,'2016-06-28 00:00:00','2016-06-29 00:00:00'),
('100元秒杀红米note',400,'2016-06-28 00:00:00','2016-06-29 00:00:00');
-- show create table seckill;
-- 为什么手写DDL,记录每次上线的DDL修改
```
DAO实体和接口
- 创建实体包org.seckill.entity
- 创建DAO包org.seckill.dao
- 创建SecKill实体类,生成getter和setter,重写toString
private long secKillId;
private String name;
private int number;
private Date startTime;
private Date endTime;
private Date createTime; -
创建SuccessKilled实体类,生成getter和setter,重写toString
private long secKillId;
private long userPhone;
private short state;
private Date createTime; -
创建DAO接口SecKillDao,添加减库存,根据ID查询秒杀对象,查询秒杀商品列表方法
/**
* 减库存
* @param secKillId
* @param killTime
* @return如果影响行数大于1,表示更新的记录行数
*/
int reduceNumber(long secKillId,Date killTime);
/**
* 根据id查询秒杀对象
* @param secKillId
* @return
*/
SecKill queryById(long secKillId);
/**
* 根据偏移量查询秒杀商品列表
* @param offset
* @param limit
* @return
*/
List<SecKill> queryAll(int offset,int limit); -
创建DAO接口SuccessKilledDao,添加插入购买明细,根据ID查询购买明细实体的方法
/**
* 插入购买明细,可过滤重复
* @param secKillId
* @param userPhone
* @return插入的行数
*/
int inertSuccessKilled(long secKillId,long userPhone);
/**
*根据ID查询SuccessKilled并携带秒杀产品对象实体
* @param secKillId
* @return
*/
SuccessKilled queryByIdWithSecKill(long secKillId); -
基于MyBaits实现DAO接口
-
创建mybatis-config.xml全局配置文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<!--配置全局属性-->
<settings>
<!--使用jdbc的getGeneratedKeys获取数据库自增主键值-->
<setting name="useGenerateKeys" value="true"/>
<!--使用列别名替换列名 默认为true-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换-->
<setting name="mapUnderscoreCamelCase" value="true"
</settings>
</configuration> - 创建mapper文件夹,用于存储mybatis映射文件
-
创建SecKilledDao.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SecKillDao">
<!--为DAO接口方法提供sql语句配置-->
<update id="reduceNumber">
<!--具体的sql-->
update
seckill
set
number = number -1
where seckill_id = #{secKillId}
and start_time <![CDATA[ <= ]]> #{killTime}
and end_time >= #{killTime}
and number > 0;
</update>
<select id="queryById" resultType="SecKill" parameterType="long">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
where seckill_id = #{secKillId}
</select>
<select id="queryAll" resultType="SecKill">
select seckill_id,name,number,start_time,end_time,create_time
from seckill
order by create_time desc
limit #{offset},#{limit}
</select>
</mapper> -
创建SuccessKilledDao.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="org.seckill.dao.SuccessKilledDao">
<!--为DAO接口方法提供sql语句配置-->
<insert id="insertSuccessKilled">
<!--ignore忽略主键冲突-->
insert ignore into success_killed(seckill_id,user_phone)
values (#{secKilled},#{userPhone})
</insert>
<select id="queryByIdWithSecKill" resultType="SuccessKilled">
<!--根据id查询seccessKilled并携带seckill实体 如何告诉mybatis把结果映射到successkilled同时映射到seckill属性 mybatis可以*控制sql-->
select
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id "seckill.seckill_id",
s.name "seckill.name",
s.number "seckill.number",
s.start_time "seckill.start_time",
s.end_time "seckill.end_time",
s.create_time "seckill.crate_time"
from success_killed sk
inner join seckill s on sk.seckill_id = s.seckill_id
where sk.seckill_id = #{secKillId}
</select>
</mapper>
-
-
mybatis整合spring
- 创建spring文件,用于存储spring配置文件
- 创建spring-dao.xml配置文件
- 创建jdbc.properties配置文件,用于存储数据库相关信息
driver=com.mysql.jdbc.Driver
url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf-8
username=root
password=purple -
在spring-dao.xml配置文件中进行四个步骤的配置
- 配置数据库相关参数
- 配置数据库连接池
- 配置sqlSessionFactory对象
- 配置扫描dao接口包,动态实现 dao接口,并注入到spring容器中
<?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"
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-3.1.xsd">
<!--配置整合mybatis过程-->
<!--配置数据库相关参数-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--数据库连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<property name="driverClass" value="${driver" />
<property name="jdbcUrl" value="${url}"/>
<property name="user" value="${username}"/>
<property name="password" value="${password}"/>
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!--关闭连接后不自动commit-->
<property name="autoCommitOnClose" value="false"/>
<!--获取连接超时的时间-->
<property name="checkoutTimeout" value="1000"/>
<!--获取连接失败的重试次数-->
<property name="acquireRetryAttempts" value="2"/>
</bean>
<!--配置sqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--注入数据库连接池-->
<property name="dataSource" value="dataSource"/>
<!--配置mybatis全局配置文件-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描entity包,使用别名-->
<property name="typeAliasesPackage" value="org.seckill.entity"/>
<!--扫描sql配置文件-->
<property name="mapperLocations" value="mapper/*.xml"/>
</bean>
<!--配置扫描dao接口包,动态实现 dao接口,并注入到spring容器中-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入sqlSessionFactory对象-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!--给出扫描Dao接口包-->
<property name="basePackage" value="org.seckill.dao"/>
</bean>
</beans>
-
Junit4与Spring进行整合,进行Junit4单元测试
-
创建SecKillDao的单元测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
public class SecKillDaoTest {
//注入DAO实现类依赖
@Resource
private SecKillDao secKillDao;
@Test
public void testReduceNumber() throws Exception {
Date killTime = new Date();
int result = secKillDao.reduceNumber(1000L,killTime);
System.out.println(result);
}
@Test
public void testQueryById() throws Exception {
long id = 1000;
SecKill secKill = secKillDao.queryById(id);
System.out.println(secKill.getName());
}
@Test
public void testQueryAll() throws Exception {
List<SecKill> secKillList = secKillDao.queryAll(0,1000);
for(SecKill row : secKillList){
System.out.println(row.toString());
}
}
} -
创建SuccessKilledDao的单元测试类
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring/spring-dao.xml")
public class SuccessKilledDaoTest {
@Resource
private SuccessKilledDao successKilledDao;
@Test
public void testInertSuccessKilled() throws Exception {
int result = successKilledDao.insertSuccessKilled(1000L,28059830451L);
System.out.println(result);
}
@Test
public void testQueryByIdWithSecKill() throws Exception {
SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(1000L,2147483647L);
System.out.println(successKilled.toString());
}
} - 学习点
- 单元测试类可以利用IDEA的快捷键,直接在要测试的类中进行代码的生成
- mybatis的传参,需要在DAO接口方法的形参中使用@Param注解进行指明
-
业务层设计
-
秒杀业务接口设计
- 创建业务包service
- 创建数据传输实体包dto
- 创建异常包exception
-
创建dto实体
-
创建暴露秒杀地址DTO:Exposer
public class Exposer {
/**
* 是否开启秒杀
*/
private boolean exposed;
/**
* 秒杀ID
*/
private long secKillId;
/**
* 一种加密措施
*/
private String md5;
/**
*系统当前时间(毫秒值)
*/
private long now;
private long start;
private long end;
public Exposer(boolean exposed, String md5, long secKillId) {
this.exposed = exposed;
this.md5 = md5;
this.secKillId = secKillId;
}
public Exposer(boolean exposed, long now, long start, long end) {
this.exposed = exposed;
this.now = now;
this.start = start;
this.end = end;
}
public Exposer(boolean exposed, long secKillId) {
this.exposed = exposed;
this.secKillId = secKillId;
}
public boolean isExposed() {
return exposed;
}
public void setExposed(boolean exposed) {
this.exposed = exposed;
}
public long getSecKillId() {
return secKillId;
}
public void setSecKillId(long secKillId) {
this.secKillId = secKillId;
this.secKillId = secKillId;
}
public String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public long getNow() {
return now;
}
public void setNow(long now) {
this.now = now;
}
public long getStart() {
return start;
}
public void setStart(long start) {
this.start = start;
}
public long getEnd() {
return end;
}
public void setEnd(long end) {
this.end = end;
}
} -
创建封装秒杀执行后结果DTO:SecKillExecution
public class SecKillExecution {
private long secKillId;
/**
* 秒杀执行结果状态
*/
private int state;
/**
* 状态表示
*/
private String stateInfo;
private SuccessKilled successKilled;
public SecKillExecution(long secKillId, int state, String stateInfo, SuccessKilled successKilled) {
this.secKillId = secKillId;
this.state = state;
this.stateInfo = stateInfo;
this.successKilled = successKilled;
}
public SecKillExecution(long secKillId, int state, String stateInfo) {
this.secKillId = secKillId;
this.state = state;
this.stateInfo = stateInfo;
}
public long getSecKillId() {
return secKillId;
}
public void setSecKillId(long secKillId) {
this.secKillId = secKillId;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getStateInfo() {
return stateInfo;
}
public void setStateInfo(String stateInfo) {
this.stateInfo = stateInfo;
}
public SuccessKilled getSuccessKilled() {
return successKilled;
}
public void setSuccessKilled(SuccessKilled successKilled) {
this.successKilled = successKilled;
}
}
-
-
创建异常类
-
创建业务相关异常:SecKillException
public class SecKillException extends RuntimeException{
public SecKillException(String message) {
super(message);
}
public SecKillException(String message, Throwable cause) {
super(message, cause);
}
} -
创建重复秒杀异常类:RepeatKillException
public class RepeatKillException extends SecKillException{
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
public RepeatKillException(String message) {
super(message);
}
} -
创建秒杀关闭异常类:SecKillCloseExce
ptionpublic class SecKillCloseException extends SecKillException{
public SecKillCloseException(String message) {
super(message);
}
public SecKillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
-
-
创建SecKillService业务接口:SecKillService
- 创建查询所有的秒杀记录方法:getSecKillList
- 创建查询单个秒杀记录方法:getById
- 创建秒杀开启时输出秒杀接口地址方法:exportSecKillUrl
-
创建执行秒杀操作方法:executeSecKill
public interface SecKillService {
/**
* 查询所有的秒杀记录
* @return
*/
List<SecKill> getSecKillList();
/**
* 查询单个秒杀记录
* @param secKillId
* @return
*/
SecKill getById(long secKillId);
/**
* 秒杀开启时输出秒杀接口地址
* 否则输出系统时间和秒杀时间
* 防止用户猜测出秒杀地址的规律
* @param secKillId
*/
Exposer exportSecKillUrl(long secKillId);
/**
*执行秒杀操作
* @param secKillId
* @param userPhone
* @param md5
*/
SecKillExecution executeSecKill(long secKillId,long userPhone,String md5) throws SecKillException,RepeatKillException,SecKillCloseException;
}
-
业务接口设计的学习点
- 站在使用者的角度进行设计接口,不要冗余设计
- 方法定义粒度,目的明确。非常友好的让使用者调用接口
- 参数要简炼
- 返回类型要清晰
-
秒杀业务接口实现
- 新建enums枚举包,将数据字典放到枚举中
-
在枚举包下创建秒杀状态枚举:SecKillStatEnum
public enum SecKillStatEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改");
private int state;
private String stateInfo;
SecKillStatEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public static SecKillStatEnum stateOf(int index){
for(SecKillStatEnum state : values()) {
if(state.getState() == index){
return state;
}
}
return null;
}
} - 在service包下新建impl包
-
创建SecKillServiceImpl实现类,实现SecKillService接口方法
public class SecKillServiceImpl implements SecKillService{
private Logger logger = LoggerFactory.getLogger(SecKillService.class);
private SecKillDao secKillDao;
private SuccessKilledDao successKilledDao;
//混淆字符,用于混淆MD5
private final String salt = "sdlkjs#$#$dfowierlkjafdmv232k3j@@##$";
@Override
public List<SecKill> getSecKillList() {
return secKillDao.queryAll(0,4);
}
@Override
public SecKill getById(long secKillId) {
return secKillDao.queryById(secKillId);
}
@Override
public Exposer exportSecKillUrl(long secKillId) {
SecKill secKill = secKillDao.queryById(secKillId);
if(null == secKill){
return new Exposer(false,secKillId);
}
Date startTime = secKill.getStartTime();
Date endTime = secKill.getEndTime();
Date nowTime = new Date();
if(nowTime.getTime() < startTime.getTime()
|| nowTime.getTime() > endTime.getTime()){
return new Exposer(false,secKillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
}
//转化特定字符串的过程,不可逆
String md5 = getMD5(secKillId);
return new Exposer(true,md5,secKillId);
}
@Override
public SecKillExecution executeSecKill(long secKillId, long userPhone, String md5)
throws SecKillException, RepeatKillException, SecKillCloseException {
if(null == md5 || md5.equals(getMD5(secKillId))){
throw new SecKillException("seckill datarewirte");
}
try{
//执行秒杀逻辑,减库存,记录购买行为
Date nowTime = new Date();
//减库存
int updateCount = secKillDao.reduceNumber(secKillId,nowTime);
if(updateCount <= 0){
//没有更新到记录,秒杀结束
throw new SecKillCloseException("seckill is Closed");
}else{
//记录购买行为
int insertCount = successKilledDao.insertSuccessKilled(secKillId,userPhone);
//唯一:secKillId,userPhone
if(insertCount <= 0){
//重复秒杀
throw new RepeatKillException("seckill repeated");
}else{
//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(secKillId,userPhone);
return new SecKillExecution(secKillId, SecKillStatEnum.SUCCESS,successKilled);
}
}
}catch(SecKillCloseException e1){
throw e1;
}catch(RepeatKillException e2){
throw e2;
}catch (Exception e){
logger.error(e.getMessage(),e);
//所有编译期异常,转化为运行期异常
throw new SecKillException("seckill inner error:" + e.getMessage());
}
}
/**
* 生成MD5
* @param secKillId
* @return
*/
private String getMD5(long secKillId){
String base = secKillId + "/" + salt;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
}
-
基于Spring托管Service实现类
-
创建Spring的service配置spring-service.xml,进行service包下的注解类型的扫描配置
<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"
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-3.1.xsd">
<!--扫描service包下所有注解的类型-->
<context:component-scan base-package="org.seckill.service"/>
</beans> - 在service实现类中添加上@Service的注解,在类中的dao对象添加上@Autowired的注解
-
-
配置并使用Spring声明式事务
- 在spring-service.xml中添加上配置事务管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean> - 在spring-service.xml中添加上配置基于注解的声明式事务
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在业务类的executeSecKill方法中添加上@Transactional事务注解
- 学习点:使用注解控制事务方法的优点
- 开发团队达到一致约定,明确标注事务方法的编程风格
- 保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求,或者剥离到事务方法外部
- 不是所有的方法都需要事务,如只有一条修改操作,只读操作就不需要事务控制
- 在spring-service.xml中添加上配置事务管理器
-
Service集成测试
-
添加上logback的日志配置文件logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration debug="true">
<!-- ch.qos.logback.core.ConsoleAppender 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>[%-5level] %d{HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<!-- 日志级别 -->
<root>
<level value="debug" />
<appender-ref ref="STDOUT" />
</root>
</configuration> - 使用IDEA为SecKillService业务接口创建单元测试类SecKillServiceTest
-
编写单元测试方法
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml","classpath:spring/spring-service.xml"})
public class SecKillServiceTest {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SecKillService secKillService;
@Test
public void testGetSecKillList() throws Exception {
List<SecKill> list = secKillService.getSecKillList();
logger.info("list={}",list);
}
@Test
public void testGetById() throws Exception {
SecKill secKill = secKillService.getById(1000L);
logger.info("secKill:{}",secKill);
}
/**
* 测试完整业务,注意集成测试代码完整逻辑,注意可重复执行
* @throws Exception
*/
@Test
public void testSecKillLogic() throws Exception {
long id = 1000L;
Exposer exposer = secKillService.exportSecKillUrl(id);
if(exposer.isExposed()){
logger.info("exposer={}",exposer);
long phone = 18059830432L;
SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,exposer.getMd5());
logger.info("secKillExecution:{}",secKillExecution);
}else{
//秒杀未开始
logger.warn("exposer={}",exposer);
}
}
@Test
public void testExportSecKillUrl() throws Exception {
long id = 1000L;
Exposer exposer = secKillService.exportSecKillUrl(id);
logger.info("exposer={}",exposer);
}
@Test
public void testExecuteSecKill() throws Exception {
long id = 1000L;
long phone = 18059830452L;
String md5 = "f1974250b060f51c4a8e48df67232d53";
SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,md5);
logger.info("secKillExecution:{}",secKillExecution);
}
} - 单元测试的学习点
- 集成测试的业务逻辑的完整性
- 注意测试的可重复执行
-
WEB层设计
设计Restful接口
-
SpringMVC整合Spring
- 在web.xml中配置DispatcherServlet
- 创建web包
- 创建spring-web.xml配置文件
-
在spring-web.xml进行SpringMVC的配置
- 开启SpringMVC注解模式
- servlet-mapping映射路径
- 配置jsp显示viewResolver
-
扫描web相关的bean
<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:conext="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">
<!--配置Spring MVC-->
<!--开启SpringMVC注解模式-->
<!--简化配置
1、自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
2、提供一系列功能:数据绑定,数字和日期的转化@NumberFormat,@DataTimeFormat
xml,json默认读写支持
-->
<mvc:annotation-driven/>
<!--servlet-mapping映射路径-->
<!--静态资源默认servlet配置
1、加入对静态资源的处理:js,css,img
2、允许使用/做整体映射
-->
<mvc:default-servlet-handler/>
<!--配置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>
<!--扫描web相关的bean-->
<conext:component-scan base-package="org.seckill.web"/>
</beans>
-
实现秒杀相关的Restful接口
-
创建控制类SecKillController,实现获取列表,获取单条数据,获取系统时间,获取秒杀地址,秒杀的方法
@Controller
@RequestMapping("/seckill/")//模块/资源
public class SecKillController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private SecKillService secKillService;
@RequestMapping(name="/list",method= RequestMethod.GET)
public String list(Model model){
List<SecKill> list = secKillService.getSecKillList();
model.addAttribute("list",list);
return "list";
}
@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 "redirect:/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> exposer(@PathVariable("secKillId") Long secKillId){
SecKillResult<Exposer> result = null;
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"})
public SecKillResult<SecKillExecution> excute(@PathVariable("secKillId") Long secKillId,
@PathVariable("md5") String md5,
@CookieValue(value="killPhone",required = false) Long userPhone){
//springmvc valid
if(userPhone == null){
return new SecKillResult<SecKillExecution>(false,"未注册");
}
SecKillResult<SecKillExecution> result = null;
try{
SecKillExecution secKillExecution = secKillService.executeSecKill(secKillId,userPhone,md5);
result = new SecKillResult<SecKillExecution>(true,secKillExecution);
}catch(RepeatKillException e){
SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.REPEAT);
result = new SecKillResult<SecKillExecution>(false,secKillExecution);
}catch(SecKillCloseException e){
SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.END);
result = new SecKillResult<SecKillExecution>(false,secKillExecution);
}catch(Exception e){
logger.error(e.getMessage(),e);
SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.INNER_ERROR);
result = new SecKillResult<SecKillExecution>(false,secKillExecution);
}
return result;
}
@RequestMapping(value="/time/now",method=RequestMethod.GET)
public SecKillResult<Long> time(){
Date now = new Date();
return new SecKillResult<Long>(true,now.getTime());
}
}
-
-
基于Bootstrap开发页面结构
-
创建jsp文件夹,创建common/header.jsp,common/tag.jsp,list.jsp,detail.jsp,并引入bootstrap框架,jquery、cookie、countdown插件,可以从百度和bootcss的CDN中引入插件。
-
创建js文件seckill.js,进行登录、计时的交互逻辑的编码,并在详细页面中引入
```
var seckill = {
//封装秒杀相关ajax的url
URL: {now: function(){
return '/seckill/time/now';
},
exposer: function(id){
return '/seckill/' + id + '/exposer';
},
execution : function(id,md5){
return '/seckill/' + id + '/' + md5 + '/execution';
}},
//处理秒杀逻辑
handleSecKillKill: 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 killUrl = seckill.URL.execution(secKillId,exposer.md5);
console.log('killUrl:',killUrl);
//绑定一次点击事件
$('#killBtn').one('click',function(){
//执行秒杀请求
$(this).addClass('disabled');
$.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{
//未开启秒杀
//重新计算计时逻辑
seckill.countdown(secKillId,exposer.now,exposer.start,exposer.end);
}
}else{
console.error('result:',result);
}
});},
//计时
countdown: function(secKillId,nowTime,startTime,endTime){var $secKillBox = $('#seckill-box');
if(nowTime > endTime){
$secKillBox.html('秒杀结束');
}else if(nowTime < startTime){
$secKillBox.html('秒杀未开始');
var killTime = new Date(startTime + 1000);
$secKillBox.countdown(killTime,function(event){
var format = event.strftime('秒杀倒计时:%D天 %H时 %M分 %S秒');
$secKillBox.html(format);
}).on('finish.countdown',function(){
//获取秒杀地址,控制实现逻辑,执行秒杀
seckill.handleSecKillKill(secKillId,$secKillBox);
});
}else{
//秒杀开始
seckill.handleSecKillKill(secKillId,$secKillBox);
}
-
},
//验证手机号
validatePhone: function(phone){
if(phone && phone.length == 11 && !isNaN(phone)){
return true;
}else{
return false;
}
},
//详情页秒杀逻辑
detail: {
//详情页初始化
init: function(params){
//用户手机验证和登录,计时交互
//规划交互流程
//在cookie中查找手机号
var killPhone = $.cookie('killPhone'),
startTime = params.startTime,
endTime = params.endTime,
secKillId = params.secKillId;
//验证手机号
if(!seckill.validatePhone(killPhone)){
var killPhoneModal = $('#killPhoneModal');
killPhoneModal.modal({
show: true,
backdrop: 'static',//禁止位置关闭
keyboard: false//关闭键盘事件
});
$('#killPhoneBtn').click(function(){
var inputPhone = $('#killPhoneKey').val();
if(seckill.validatePhone(inputPhone)){
//电话写入cookie
$.cookie('killPhone',inputPhone,{expires:7,path: '/seckill'})
window.location.reload();
}else{
//正常下会有一个前端字典
$('#killPhoneMessage').hide().html('<label class="label label-danger">手机号码错误</label>').show(300);
}
});
}
//用户已经登录
//计时交互
$.get(seckill.URL.now(),function(result){
if(result && result.success){
var nowTime = result.data;
seckill.countdown(secKillId,nowTime,startTime,endTime);
}else{
consolw.error('result:',result);
}
});
}
}
}
```
3. 在detail.jsp页面中引入seckill.js文件,并进行初始化
```
<script type="text/javascript">
$(function(){
//使用EL表达式传入参数
seckill.detail.init({
secKillId: ${secKill.secKillId},
startTime: ${secKill.startTime.time},
endTime: ${secKill.endTime.time}
});
});
</script>
```