-
Java高并发秒杀API之Service层
第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层的数据传递。
首先创建我们Service接口,里面的方法应该是按”使用者”的角度去设计,SeckillService.java,代码如下:
/**
* 该接口中前面两个方法返回的都是跟我们业务相关的对象,而后两个方法返回的对象与业务不相关,这两个对象我们用于封装service和web层传递的数据
* 业务接口,站在“使用者”的角度设计接口,而不是如何实现
* 三个方面:方法定义粒度,参数(越简练越直接越好),返回类型(retrun 类型(要友好)/异常(有的业务允许抛出异常))
* @author TwoHeads
*
*/
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
*/
SeckillExecution executeSeckill(long seckillId,long userPhone,String md5) throws SeckillException,SeckillCloseException,RepeatKillException;
}
相应在的dto包中创建Exposer.java,用于封装秒杀的地址信息,代码如下:
/**
* 暴露秒杀地址DTO(数据传输层)
* @author TwoHeads
*
*/
public class Exposer {
//是否开启秒杀
private boolean exposed;
//对秒杀地址加密措施
private String md5;
//id为seckillId的商品的秒杀地址
private long seckillId;
//系统当前时间(毫秒)
private long now;
//秒杀的开启时间
private long start;
//秒杀的结束时间
private long end;
/**
* 不同的构造方法方便对象初始化
* @param exposed
* @param md5
* @param seckillId
*/
public Exposer(boolean exposed, String md5, long seckillId) {
super();
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
public Exposer(long now, long start, long end) {
super();
this.now = now;
this.start = start;
this.end = end;
}
public Exposer(boolean exposed, long seckillId) {
super();
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;
}
}
和SeckillExecution.java:
/**
* 封装秒杀执行后的结果
* 用于判断秒杀是否成功,成功就返回秒杀成功的所有信息(秒杀的商品id、秒杀成功状态、成功信息、用户明细),
* 失败就抛出一个我们允许的异常(重复秒杀异常、秒杀结束异常)
* @author TwoHeads
*
*/
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;
}
}
然后需要创建我们在秒杀业务过程中允许的异常,重复秒杀异常RepeatKillException.java:
/**
* 重复秒杀异常(运行期异常)
* @author TwoHeads
*
*/
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public RepeatKillException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
}
秒杀关闭异常SeckillCloseException.java:
/**
* 秒杀关闭异常(关闭了还执行秒杀)
* @author TwoHeads
*
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public SeckillCloseException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
}
和一个异常包含与秒杀业务所有出现的异常SeckillException.java:
public class SeckillException extends RuntimeException{
public SeckillException(String message, Throwable cause) {
super(message, cause);
// TODO Auto-generated constructor stub
}
public SeckillException(String message) {
super(message);
// TODO Auto-generated constructor stub
}
}
1.3秒杀service接口的实现
在service包下创建impl包存放它的实现类,SeckillServiceImpl.java,内容如下:
public class SeckillServiceImpl implements SeckillService{
//日志对象slf4g
private Logger logger = LoggerFactory.getLogger(this.getClass());
private SeckillDao seckillDao;
private SuccessKilledDao successKilledDao;
//md5盐值字符串,用于混淆md5
private final String slat = "asdfasvrg54mbesognoamg;s'afmaslgma";
@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(seckill == null) {
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;
}
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeatKillException {
//将用户传来的md5与内部的md5比较
if(md5 == null || md5.equals(getMD5(seckillId)) == false) {
throw new SeckillException("seckill data rewrite");
}
//执行秒杀逻辑,减库存+记录购买行为
Date nowDate = new Date();
try {
// 减库存
int updateCount = seckillDao.reduceNumber(seckillId, nowDate);
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());
}
}
}
上述代码中return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);本来是return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
我们返回的state和stateInfo参数信息应该是输出给前端的,但是我们不想在我们的return代码中硬编码这两个参数,所以我们应该考虑用枚举的方式将这些常量封装起来,在org.myseckill包下新建一个枚举包enums,创建一个枚举类型SeckillStatEnum.java,内容如下:
/**
* 使用枚举表示常量数据字段
* 封装state和stateInfo
* @author TwoHeads
*
*/
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 String getInfo() {
return info;
}
public static SeckillStatEnum stateOf(int index)
{
for (SeckillStatEnum state : values())
{
if (state.getState()==index)
{
return state;
}
}
return null;
}
}
然后修改执行秒杀操作的非业务类SeckillExecution.java里面涉及到state和stateInfo参数的构造方法:
//不同的构造方法,秒杀成功返回所有信息
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, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getInfo();
}
使一些常用常量数据被封装在枚举类型里。
目前Service的实现全部完成,接下来要将Service交给Spring的容器托管,进行一些配置。
第2章 基于Spring托管Service实现类
第三种不常用
这也是大多数使用spring的方式
在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容器中:
//注解有 @Component @Service @Dao @Controller(web层),这里已知是service层
@Service
public class SeckillServiceImpl implements SeckillService{
//日志对象slf4g
private Logger logger = LoggerFactory.getLogger(this.getClass());
//注入service的依赖
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
第3章 配置并使用spring声明式事务
声明式事务的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。3.注解@Transactional的方式。在实际开发中,建议使用第三种对我们的事务进行控制
声明式事务参看blog http://blog.csdn.net/bao19901210/article/details/41724355
事务的定义:事务是指多个操作单元组成的合集,多个单元操作是整体不可分割的,要么都操作不成功,要么都成功。其必须遵循四个原则(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实现类的方法中,在需要进行事务声明的方法上加上事务的注解:
@Override
@Transactional
/**
* 使用注解控制事务方法的优点: 1.开发团队达成一致约定,明确标注事务方法的编程风格
* 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
* 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
*/
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeatKillException {
//将用户传来的md5与内部的md5比较
if(md5 == null || md5.equals(getMD5(seckillId)) == false) {
throw new SeckillException("seckill data rewrite");
}
//执行秒杀逻辑,减库存+记录购买行为
Date nowDate = new Date();
try {
// 减库存
int updateCount = seckillDao.reduceNumber(seckillId, nowDate);
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());
}
}
第4章 完成Service集成测试
在resources下新建logback.xml
在logback官网https://logback.qos.ch/manual/configuration.html找到配置文件范例粘贴到logback.xml并加入xml头
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type
ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
生成测试类SeckillServiceTest
@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注入到测试类下
@Autowired
private SeckillService seckillService;
@Test
public void testGetSeckillList() {
List<Seckill> list = seckillService.getSeckillList();
logger.info("list={}",list); //把list放入占位符{}中
}
@Test
public void testGetById() {
long id = 1000;
Seckill seckill = seckillService.getById(id);
logger.info("seckill={}",seckill);
}
@Test
public void testExportSeckillUrl() {
long id = 1000;
Exposer exposer = seckillService.exportSeckillUrl(id);
logger.info("exposer={}",exposer);
}
// 输出exposer=Exposer [exposed=true,
// md5=07cde05fe83a6df7309eb56e727bf2fd,
// seckillId=1000,
// now=0, start=0, end=0]
@Test
public void testExecuteSeckill() {
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());
}
}
}
测试testGetSeckillList()
输出
13:18:03.704 [main] DEBUG o.myseckill.dao.SeckillDao.queryAll - <== Total: 4
13:18:03.713 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6dd7b5a3]
13:18:03.715 [main] INFO o.m.service.SeckillServiceTest - list=[Seckill{seckillId=1000, name='1000元秒杀iphone6', number=100, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1001, name='800元秒杀ipad', number=200, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1002, name='6600元秒杀mac book pro', number=300, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1003, name='7000元秒杀iMac', number=400, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}]
non transactional SqlSession说明不是在事务控制下
测试testExportSeckillUrl
13:25:36.078 [main] INFO o.m.service.SeckillServiceTest - exposer=Exposer [exposed=false, md5=null, seckillId=1000, now=1517030736078, start=1514736000000, end=1514822400000]
没有给我们返回id为1000的商品秒杀地址,是因为我们当前的时间并不在秒杀时间开启之内,所以该商品还没有开启。
需要修改数据库中该商品秒杀活动的时间在我们测试时的当前时间之内,然后再进行该方法的测试,控制台中输出如下信息:
13:33:54.040 [main] INFO o.m.service.SeckillServiceTest - exposer=Exposer [exposed=true, md5=07cde05fe83a6df7309eb56e727bf2fd, seckillId=1000, now=0, start=0, end=0]
可知开启了id为1000的商品的秒杀,并给我们输出了该商品的秒杀地址。
测试testExecuteSeckill,需要使用刚才得到的md5
控制台输出
13:49:34.228 [main] INFO o.m.service.SeckillServiceTest - excution=SeckillExecution [seckillId=1000, state=1, stateInfo=秒杀成功, successKilled=SuccessKilled{seckillId=1000, userPhone=17808315995, state=0, createTime=Sat Jan 27 13:49:33 CST 2018}]
查看数据库,该用户秒杀商品的明细信息已经被插入明细表,说明我们的业务逻辑没有问题。但其实这样写测试方法还有点问题,此时再次执行该方法,控制台报错,因为用户重复秒杀了。我们应该在该测试方法中添加try catch,将程序允许的异常包起来而不去向上抛给junit,更改测试代码如下:
@Test
public void testExecuteSeckill() {
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());
}
}
这样再测试该方法,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);
}
}
运行该测试类,控制台成功输出信息,库存会减少,明细表也会增加内容。重复执行,控制台不会报错,只是会抛出一个允许的重复秒杀异常。
目前为止,Dao层和Service层的集成测试我们都已经完成,接下来进行Web层的开发编码工作
原文:https://www.cnblogs.com/twoheads/p/8360830.html