文章目录
一、秒杀业务接口设计与实现
1、秒杀Service接口设计
main—java下新建service、dto、exception文件夹
dto层:类似于entity,但是这些类字段与业务其实不是很相关,只是为了方便service返回数据而进行了封装。
(1)业务接口
站在“使用者”角度设计接口
三个方面:方法定义粒度明确,参数简练直接,返回类型友好(return 类型/异常)
service—SeckillService.java
package org.luyangsiyi.seckill.service;
import org.luyangsiyi.seckill.dto.Exposer;
import org.luyangsiyi.seckill.dto.SeckillExecution;
import org.luyangsiyi.seckill.entity.Seckill;
import org.luyangsiyi.seckill.exception.RepeatKillException;
import org.luyangsiyi.seckill.exception.SeckillClosedException;
import org.luyangsiyi.seckill.exception.SeckillException;
import java.util.List;
/**
* 业务接口:站在"使用者"角度设计接口
* 三个方面:方法定义粒度明确,参数简练直接,返回类型(return 友好的类型/异常)
* Created by luyangsiyi on 2020/2/22
*/
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
* @return
* @throws SeckillException 是其余两类异常的父类,但是因为抛出的具体原因不同,所以都需要抛出让使用者明确异常的原因
* @throws RepeatKillException
* @throws SeckillClosedException
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillClosedException;
}
(2)新建eums文件下,定义枚举类
SeckillStatEnums.java
package org.seckill.enums;
/**
* 使用枚举类表述常量数据字段
* Created by luyangsiyi on 2020/1/29
*/
public enum SeckillStatEnums {
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改");
private int state;
private String stateInfo;
SeckillStatEnums(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
public static SeckillStatEnums stateOf(int index){
for(SeckillStatEnums state : values()){
if(state.getState() == index){
return state;
}
}
return null;
}
}
(3)dto中定义service中需要用到的实体
Exposer.java
package org.luyangsiyi.seckill.dto;
/**
* 暴露秒杀地址DTO
* 这些字段与业务其实不相关,只是为了方便service返回数据的封装
* Created by luyangsiyi on 2020/2/22
*/
public class Exposer {
//是否开启秒杀
private boolean exposed;
//一种加密措施
private String md5;
//id
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 now, long start, long end) {
this.exposed = exposed;
this.now = now;
this.start = start;
this.end = end;
}
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 +
'}';
}
}
SeckillException.java
package org.luyangsiyi.seckill.dto;
import org.luyangsiyi.seckill.entity.SuccessKilled;
import org.luyangsiyi.seckill.enums.SeckillStatEnum;
/**
* 封装秒杀执行后的结果
* Created by luyangsiyi on 2020/2/22
*/
public class SeckillExecution {
private long seckillId;
//秒杀执行结果
private int state;
//状态标识
private String stateInfo;
//秒杀成功对象
private SuccessKilled successKilled;
//秒杀成功返回的结果
public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = seckillStatEnum.getState();
this.stateInfo = seckillStatEnum.getStateInfo();
this.successKilled = successKilled;
}
//秒杀失败返回的结果
public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum) {
this.seckillId = seckillId;
this.state = seckillStatEnum.getState();
this.stateInfo = seckillStatEnum.getStateInfo();
}
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 +
'}';
}
}
(3)Exception中定义可能的异常
SeckillException.java
package org.seckill.exception;
/**
* 秒杀相关业务异常
* Created by luyangsiyi on 2020/1/29
*/
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
RepeatKillException.java
package org.seckill.exception;
/**
* 重复秒杀异常(运行期异常)
* Created by luyangsiyi on 2020/2/22
*/
public class RepeatKillException extends SeckillException{
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
SeckillCloseException.java
package org.seckill.exception;
/**
* 秒杀关闭异常
* Created by luyangsiyi on 2020/2/22
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
(4)接口实现
service下新建impl文件夹,新建SeckillServiceImpl.java实现接口
SeckillServiceImpl.java
package org.seckill.service.impl;
import org.seckill.dao.SeckillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStatEnums;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.SecikillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;
import java.util.Date;
import java.util.List;
/**
* Created by luyangsiyi on 2020/2/22
*/
public class SeckillServiceImpl implements SeckillService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
private SeckillDao seckillDao;
private SuccessKilledDao successKilledDao;
//md5盐值字符串,用于混淆
private final String slat = "sawfhooauwencklohegd!@@34456ASGR?>'";
@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);
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
//系统当前时间
Date nowTime = new Date();
//getTime()返回毫秒级表示
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);
}
private String getMD5(long seckillId) {
String base = seckillId + "/" + slat;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());//spring工具类
return md5;
}
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {
if (md5 == null || !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);
//唯一:seckillId,userPhone
if (insertCount <= 0) {
//重复秒杀
throw new RepeatKillException("seckill repeated");
} else {
//秒杀成功
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStatEnums.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托管Service实现类
(1)Spring IOC功能理解
对象工厂、依赖管理–>一致的访问接口
- 业务对象依赖:SeckillService依赖SeckillDao、SuccessKilledDao依赖SqlSessionFactory依赖DataSource…
- 为什么要用IOC:对象创建统一托管、规范的生命周期管理、灵活的依赖注入、一致的获取对象
- Spring-IOC注入方式和场景:
| XML | 注解 | Java配置类 |
|---|---|---|
| 1、Bean实现类来自第三方类库,如DataSource等;2:需要命名空间配置,如context、aop、mvc等。 | 项目中自身开发使用的类,可直接在代码中使用注解,如@Service,@Controller | 需要通过代码控制对象创建逻辑的场景,如自定义修改依赖类库 |
- 本项目IOC使用
XML配置、package-scan、Annotation注解
(2)使用spring托管service依赖配置
resources—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.0.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.seckill.service"/>
</beans>
在SeckillServiceImpl.java中注解整个class为@Service,将两个Dao注解为@Autowired
@Service
public class SeckillServiceImpl implements SecikillService {
private Logger logger = LoggerFactory.getLogger(this.getClass());
//注入Service依赖
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
三、配置并使用spring声明式事务
1、Spring声明式事务
- 什么是声明式事务
开启事务—修改SQL-1、修改SQL-2、…、修改SQL-n—提交/回滚事务 ==>全自动的方式即声明式事务 - 声明式事务使用方式
ProxyFactoryBean+XML–>早起使用方式
tx:advice_aop命名空间–>一次配置永久生效
注解@Transactional–>注解控制(建议使用这种方式,只对事务相关的业务进行注解) - 事务方法嵌套
声明式事务独有的概念
传播行为–>propagation_required(当有新事务时,加入到原有的事务中) - 什么时候回滚事务
抛出运行期异常(RuntimeException)、小心不当的try-catch
2、使用spring声明式事务配置
(1)继续配置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"
xmlns:tx="http://www.springframework.org/schema/tx"
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.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.luyangsiyi.seckill.service"/>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库的连接池-->
<property name="dataSource" ref="dataSource"/><!--这边dataSource标红是因为在另一个spring配置文件中,但是运行时都给到程序即可-->
</bean>
<!--配置基于注解的声明式事务,默认使用注解来管理事务行为-->
<!--有很多annotation-driven,选择http://www.springframework.org/schema/tx这个-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
(2)在SeckillSericeImpl.java中增加@Transactional注解
@Override
@Transactional
/**
* 使用注解控制事务方法的优点:
* 1:开发团队达成一致的预定,明确标注事务方法的编程风格
* 2:保证事务方法的执行时间尽可能短,不要禅茶其他风格操作RPC/HTTP请求或者剥离到事务方法外部
* 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制,所以不需要使用tx:advice_aop方式
*/
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillClosedException {
四、完成Service集成测试
1、配置logback
在resources下新建logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<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>
2、测试代码
同dao层测试一样选中service层接口名字生成test测试模块:
注意测试的时候商品的时间可以修改为已开启/未开启秒杀的时间。
package org.luyangsiyi.seckill.service;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.luyangsiyi.seckill.dto.Exposer;
import org.luyangsiyi.seckill.dto.SeckillExecution;
import org.luyangsiyi.seckill.entity.Seckill;
import org.luyangsiyi.seckill.exception.RepeatKillException;
import org.luyangsiyi.seckill.exception.SeckillClosedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import java.util.List;
import static org.junit.Assert.*;
/**
* Created by luyangsiyi on 2020/2/22
*/
@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 getSeckillList() {
List<Seckill> seckills = seckillService.getSeckillList();
logger.info("list={}",seckills);
}
@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);
//exposer=Exposer{exposed=true, md5=' 55FDBFEA6C5D3BBB4352DAFAFAED9053',
//seckillId=1000, now=0, start=0, end=0}
}
@Test
public void executeSeckill() {
long id = 1000;
long phone = 13313344560L;
String md5 = "55FDBFEA6C5D3BBB4352DAFAFAED9053";
//try-catch是为了让我们允许的异常不被junit当做是需要抛出的异常
try{
SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
logger.info("seckillExecution={}",seckillExecution);
} catch (RepeatKillException e){
logger.error(e.getMessage(),e);
} catch (SeckillClosedException e){
logger.error(e.getMessage(),e);
}
//seckillExecution=SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功',
//successKilled=SuccessKilled{seckillId=1000, userPhone=13313344560,state=0, createTime=Sun Feb 23 11:59:14 CST 2020,
//seckill=Seckill{seckId=0, name='2000元秒杀iphone8p', number=99, startTime=Thu Feb 20 14:00:00 CST 2020, endTime=Tue Feb 25 14:00:00 CST 2020, createTime=Sun Feb 23 05:23:04 CST 2020}}}
}
//联合测试exportSeckillUrl()和executeSeckill()
@Test
public void seckillLogicTest(){
long id = 1000;
Exposer exposer = seckillService.exportSeckillUrl(id);
if(exposer.isExposed()){
logger.info("exposer={}",exposer);
long phone = 13313344568L;
String md5 = exposer.getMd5();
try{
SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
logger.info("seckillExecution={}",seckillExecution);
} catch (RepeatKillException e){
logger.error(e.getMessage(),e);
} catch (SeckillClosedException e){
logger.error(e.getMessage(),e);
}
} else {
//秒杀未开启
logger.warn("exposer={}",exposer);
}
//注意测试的多种情况:
//(1)重复秒杀[main] ERROR o.l.s.service.SeckillServiceTest - seckill repeated
// (2) 秒杀未开启 exposer=Exposer{exposed=false, md5='null', seckillId=1002, now=1582380451359, start=1582437600000, end=1582610400000}
// (3) 正常秒杀 seckillExecution=SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功', successKilled=SuccessKilled{seckillId=1000, userPhone=13313344568, state=0, createTime=Sun Feb 23 12:08:53 CST 2020, seckill=Seckill{seckId=0, name='2000元秒杀iphone8p', number=98, startTime=Thu Feb 20 14:00:00 CST 2020, endTime=Tue Feb 25 14:00:00 CST 2020, createTime=Sun Feb 23 05:23:04 CST 2020}}}
}
}
版权声明:本文为qq_40709702原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。