开始Service层的编码以前,咱们首先须要进行Dao层编码以后的思考:在Dao层咱们只完成了针对表的相关操做包括写了接口方法和映射文件中的sql语句,并无编写逻辑的代码,例如对多个Dao层方法的拼接,当咱们用户成功秒杀商品时咱们须要进行商品的减库存操做(调用SeckillDao接口)和增长用户明细(调用SuccessKilledDao接口),这些逻辑咱们都须要在Service层完成。这也是一些初学者容易出现的错误,他们喜欢在Dao层进行逻辑的编写,其实Dao就是数据访问的缩写,它只进行数据的访问操做,接下来咱们便进行Service层代码的编写。html
在org.myseckill下建立一个service包用于存放咱们的Service接口和其实现类,建立一个exception包用于存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等容许出现的异常,一个dto包做为数据传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto关注的是web和service层的数据传递。前端
首先建立咱们Service接口,里面的方法应该是按”使用者”的角度去设计,SeckillService.java,代码以下: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,用于封装秒杀的地址信息,代码以下:web
/** * 暴露秒杀地址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:spring
/** * 封装秒杀执行后的结果 * 用于判断秒杀是否成功,成功就返回秒杀成功的全部信息(秒杀的商品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:sql
/** * 重复秒杀异常(运行期异常) * @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:apache
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
} }
在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的容器托管,进行一些配置。
第三种不经常使用
这也是大多数使用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;
声明式事务的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。3.注解@Transactional的方式。在实际开发中,建议使用第三种对咱们的事务进行控制
声明式事务参看blog http://blog.csdn.net/bao19901210/article/details/41724355
spring支持编程式事务管理和声明式事务管理两种方式。
编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。对于编程式事务管理,spring推荐使用TransactionTemplate。
声明式事务管理创建在AOP之上的。其本质是对方法先后进行拦截,而后在目标方法开始以前建立或者加入一个事务,在执行完目标方法以后根据执行状况提交或者回滚事务。声明式事务最大的优势就是不须要经过编程的方式管理事务,这样就不须要在业务逻辑代码中掺琐事务管理的代码,只需在配置文件中作相关的事务规则声明(或经过基于@Transactional注解的方式),即可以将事务规则应用到业务逻辑中。
显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就能够得到彻底的事务支持。和编程式事务相比,声明式事务惟一不足地方是,后者的最细粒度只能做用到方法级别,没法作到像编程式事务那样能够做用到代码块级别。可是即使有这样的需求,也存在不少变通的方法,好比,能够将须要进行事务管理的代码块独立为方法等等。
事务隔离级别
隔离级别是指若干个并发的事务之间的隔离程度。TransactionDefinition 接口中定义了五个表示隔离级别的常量:
事务传播行为
所谓事务的传播行为是指,若是在开始当前事务以前,一个事务上下文已经存在,此时有若干选项能够指定一个事务性方法的执行行为。在TransactionDefinition定义中包括了以下几个表示传播行为的常量:
配置声明式事务,在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()); } }
在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层的开发编码工做