Dao层设计与实现=》接口设计+SQL编写
实现了代码和SQL的分离,方便Review,dao层也叫做数据访问层,是对远程存储系统执行操作的过程,这些操作统一存放在Dao层。
而通过Dao组成的 逻辑 则是有Service来完成。
1 秒杀Service接口设计
在src/main/java/or.seckill包下创建:
service包:存放我们的Service接口和其实现类
exception包:存放service层出现的异常例如重复秒杀商品异常、秒杀已关闭等异常
dto包:作为传输层,dto和entity的区别在于:entity用于业务数据的封装,而dto用于完成web和service层的数据传递。
创建Service接口 SeckillService.java
/** * 业务接口:站在“使用者”的角度设计接口 * 三个方面:方法定义粒度,参数,返回类型(return类型、异常要友好,不能乱返回) * @author Terence * */ 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 */ void executeSeckill(long seckillId,long userPhone,String md5) throws SeckillException,RepeatKillException,SeckillCloseException; }
抛出的三个异常分别是秒杀业务相关异常、秒杀关闭异常、秒杀重复异常
秒杀业务相关异常SeckillException.java
/** * 秒杀业务相关异常 * @author Terence * */ public class SeckillException extends RuntimeException { public SeckillException(String message) { super(message); } public SeckillException(String message, Throwable cause) { super(message, cause); } }
秒杀关闭异常SeckillCloseException.java
/** * 秒杀关闭异常,当秒杀结束时用户还要进行秒杀就会出现这个异常 * */ public class SeckillCloseException extends SeckillException{ public SeckillCloseException(String message) { super(message); } public SeckillCloseException(String message, Throwable cause) { super(message, cause); } }
秒杀重复异常RepeatKillException.java
/** * 重复秒杀异常,是一个运行期异常,不需要我们手动try catch * Mysql只支持运行期异常的回滚操作 * @author Terence * */ public class RepeatKillException extends SeckillException { public RepeatKillException(String message) { super(message); } public RepeatKillException(String message, Throwable cause) { super(message, cause); } }
2 秒杀Service接口的实现
Service包下创建impl包:存放service包下接口的实现类。
SeckillServiceImpl.java内容如下:
public class SeckillServiceImpl implements SeckillService { private Logger logger=LoggerFactory.getLogger(this.getClass()); private SeckillDao seckillDao; private SuccessKilledDao successKilledDao; //md5盐值字符串,用于混淆md5; private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v"; public List<Seckill> getSeckillList() { return seckillDao.queryAll(0, 4); } public Seckill getById(long seckillId) { return seckillDao.queryById(seckillId); } 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(); 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); return new Exposer(true,md5,seckillId); } private String getMD5(long seckillId){ String base=seckillId+"/"+salt; //通过盐值转化为加密数据 String md5=DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } //秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚 public SeckillExecutionexecuteSeckill(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); //看是否该明细被重复插入,即用户是否重复秒杀 if (insertCount<=0) { throw new RepeatKillException("seckill repeated"); }else { //秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone); return new SeckillExecution(seckillId,1,”秒杀成功” ,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 newSeckillExecution(seckillId,1,”秒杀成功” ,successKilled);
其中,“1”和“秒杀成功”这两个字段表示的是一种操作执行的状态,用来输出给前端,通常用数据字段记录这些状态;
在这里考虑用枚举将封存常量表示状态,实现数据字典;具体枚举语法请自行学习。
在org.seckill包下新建一个枚举包enums,创建枚举类SeckillStateEnum.java:
/** * 使用枚举表示常量数据字典 * @author Terence * */ public enum SeckillStateEnum { SUCCESS(1,"秒杀成功"), END(0,"秒杀结束"), REPEAT_KILL(-1,"重复秒杀"), INNER_ERROR(-2,"系统异常"), DATA_REWRITE(-3,"数据篡改"); private int state; private String stateInfo; SeckillStateEnum(int state, String info) { this.state = state; this.stateInfo = info; } public int getState() { return state; } public String getInfo() { return stateInfo; } public static SeckillStateEnum stateOf(int index) { for (SeckillStateEnum state : values()) { if (state.getState()==index) { return state; } } return null; } }
修改秒杀操作的非业务类SeckillExcution.java里面涉及到的state和stateInfo参数的构造方法,将其替换为枚举类型:
public SeckillExecution(long seckillId,SeckillStateEnum stateEnum, SuccessKilled successKilled) { super(); this.seckillId = seckillId; this.state = stateEnum.getState(); this.stateInfo = stateEnum.getInfo(); this.successKilled = successKilled; } public SeckillExecution(long seckillId,SeckillStateEnum stateEnum) { super(); this.seckillId = seckillId; this.state = stateEnum.getState(); this.stateInfo = stateEnum.getInfo(); }
并且,使用枚举类型修改Service接口实现类的返回语句:将returnnewSeckillExecution(seckillId,1,”秒杀成功” ,successKilled)修改为return new SeckillExecution (seckillId,SeckillStateEnum.SUCCESS,successKilled)表示一种操作执行状态。
至此,Service接口实现类实现完成,接下来要将Service交付给Spring容器托管,主要是进行一些配置。
3 基于Spring的Service依赖管理
Sprign托管Service,实际就是通过Spring IOC管理依赖,主要通过依赖注入。
利用对象工程这些依赖进行依赖管理,给出一致的访问接口,通过applicationContext或者注解来拿到一个管理的实例。
那么,对于该项目有哪些依赖呢?
如图:
那么,为什么使用Spring IOC呢?
第一,对象的创建统一托管
第二,规范的生命周期的管理
第三,灵活的依赖注入
第四,一致的对象注入
Spring-IOC注入方式和场景是怎么的呢?
实现:
在spring包下创建一个文件spring-service.xml文件:用于扫描service类
<?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.0.xsd" > <!--扫描service包下素有使用注解的类型 --> <context:component-scan base-package="org.seckill.service"/> </beans>
然后采用注解的方式将Service接口的实现类添加到SpringIOC容器中对其进行管理:
/** * spring提供的注解 * @component代表所有的组件统称Spring组件实例,如果知道具体的组件类型则使用@Service、@Dao/@Controller * * */ @Service public class SeckillServiceImpl implements SeckillService { private Logger logger=LoggerFactory.getLogger(this.getClass()); @Autowired //主动获取实例注入 private SeckillDao seckillDao; @Autowired private SuccessKilledDao successKilledDao; //md5盐值字符串,用于混淆md5; private final String salt="jnqw&o4ut922v#y54vq34U#*mn4v"; public List<Seckill> getSeckillList() { return seckillDao.queryAll(0, 4); } ……
对Service类使用@Service注解表明这是一个Service类,注入SpringIOC容器被管理。
对要使用的到实例使用@Autowired声明,实现实例的获取,并自动注入。
接下来,我们来运用Spring的声明式事务对我们项目中的事务进行管理。
4 使用Spring声明式事物配置管理事物
事物管理流程:事物开启=》修改SQL1,修改SQL2,修改SQLn=》提交/回滚事务
现在使用第三方框架管理这个流程,使其摆脱事务编码,这个就叫做声明式事务,
方式一:早期的Spring管理事务是ProxyFactoryBean+XML的方式
方式二:后来添加了tx:advice+aop命名空间使得一次配置永久生效
方式三:使用注解@Transactional 来控制事务,也是推荐的一种方式。
为什么推荐使用注解控制事务的呢?
(1)开发团队达成一致约定,明确标注事务方法的编程风格
(2)保证事务方法的执行时间尽可能短,不要穿插其他网络操作(RPC/HTTP请求或者),或者剥离到事务方法外部。
(3)不是所有的方法都需要事务,如只有一条修改操作或只读操作不需要事务控制。
事务方法嵌套,是声明式事务独有的概念,主要体现在其传播行为上。
什么时候回滚事务?
抛出运行期异常(RuntimeException)可以回滚,如果抛出非运行期异常(部分成功,部分失败),则不会回滚,所以抛异常的时候一定要小心不当的try-catch;
实现:
首先,在spring-service.xml中添加对事务的配置,具体说明见注释:
<!--配置事务管理器 --> <bean id="transactionManager"class="org.springframework.datasource.DataSourceTransaction"> <!-- 注入数据库连接池 在spring-dao.xml中已经配置,此处引用即可--> <property name="dataSource"ref="dataSource"/> </bean> <!-- 配置基于注解的声明式事务 , 默认使用注解来管理事务行为--> <tx:annotation-driven transaction-manager="transactionManager"/>
然后,在Service接口实现类的方法中,在需要事务声明的方法上加上事务的注解:
/** * 使用注解控制事务方法的优点: * 1:开发团队达成一致约定,明确标注事务方法的编程风格 * 2:保证事务方法的执行时间尽可能短,不要穿插其他网络操作(RPC/HTTP请求或者),或者剥离到事务方法外部。 * 3:不是所有的方法都需要事务,如只有一条修改操作或只读操作不需要事务控制。 */ @Transactional //秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚 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");//秒杀数据被重写了 } …… }
完成了声明式事务控制,接下来要对Service业务层做集成测试了。
5 Service层集成测试
集成测试Dao层和Service层。
在test.org.seckill包下添加Service包,创建SeckillServiceTest.java
@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 getSeckillListTest() throws Exception { List<Seckill> list=seckillService.getSeckillList(); logger.info("list={}",list); for(Seckill sk:list) { System.out.println(sk); } } @Test public void getByIdTest() throws Exception { long seckillId=1000; Seckill seckill=seckillService.getById(seckillId); logger.info("seckill={}",seckill); System.out.println(seckill); } @Test //完整的逻辑代码集成测试 public void testExportSeckillLogic() throws Exception{ long id=1001; Exposerexposer=seckillService.exportSeckillUrl(id); logger.info("exposer={}",exposer); //判断秒杀是否开启,如果开启则保留地址和盐值md5开始秒杀; if (exposer.isExposed()) { //秒杀开启,则记录账户开始秒杀 System.out.println(exposer); long userPhone=13476191876L; String md5=exposer.getMd5(); try { //再次执行的时候会出现异常,要在集成测试中try-catch掉抛给Junit的异常。 SeckillExecution seckillExecution = seckillService.executeSeckill(id, userPhone, md5); System.out.println(seckillExecution); }catch (RepeatKillException e) { e.printStackTrace(); }catch (SeckillCloseException e1) { e1.printStackTrace(); } }else { //秒杀未开启 System.out.println(exposer); } } @Test //单独的执行测试 public void testExecuteSeckill() throws Exception{ long id=1001; long phone=1501936156052L; Stringmd5="1da8af7e7ad6829f9eb2e6f18cb45225"; try { SeckillExecutionexecution=seckillService.executeSeckill(id, phone, md5); logger.info("result={}",execution); System.out.println(execution); }catch (RepeatKillException e) { e.printStackTrace(); }catch (SeckillCloseException e1) { e1.printStackTrace(); } } }
单元测试getSeckillListTest()和getByIdTest()方法,可以查询出秒杀的商品列表和秒杀单一商品详情。
执行testExportSeckillLogic()进行逻辑上的集成测试:首先判断秒杀状态,如果在秒杀时间内则生成秒杀连接并返回exposer【 Exposerexposer=seckillService.exportSeckillUrl(id);】,继续利用生成的URL信息执行秒杀【SeckillExecutionexecution=seckillService.executeSeckill(id, phone, md5);】,返回一个秒杀结果,输出秒杀结果即可。
重复秒杀会抛出异常,不可重复秒杀,为了不使其报错,这里try-catch掉即可。