SSM整合开发(二)—Service层
首先感谢慕课网的老师,讲的真的很棒,学习很多书本上学不到的实用知识。
学习课程的地址:https://www.imooc.com/learn/631
Dao层只完成了针对表的相关操作包括写了接口方法和映射文件中的sql语句,并没有编写逻辑的代码,例如对多个Dao层方法的拼接,当我们用户成功秒杀商品时我们需要进行商品的减库存操作(调用SeckillDao接口)和增加用户明细(调用SuccessKilledDao接口),这些逻辑都需要在Service层完成。这也是一些初学者容易出现的错误,他们喜欢在Dao层进行逻辑的编写,其实Dao就是数据访问的缩写,它只进行数据的访问操作。
针对Service层的编写也是从接口编写开始,然后再写实现类的细节。
首先配置spring-service.xml文件,头部依然可以在官网上找最新的。这里主要是扫描service包下注解(@Service)的bean,将其托管给spring容器,同时配置事务管理器等。
xml version="1.0" encoding="UTF-8"?>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.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.seu.service"/> id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <tx:annotation-driven transaction-manager="transactionManager"/>name="dataSource" ref="dataSource"/>
service接口设计,业务接口:站在使用者(程序员)的角度设计接口。三个方面:1.方法定义粒度,方法定义的要非常清楚;2.参数,要越简练越好;3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)。
/**业务接口:站在使用者(程序员)的角度设计接口 * 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好 * 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常) */ public interface SeckillService { /** * 查询全部的秒杀记录 * @return */ ListgetSeckillList(); /** *查询单个秒杀记录 * @param seckillId * @return */ Seckill getById(long seckillId); //再往下,是我们最重要的行为的一些接口 /** * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间 * @param seckillId */ Exposer exportSeckillUrl(long seckillId); /** * 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常 * @param seckillId * @param userPhone * @param md5 * @return */ SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException; }
实现类一般为接口名后面加Impl。
@Service public class SeckillServiceImpl implements SeckillService { //日志对象 private Logger logger= LoggerFactory.getLogger(this.getClass()); //加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好 private final String salt="sad$%^}{ljgb"; //注入Service依赖 @Autowired //@Resource private SeckillDao seckillDao; @Autowired //@Resource private SuccessKilledDao successKilledDao; @Autowired private RedisDao redisDao; public ListgetSeckillList() { return seckillDao.queryAll(0,4); } public Seckill getById(long seckillId) { return seckillDao.queryById(seckillId); } public Exposer exportSeckillUrl(long seckillId) { 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 (startTime.getTime()>nowTime.getTime() || endTime.getTime() return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime()); } //秒杀开启,返回秒杀商品的id、用给接口加密的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; } //秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚 @Transactional /** * 使用注解控制事务方法的优点: * 1.开发团队达成一致约定,明确标注事务方法的编程风格 * 2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部 * 3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制 */ 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 insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone); //看是否该明细被重复插入,即用户是否重复秒杀 if (insertCount<=0) { throw new RepeatKillException("seckill repeated"); }else { //减库存,热点商品竞争 int updateCount=seckillDao.reduceNumber(seckillId,nowTime); if (updateCount<=0) { //没有更新库存记录,说明秒杀结束 rollback throw new SeckillCloseException("seckill is closed"); }else { //秒杀成功,得到成功插入的明细记录,并返回成功秒杀的信息 commit 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()); } } }
相关配置已经体现在上述代码中,这里强调一下。
声明式事务的使用方式:
1.早期使用的方式:ProxyFactoryBean+XMl。
2.tx:advice+aop命名空间,这种配置的好处就是一次配置永久生效。
3.注解@Transactional的方式。在实际开发中,建议使用第三种对我们的事务进行控制。
使用注解控制事务方法的优点:
1.开发团队达成一致约定,明确标注事务方法的编程风格
2.保证事务方法的执行时间尽可能短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部
3.不是所有的方法都需要事务,如只有一条修改操作、只读操作不要事务控制
输出一些业务不相关的数据,区别于entity实体,可以理解成普通的数据实例作为传输操作的对象。这里省略getter、setter和构造函数。
Exposer.java
public class Exposer { //是否开启秒杀 private boolean exposed; //加密措施 private String md5; //秒杀商品id private long seckillId; //系统当前时间(毫秒) private long now; //秒杀的开启时间 private long start; //秒杀的结束时间 private long end; }
SeckillExecution.java
public class SeckillExecution { private long seckillId; //秒杀执行结果的状态 private int state; //状态的明文标识 private String stateInfo; //当秒杀成功时,需要传递秒杀成功的对象回去 private SuccessKilled successKilled; }
将常量封装在枚举类型中,便于统一修改。
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; } }
IDEA使用shift+ctrl+T快捷键快速生成其方法的测试类。
@RunWith(SpringJUnit4ClassRunner.class) //告诉junit spring的配置文件 @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() throws Exception { Listseckills= seckillService.getSeckillList(); System.out.println(seckills); } @Test public void getById() throws Exception { long seckillId=1000; Seckill seckill=seckillService.getById(seckillId); System.out.println(seckill); } @Test//完整逻辑代码测试,注意可重复执行 public void testSeckillLogic() throws Exception { long seckillId=1000; Exposer exposer=seckillService.exportSeckillUrl(seckillId); if (exposer.isExposed()) { System.out.println(exposer); long userPhone=13476191876L; String md5=exposer.getMd5(); try { SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); System.out.println(seckillExecution); }catch (RepeatKillException e) { e.printStackTrace(); }catch (SeckillCloseException e1) { e1.printStackTrace(); } }else { //秒杀未开启 System.out.println(exposer); } } @Test public void executeSeckill() throws Exception { //将代码整合到testSeckillLogic()中进行完整逻辑测试 } }