首先在编写Service
层代码前,我们应该首先要知道这一层到底时干什么的,这里摘取来自ITEYE
一位博主的原话
Service层主要负责业务模块的逻辑应用设计。同样是首先设计接口,再设计其实现的类,接着再Spring的配置文件中配置其实现的关联。这样我们就可以在应用中调用Service接口来进行业务处理。Service层的业务实现,具体要调用到已定义的DAO层的接口,封装Service层的业务逻辑有利于通用的业务逻辑的独立性和重复利用性,程序显得非常简洁。
在项目中要降低耦合的话,分层是一种很好的概念,就是各层各司其职,尽量不做不相干的事,所以Service
层的话顾名思义就是业务逻辑,处理程序中的一些业务逻辑,以及调用DAO
层的代码,这里我们的DAo
层就是连接数据库的那一层,调用关系可以这样表达:
View(页面)>Controller(控制层)>Service(业务逻辑)>Dao(数据访问)>Database(数据库)
SeckillService
首先在som.suny
包下建立interfaces
这个包,这个包里面存放Service
相关的接口,然后建立SeckillService
接口文件,代码如下:public interface SeckillService { /** * 查询全部的秒杀记录. * @return 数据库中所有的秒杀记录 */ List<Seckill> getSeckillList(); /** * 查询单个秒杀记录 * @param seckillId 秒杀记录的ID * @return 根据ID查询出来的记录信息 */ Seckill getById(long seckillId); /** * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址 * @param seckillId 秒杀商品Id * @return 根据对应的状态返回对应的状态实体 */ Exposer exportSeckillUrl(long seckillId); /** * 执行秒杀操作,有可能是失败的,失败我们就抛出异常 * @param seckillId 秒杀的商品ID * @param userPhone 手机号码 * @param md5 md5加密值 * @return 根据不同的结果返回不同的实体信息 */ SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)throws SeckillException,RepeatKillException,SeckillCloseException;
建立后接口之后我们要写实现类了,在写实现类的时候我们肯定会碰到一个这样的问题,你要向前端返回json
数据的话,你是返回什么样的数据好?直接返回一个数字状态码或者时文字?这样设计肯定是不好的,所以我们应该向前段返回一个实体信息json
,里面包含了一系列的信息,无论是哪种状态都应该可以应对,既然是与数据库字段无关的类,那就不是PO
了,所以我们建立一个DTO
数据传输类,关于常见的几种对象我的解释如下:
在com.suny
下建立dto
包,然后建立Exposer
类,这个类是秒杀时数据库那边处理的结果的对象
public class Exposer { /*是否开启秒杀 */ private boolean exposed; /* 对秒杀地址进行加密措施 */ private String md5; /* id为seckillId的商品秒杀地址 */ private long seckillId; /* 系统当前的时间 */ private LocalDateTime now; /* 秒杀开启的时间 */ private LocalDateTime start; /* 秒杀结束的时间 */ private LocalDateTime end; public Exposer() { } public Exposer(boolean exposed, String md5, long seckillId) { this.exposed = exposed; this.md5 = md5; this.seckillId = seckillId; } public Exposer(boolean exposed, long seckillId, LocalDateTime now, LocalDateTime start, LocalDateTime 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 LocalDateTime getNow() { return now; } public void setNow(LocalDateTime now) { this.now = now; } public LocalDateTime getStart() { return start; } public void setStart(LocalDateTime start) { this.start = start; } public LocalDateTime getEnd() { return end; } public void setEnd(LocalDateTime end) { this.end = end; } @Override public String toString() { return "Exposer{" + "秒杀状态=" + exposed + ", md5加密值='" + md5 + '\'' + ", 秒杀ID=" + seckillId + ", 当前时间=" + now + ", 开始时间=" + start + ", 结束=" + end + '}'; } }
然后我们给页面返回的数据应该是更加友好的封装数据,所以我们再在com.suny.dto
包下再建立SeckillExecution
用来封装给页面的结果:
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; } @Override public String toString() { return "SeckillExecution{" + "秒杀的商品ID=" + seckillId + ", 秒杀状态=" + state + ", 秒杀状态信息='" + stateInfo + '\'' + ", 秒杀的商品=" + successKilled + '}'; } }
SeckillException
/** * 秒杀基础异常 * Created by 孙 */ public class SeckillException extends RuntimeException { public SeckillException(String message) { super(message); } public SeckillException(String message, Throwable cause) { super(message, cause); } }
+ 首选可能会出现秒杀关闭后被秒杀情况,所以建立秒杀关闭异常`SeckillCloseException`,需要继承我们一开始写的基础异常
/** * 秒杀已经关闭异常,当秒杀结束就会出现这个异常 * Created by 孙 */ public class SeckillCloseException extends SeckillException{ public SeckillCloseException(String message) { super(message); } public SeckillCloseException(String message, Throwable cause) { super(message, cause); } }
RepeatKillException
/** * 重复秒杀异常,不需要我们手动去try catch * Created by 孙 */ public class RepeatKillException extends SeckillException{ public RepeatKillException(String message) { super(message); } public RepeatKillException(String message, Throwable cause) { super(message, cause); } }
Service
接口@Service public class SeckillServiceImpl implements SeckillService { private Logger logger = LoggerFactory.getLogger(this.getClass()); /* 加入一个盐值,用于混淆*/ private final String salt = "thisIsASaltValue"; @Autowired private SeckillMapper seckillMapper; @Autowired private SuccessKilledMapper successKilledMapper; /** * 查询全部的秒杀记录. * * @return 数据库中所有的秒杀记录 */ @Override public List<Seckill> getSeckillList() { return seckillMapper.queryAll(0, 4); } /** * 查询单个秒杀记录 * * @param seckillId 秒杀记录的ID * @return 根据ID查询出来的记录信息 */ @Override public Seckill getById(long seckillId) { return seckillMapper.queryById(seckillId); } /** * 在秒杀开启时输出秒杀接口的地址,否则输出系统时间跟秒杀地址 * * @param seckillId 秒杀商品Id * @return 根据对应的状态返回对应的状态实体 */ @Override public Exposer exportSeckillUrl(long seckillId) { // 根据秒杀的ID去查询是否存在这个商品 /* Seckill seckill = seckillMapper.queryById(seckillId); if (seckill == null) { logger.warn("查询不到这个秒杀产品的记录"); return new Exposer(false, seckillId); }*/ Seckill seckill = redisDao.getSeckill(seckillId); if (seckill == null) { // 访问数据库读取数据 seckill = seckillMapper.queryById(seckillId); if (seckill == null) { return new Exposer(false, seckillId); } else { // 放入redis redisDao.putSeckill(seckill); } } // 判断是否还没到秒杀时间或者是过了秒杀时间 LocalDateTime startTime = seckill.getStartTime(); LocalDateTime endTime = seckill.getEndTime(); LocalDateTime nowTime = LocalDateTime.now(); // 开始时间大于现在的时候说明没有开始秒杀活动 秒杀活动结束时间小于现在的时间说明秒杀已经结束了 /* if (!nowTime.isAfter(startTime)) { logger.info("现在的时间不在开始时间后面,未开启秒杀"); return new Exposer(false, seckillId, nowTime, startTime, endTime); } if (!nowTime.isBefore(endTime)) { logger.info("现在的时间不在结束的时间之前,可以进行秒杀"); return new Exposer(false, seckillId, nowTime, startTime, endTime); }*/ if (nowTime.isAfter(startTime) && nowTime.isBefore(endTime)) { //秒杀开启,返回秒杀商品的id,用给接口加密的md5 String md5 = getMd5(seckillId); return new Exposer(true, md5, seckillId); } return new Exposer(false, seckillId, nowTime, startTime, endTime); } private String getMd5(long seckillId) { String base = seckillId + "/" + salt; return DigestUtils.md5DigestAsHex(base.getBytes()); } /** * 执行秒杀操作,失败的,失败我们就抛出异常 * * @param seckillId 秒杀的商品ID * @param userPhone 手机号码 * @param md5 md5加密值 * @return 根据不同的结果返回不同的实体信息 */ @Override public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException { if (md5 == null || !md5.equals(getMd5(seckillId))) { logger.error("秒杀数据被篡改"); throw new SeckillException("seckill data rewrite"); } // 执行秒杀业务逻辑 LocalDateTime nowTIme = LocalDateTime.now(); try { //执行减库存操作 int reduceNumber = seckillMapper.reduceNumber(seckillId, nowTIme); if (reduceNumber <= 0) { logger.warn("没有更新数据库记录,说明秒杀结束"); throw new SeckillCloseException("seckill is closed"); } else { // 这里至少减少的数量不为0了,秒杀成功了就增加一个秒杀成功详细 int insertCount = successKilledMapper.insertSuccessKilled(seckillId, userPhone); // 查看是否被重复插入,即用户是否重复秒杀 if (insertCount <= 0) { throw new RepeatKillException("seckill repeated"); } else { // 秒杀成功了,返回那条插入成功秒杀的信息 SuccessKilled successKilled = successKilledMapper.queryByIdWithSeckill(seckillId, userPhone); // return new SeckillExecution(seckillId,1,"秒杀成功"); return new SeckillExecution(seckillId,1,"秒杀成功",successKilled); } } } catch (SeckillCloseException | RepeatKillException e1) { throw e1; } catch (Exception e) { logger.error(e.getMessage(), e); // 把编译期异常转换为运行时异常 throw new SeckillException("seckill inner error : " + e.getMessage()); } }
在这里我们捕获了运行时异常,这样做的原因就是Spring
的事物默认就是发生了RuntimeException
才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功
,秒杀失败
等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在con.suny
包下建立enum
包,专门放置枚举类,然后再建立SeckillStatEnum
枚举类:
/** * 常量枚举类 * Created by 孙 */ public enum SeckillStatEnum { SUCCESS(1, "秒杀成功"), END(0, "秒杀结束"), REPEAT_KILL(-1, "重复秒杀"), INNER_ERROR(-2, "系统异常"), DATE_REWRITE(-3, "数据篡改"); private int state; private String info; SeckillStatEnum() { } 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 statEnum : values()) { if (statEnum.getState() == index) { return statEnum; } } return null; } }
既然把这些改成了枚举,那么在SeckillServiceImpl
类中的executeSeckill
方法中成功秒杀的返回值就应该修改为
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);
改了这里以后会发现会报错,因为在实体类那边构造函数可不是这样的,然后修改SeckillExecution
类的构造函数,把state
跟stateInfo
的值设置从构造函数里面的SeckillStatEnum
中取出值来设置:
/* 秒杀成功返回的实体 */ 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(); }
首先在resources/spring
下建立applicationContext-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" 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"> <!--配置自动扫描service包下的注解,在这里配置了自动扫描后,com.suny.service包下所有带有@Service注解的类都会被加入Spring容器中--> <context:component-scan base-package="com.suny.service"/> <!--配置事物,这里时使用基于注解的事物--> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入数据库连接池--> <property name="dataSource" ref="dataSource"/> </bean> <!--开启基于注解的申明式事物--> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>
在这里开启了基于注解的事物,常见的事物操作有以下几种方法
方法定义
,接口定义
,类定义
,public方法上
,但是不能注解在private
,final
,static
等方法上,因为Spring的事物管理默认是使用Cglib动态代理的:序号 | 动态代理策略 | 不能被事物增强的方法 |
---|---|---|
1 | 基于接口的动态代理 | 出了public以外的所有方法,并且 public static 的方法也不能被增强 |
2 | 基于Cglib的动态代理 | private,static,final的方法 |
然后你要在Service
类上添加注解@Service
,不用在接口上添加注解:
@Service public class SeckillServiceImpl implements SeckillService
既然已经开启了基于注解的事物,那我们就去需要被事物的方法上加个注解@Transactional
吧:
@Transactional @Override public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException
写测试类,我这里的测试类名为SeckillServiceImplTest
:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration({"classpath:spring/applicationContext-dao.xml", "classpath:spring/applicationContext-service.xml"}) public class SeckillServiceImplTest { private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SeckillService seckillService; @Test public void getSeckillList() throws Exception { List<Seckill> seckillList = seckillService.getSeckillList(); logger.info(seckillList.toString()); System.out.println(seckillList.toString()); } @Test public void getById() throws Exception { long seckillId = 1000; Seckill byId = seckillService.getById(seckillId); System.out.println(byId.toString()); } @Test public void exportSeckillUrl() throws Exception { long seckillId = 1000; Exposer exposer = seckillService.exportSeckillUrl(seckillId); System.out.println(exposer.toString()); } @Test public void executeSeckill() throws Exception { long seckillId = 1000; Exposer exposer = seckillService.exportSeckillUrl(seckillId); if (exposer.isExposed()) { long userPhone = 12222222222L; String md5 = "bf204e2683e7452aa7db1a50b5713bae"; try { SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5); System.out.println(seckillExecution.toString()); } catch (SeckillCloseException | RepeatKillException e) { e.printStackTrace(); } } else { System.out.println("秒杀未开启"); } } @Test public void executeSeckillProcedureTest() { long seckillId = 1001; long phone = 1368011101; Exposer exposer = seckillService.exportSeckillUrl(seckillId); if (exposer.isExposed()) { String md5 = exposer.getMd5(); SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, phone, md5); System.out.println(execution.getStateInfo()); } } }
测试的话如果每个方法测试都通过就说明通过,如果报错了话就仔细看下哪一步错了检查下
本文分享自微信公众号 - Java团长(javatuanzhang)
原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。
原始发表时间:2018-12-12
本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。
我来说两句