第1章 秒杀业务接口设计与实现
1.1service层开发之前的说明
开始Service层的编码之前,我们首先需要进行Dao层编码之后的思考:在Dao层我们只完成了针对表的相关操作包括写了接口方法和映射文件中的sql语句,并没有编写逻辑的代码,例如对多个Dao层方法的拼接,当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)和增加用户明细(调用SuccessKilledDao接口),这些逻辑我们都需要在Service层完成。这也是一些初学者容易出现的错误,他们喜欢在Dao层进行逻辑的编写,其实Dao就是数据访问的缩写,它只进行数据的访问操作,接下来我们便进行Service层代码的编写。
1.2 秒杀service接口设计
在org.myseckill下创建一个service包用于存放我们的Service接口和其实现类,创建一个exception包用于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等允许出现的异常,一个dto包作为数据传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto关注的是web和service层的数据传递。
A首先创建我们Service接口
里面的方法应该是按”使用者”的角度去设计,SeckillService.java,代码如下:
package org.myseckill.service;
import org.myseckill.dto.Exposer;
import org.myseckill.entity.Seckill;
import org.myseckill.dto.SeckillExecution;
import org.myseckill.exception.RepeatKillException;
import org.myseckill.exception.SeckillCloseException;
import org.myseckill.exception.SeckillException;
import java.util.List;
/**
* 业务接口:
* 该接口中前面两个方法返回的都是跟我们业务相关的对象,而后两个方法返回的对象与业务不相关,
* 这两个对象我们用于封装service和web层传递的数据
* 业务接口,站在“使用者”的角度设计接口,而不是如何实现
* 三个方面:方法定义粒度,参数(越简练越直接越好),返回类型(retrun 类型(要友好)/异常(有的业务允许抛出异常))
* @author kankan
* @creater 2019-06-26 7:52
*/
public interface SeckillService {
/**
* 查询所有的秒杀记录
* @return
*/
List<Seckill> getSeckillList();
/**
*查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 秒杀开启时才会暴露出秒杀接口地址,
* 否则输出系统时间和秒杀时间
* 防止用户提前拼接出秒杀url通过插件进行秒杀
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作,如果传入的md5与内部的不相符,说明用户的url被篡改了,此时拒绝执行秒杀
* 有可能失败,有可能成功,所以要抛出我们允许的异常
* @param seckillId
* @param userPhone
* @param md5
*/
//抛出异常不会有这个问题, 在try/catch时先catch小的
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException,SeckillCloseException,RepeatKillException;
}
B分析:-Exposer exportSeckillUrl(long seckillId);
接口方法 ->返回类型为dto包下的Exposer类:用于封装秒杀的地址信息(这个类是秒杀时数据库那边处理的结果的对象),代码如下
package org.myseckill.dto;
/**
*
* dto和entity的区别在于:entity用于业务数据的封装,而dto关注的是web和service层的数据传递
* 用于封装秒杀的地址信息
* 暴露秒杀地址DTO(数据传输层)
* @author kankan
* @creater 2019-06-26 7:57
*/
public class Exposer {
//是否开启秒杀
private boolean exposed;
//对秒杀地址加密措施
private String md5;
//id为seckillId的商品的秒杀地址
private long seckillId;
//系统当前时间(毫秒)
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 seckillId, long now, long start, long end) {
this.exposed = exposed;
this.seckillId = seckillId;
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 String getMd5() {
return md5;
}
public void setMd5(String md5) {
this.md5 = md5;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
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;
}
@Override
public String toString() {
return "Exposer{" +
"exposed=" + exposed +
", md5='" + md5 + '\'' +
", seckillId=" + seckillId +
", now=" + now +
", start=" + start +
", end=" + end +
'}';
}
}
由于秒杀系统的特殊性,不能提前暴露秒杀接口,否则会被提前写好脚本来进行秒杀,所有就有了该暴露秒杀接口的方法,在秒杀时间未达到开始时间时,秒杀页面只显示当前的系统时间和秒杀开始的时间。
所以暴露秒杀接口的方法的返回值,应根据不同需要创建,在此建立一个dto对象,即Exposer类,通过Exposer类不同的构造函数,来创建不同场景的实例对象暴露给用户。
C SeckillExecution用来封装给页面的结果
然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.myseckill.dto包下再建立SeckillExecution用来封装给页面的结果:
和SeckillExecution.java:
package org.myseckill.dto;
import org.myseckill.entity.SuccessKilled;
import org.myseckill.enums.SeckillStatEnum;
/**
* 封装秒杀执行后的结果
* 用于判断秒杀是否成功,成功就返回秒杀成功的所有信息(秒杀的商品id、秒杀成功状态、成功信息、用户明细),
* 失败就抛出一个我们允许的异常(重复秒杀异常、秒杀结束异常)
*
* @author kankan
* @creater 2019-06-26 8:03
*/
public class SeckillExecution {
//秒杀商品id
private long seckillId;
//秒杀执行结果的状态
private int state;
//状态的明文标识
private String stateInfo;
//当秒杀成功时,需要传递秒杀成功的对象回去
private SuccessKilled successKilled;
//不同的构造方法,秒杀成功返回所有信息
public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getInfo();
this.successKilled = successKilled;
}
/*public SeckillExecution(long seckillId, int state, String stateInfo, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = state;
this.stateInfo = stateInfo;
this.successKilled = successKilled;
}*/
//秒杀失败-返回商品id
/*public SeckillExecution(long seckillId, int state, String stateInfo) {
this.seckillId = seckillId;
this.state = state;
this.stateInfo = stateInfo;
}*/
public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getInfo();
}
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;
}
@Override
public String toString() {
return "SeckillExecution{" +
"seckillId=" + seckillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successKilled=" + successKilled +
'}';
}
}
D 定义秒杀中可能会出现的异常
定义一个基础的异常,所有的子异常继承这个异常SeckillException
package org.myseckill.exception;
/**
* 秒杀相关的所有业务异常
* @author kankan
* @creater 2019-06-26 8:08
*/
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常SeckillCloseException
,需要继承我们一开始写的基础异常
package org.myseckill.exception;
/**
* 秒杀关闭异常(关闭了还执行秒杀)
* @author kankan
* @creater 2019-06-26 8:10
*/
public class SeckillCloseException extends SeckillException {
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
然后还有可能发生重复秒杀异常RepeatKillException
package org.myseckill.exception;
/**
* 重复秒杀异常(运行期异常)
* @author kankan
* @creater 2019-06-26 8:12
*/
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
1.3秒杀service接口的实现
在service包下创建impl包存放它的实现类,SeckillServiceImpl.java,内容如下:
package org.myseckill.service.impl;
import org.myseckill.dao.SeckillDao;
import org.myseckill.dao.SuccessKilledDao;
import org.myseckill.dto.Exposer;
import org.myseckill.dto.SeckillExecution;
import org.myseckill.entity.Seckill;
import org.myseckill.entity.SuccessKilled;
import org.myseckill.enums.SeckillStatEnum;
import org.myseckill.exception.RepeatKillException;
import org.myseckill.exception.SeckillCloseException;
import org.myseckill.exception.SeckillException;
import org.myseckill.service.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
/**
* 当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)和增加用户明细(调用SuccessKilledDao接口)
* 这些逻辑我们都需要在Service层完成
*
* @author kankan
* @creater 2019-06-26 8:14
*/
//注解有 @Component @Service @Dao @Controller(web层),这里已知是service层
//然后在Service实现类的方法中,在需要进行事务声明的方法上加上事务的注解:
@Service
public class SeckillServiceImpl implements SeckillService {
//日志对象slf4g
private Logger logger = LoggerFactory.getLogger(this.getClass());
//注入service的依赖
//当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)
@Resource
private SeckillDao seckillDao;
//增加用户明细(调用SuccessKilledDao接口)
@Resource
private SuccessKilledDao successKilledDao;
//md5盐值字符串,用于混淆md5
private final String slat = "asdfasvrg54mbesognoamg;s'afmaslgma";
/**
* 查询所有的秒杀记录
*
* @return
*/
public List<Seckill> getSeckillList() {
//根据偏移量查询秒杀商品列表
return seckillDao.queryAll(0, 4);
}
/**
* 查询单个秒杀记录
*
* @param seckillId
* @return
*/
public Seckill getById(long seckillId) {
//根据id查询秒杀的商品信息
return seckillDao.queryById(seckillId);
}
/**
* 秒杀开启时才会暴露出秒杀接口地址
* 否则输出系统时间和秒杀时间
* 防止用户提前拼接出秒杀url通过插件进行秒杀
*
* @param seckillId
*/
public Exposer exportSeckillUrl(long seckillId) {
Seckill seckill = seckillDao.queryById(seckillId);
if (null == seckill) {
//没有该商品
return new Exposer(false, seckillId);
}
//如果seckill不为空,则拿到它的开始时间和结束时间
Date startTime = seckill.getStarttime();
Date endTime = seckill.getEndtime();
//系统当前时间
Date nowTime = new Date();
// Date类型要用getTime()获取时间
if (nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime()) {
//秒杀未开启
return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(), endTime.getTime());
}
//转化特定字符串的过程,不可逆(给出md5也用户无法知道如何转化的)
String md5 = getMD5(seckillId);//getMD5方法写在下面
return new Exposer(true, md5, seckillId);
}
private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
//spring的工具包,用于生成md5
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
/**
* 执行秒杀操作,如果传入的md5与内部的不相符,说明用户的url被篡改了,此时拒绝执行秒杀
* 有可能失败,有可能成功,所以要抛出我们允许的异常
* @param seckillId
* @param userPhone
* @param md5
*/
/**
* 使用注解控制事务方法的优点:
* 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException {
if (null == md5 || !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");//秒杀数据被重写了
}
//执行秒杀逻辑:减库存+增加购买明细
Date nowTime = new Date();
try {
//减库存
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//没有更新库存记录,说明秒杀结束
throw new SeckillCloseException("seckill is closed");
} else {
//否则更新了库存,秒杀成功,增加明细
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
//看是否该明细被重复插入,即用户是否重复秒杀
if (insertCount <= 0) {
throw new RepeatKillException("seckill repeated");
} else {
//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
//return new SeckillExecution(seckillId, 1, "秒杀成功", successKilled);
//保证了一些常用常量数据被封装在枚举类型里
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());
}
}
}
在这里我们捕获了运行时异常,这样做的原因就是Spring的事物默认就是发生了RuntimeException才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功,秒杀失败等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在com.myseckill包下建立enum包,专门放置枚举类,然后再建立SeckillStatEnum枚举类:
package org.myseckill.enums;
/**
* 使用枚举表示常量数据字段
* 封装state和stateInfo
* @author kankan
* @creater 2019-06-26 8:47
*/
public enum SeckillStatEnum {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATE_REWRITE(-3,"数据篡改");
private int state;
private String info;
SeckillStatEnum(int state, String info) {
this.state = state;
this.info = info;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = state;
}
public String getInfo() {
return info;
}
public void setInfo(String info) {
this.info = info;
}
//根据state 可确定枚举对象
public static SeckillStatEnum stateOf(int index)
{
for (SeckillStatEnum state : values())
{
if (state.getState()==index)
{
return state;
}
}
return null;
}
}
既然把这些改成了枚举,那么在SeckillServiceImpl类中的executeSeckill方法中成功秒杀的返回值就应该修改为
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution类的构造函数,把state跟stateInfo的值设置从构造函数里面的SeckillStatEnum中取出值来设置:
第2章 基于Spring托管Service实现类
在spring包下创建一个spring-service.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"
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-4.1.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.myseckill.service"></context:component-scan>
</beans>
然后采用注解的方式将Service的实现类加入到Spring IOC容器中:
第3章 配置并使用spring声明式事务
声明式事务的使用方式:
1.早期使用的方式:ProxyFactoryBean+XMl.
2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。
3.注解@Transactional的方式。在实际开发中,建议使用第三种对我们的事务进行控制
事务的定义:事务是指多个操作单元组成的合集,多个单元操作是整体不可分割的,要么都操作不成功,要么都成功。其必须遵循四个原则(ACID)。
原子性(Atomicity):即事务是不可分割的最小工作单元,事务内的操作要么全做,要么全不做;
一致性(Consistency):在事务执行前数据库的数据处于正确的状态,而事务执行完成后数据库的数据还是应该处于正确的状态,即数据完整性约束没有被破坏;如银行转帐,A转帐给B,必须保证A的钱一定转给B,一定不会出现A的钱转了但B没收到,否则数据库的数据就处于不一致(不正确)的状态。
隔离性(Isolation):并发事务执行之间互不影响,在一个事务内部的操作对其他事务是不产生影响,这需要事务隔离级别来指定隔离性;
持久性(Durability):事务一旦执行成功,它对数据库的数据的改变必须是永久的,不会因比如遇到系统故障或断电造成数据不一致或丢失。
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。
事务隔离级别
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
TransactionDefinition.ISOLATION_DEFAULT:这是默认值,表示使用底层数据库的默认隔离级别。对大部分数据库而言,通常这值就是TransactionDefinition.ISOLATION_READ_COMMITTED。
TransactionDefinition.ISOLATION_READ_UNCOMMITTED:该隔离级别表示一个事务可以读取另一个事务修改但还没有提交的数据。该级别不能防止脏读,不可重复读和幻读,因此很少使用该隔离级别。比如PostgreSQL实际上并没有此级别。
TransactionDefinition.ISOLATION_READ_COMMITTED:该隔离级别表示一个事务只能读取另一个事务已经提交的数据。该级别可以防止脏读,这也是大多数情况下的推荐值。
TransactionDefinition.ISOLATION_REPEATABLE_READ:该隔离级别表示一个事务在整个过程中可以多次重复执行某个查询,并且每次返回的记录都相同。该级别可以防止脏读和不可重复读。
TransactionDefinition.ISOLATION_SERIALIZABLE:所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
事务传播行为
所谓事务的传播行为是指,如果在开始当前事务之前,一个事务上下文已经存在,此时有若干选项可以指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了如下几个表示传播行为的常量:
TransactionDefinition.PROPAGATION_REQUIRED:如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。
TransactionDefinition.PROPAGATION_REQUIRES_NEW:创建一个新的事务,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_SUPPORTS:如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。
TransactionDefinition.PROPAGATION_NOT_SUPPORTED:以非事务方式运行,如果当前存在事务,则把当前事务挂起。
TransactionDefinition.PROPAGATION_NEVER:以非事务方式运行,如果当前存在事务,则抛出异常。
TransactionDefinition.PROPAGATION_MANDATORY:如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
TransactionDefinition.PROPAGATION_NESTED:如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。
配置声明式事务,在spring-service.xml中添加对事务的配置:
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.myseckill.service"></context:component-scan>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入数据库连接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置基于属性的声明式事务
默认使用注解来管理事务行为 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
然后在Service实现类的方法中,在需要进行事务声明的方法上加上事务的注解:
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException {
if (null == md5 || !md5.equals(getMD5(seckillId))) {
throw new SeckillException("seckill data rewrite");//秒杀数据被重写了
}
//执行秒杀逻辑:减库存+增加购买明细
Date nowTime = new Date();
try {
//减库存
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//没有更新库存记录,说明秒杀结束
throw new SeckillCloseException("seckill is closed");
} else {
//否则更新了库存,秒杀成功,增加明细
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
//看是否该明细被重复插入,即用户是否重复秒杀
if (insertCount <= 0) {
throw new RepeatKillException("seckill repeated");
} else {
//秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
//return new SeckillExecution(seckillId, 1, "秒杀成功", successKilled);
//保证了一些常用常量数据被封装在枚举类型里
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());
}
}
第4章 完成Service集成测试
Service层的测试
写测试类,我这里的测试类名为SeckillServiceImplTest:
package org.myseckill.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.myseckill.dto.Exposer;
import org.myseckill.dto.SeckillExecution;
import org.myseckill.entity.Seckill;
import org.myseckill.exception.RepeatKillException;
import org.myseckill.exception.SeckillCloseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
import java.util.List;
import static org.junit.Assert.*;
/**
* @author kankan
* @creater 2019-06-26 9:15
*/
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件,要依赖于dao的配置所以2个都要加载
@ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
public class SeckillServiceTest {
//日志
private final Logger logger = LoggerFactory.getLogger(this.getClass());
//依赖注入,将SeckillService注入到测试类下
@Resource
private SeckillService seckillService;
@Test
public void getSeckillList() {
List<Seckill> seckillList = seckillService.getSeckillList();
logger.info("seckillList={}",seckillList);
}
@Test
public void getById() {
long id = 1000;
Seckill seckill = seckillService.getById(id);
logger.info("seckill={}",seckill);
}
@Test
public void exportSeckillUrl() {
long id = 1000;
Exposer exposer = seckillService.exportSeckillUrl(id);
logger.info("exposer={}",exposer);
}
@Test
public void executeSeckill() {
long id = 1000;
long phone = 17808315995L;
String md5 = "07cde05fe83a6df7309eb56e727bf2fd"; //需要用到testExportSeckillUrl得到的md5
try {
SeckillExecution excution = seckillService.executeSeckill(id, phone, md5);
logger.info("excution={}",excution);
} catch (RepeatKillException e) {
logger.error(e.getMessage());
}catch (SeckillCloseException e) {
logger.error(e.getMessage());
}
}
//完整逻辑代码测试,注意可重复执行
@Test
public void testSeckillLogic() throws Exception {
long id = 1000;
Exposer exposer = seckillService.exportSeckillUrl(id);
if(exposer.isExposed()) {
logger.info("exposer={}",exposer);
long phone = 17808315995L;
String md5 = "07cde05fe83a6df7309eb56e727bf2fd";
try {
SeckillExecution excution = seckillService.executeSeckill(id, phone, md5);
logger.info("excution={}",excution);
} catch (RepeatKillException e) {
logger.error(e.getMessage());
}catch (SeckillCloseException e) {
logger.error(e.getMessage());
}
}else {
//秒杀未开启
logger.warn("exposer={}",exposer);
}
}
}
getSeckillList
o.m.service.SeckillServiceTest - seckillList=[Seckill{seckillid=1000, name='1000元秒杀iPhone6', number=99, starttime=Wed Jun 26 00:00:00 CST 2019, endtime=Thu Jun 27 00:00:00 CST 2019, createtime=Thu Jun 20 21:29:19 CST 2019}, Seckill{seckillid=1001, name='500元秒杀iPad2', number=200, starttime=Thu Jun 20 00:00:00 CST 2019, endtime=Fri Jun 21 00:00:00 CST 2019, createtime=Thu Jun 20 21:29:19 CST 2019}, Seckill{seckillid=1002, name='300元秒杀小米4', number=300, starttime=Thu Jun 20 00:00:00 CST 2019, endtime=Fri Jun 21 00:00:00 CST 2019, createtime=Thu Jun 20 21:29:19 CST 2019}, Seckill{seckillid=1003, name='200元秒杀红米note', number=400, starttime=Thu Jun 20 00:00:00 CST 2019, endtime=Fri Jun 21 00:00:00 CST 2019, createtime=Thu Jun 20 21:29:19 CST 2019}]
getById
seckill=Seckill{seckillid=1000, name='1000元秒杀iPhone6', number=99, starttime=Wed Jun 26 00:00:00 CST 2019, endtime=Thu Jun 27 00:00:00 CST 2019, createtime=Thu Jun 20 21:29:19 CST 2019}
exportSeckillUrl
exposer=Exposer{exposed=true, md5='07cde05fe83a6df7309eb56e727bf2fd', seckillId=1000, now=0, start=0, end=0}
executeSeckill
seckill repeated
查看数据库,该用户秒杀商品的明细信息已经被插入明细表,说明我们的业务逻辑没有问题。
这样再测试该方法,junit便不会再在控制台中报错,而是认为这是我们系统允许出现的异常。由上分析可知,第四个方法只有拿到了第三个方法暴露的秒杀商品的地址后才能进行测试,也就是说只有在第三个方法运行后才能运行测试第四个方法,而实际开发中我们不是这样的,需要将第三个测试方法和第四个方法合并到一个方法从而组成一个完整的逻辑流程:
//完整逻辑代码测试,注意可重复执行
@Test
public void testSeckillLogic() throws Exception {
long id = 1000;
Exposer exposer = seckillService.exportSeckillUrl(id);
if(exposer.isExposed()) {
logger.info("exposer={}",exposer);
long phone = 17808315995L;
String md5 = "07cde05fe83a6df7309eb56e727bf2fd";
try {
SeckillExecution excution = seckillService.executeSeckill(id, phone, md5);
logger.info("excution={}",excution);
} catch (RepeatKillException e) {
logger.error(e.getMessage());
}catch (SeckillCloseException e) {
logger.error(e.getMessage());
}
}else {
//秒杀未开启
logger.warn("exposer={}",exposer);
}
}
运行该测试类,控制台成功输出信息,库存会减少,明细表也会增加内容。重复执行,控制台不会报错,只是会抛出一个允许的重复秒杀异常。
seckill repeated
目前为止,Dao层和Service层的集成测试我们都已经完成,接下来进行Web层的开发编码工作