SSM框架学习之高并发秒杀业务--笔记3-- Service层

时间:2022-08-14 04:58:59

上一节中已经包DAO层编写完成了,所谓的DAO层就是所有和数据访问的部分都应该放在这个层里,它负责与数据库打交道。对于一个web项目来说,大概由这几部分组成:

1. 前台的显示层。

2. 分发处理请求的web层,这一层来用一些MVC框架。

3. 负责业务逻辑处理的Service层。

4. 负责与数据库交互的DAO层

这样有利于代码的分离,以前上课时各种听不懂,但书上有句话记得很清楚,那就是代码的设计原则应该是“低耦合,高内聚”,MVC框架的设计正好体现了这个原则。废话不多说,开始编码。

第一步:Service接口的设计与实现

在org.seckill目录下新建service包用来存放我们要编写的service接口和实现类,新建exception包用来存放这个项目中我们自己定义业务的异常,新建dto包来存放业务数据传输对象。虽然老师这么说,但是后两个包的具体用处还不是很理解,先继续往下做,做完后再来分析。目录结构

SSM框架学习之高并发秒杀业务--笔记3-- Service层

做到这里我又卡壳了,不知道该怎么继续,不对照视频的话还是有点生疏。那么就静下心好好思考下吧。对于一个项目来说,主要的是完成各种用户所需的功能,这些功能对应到程序中的就是各种接口和函数的定义,有的方法要有数据交互即有的方法要去存取修改数据库。对于这些功能来说,都是在处理用户的各种请求,服务器收到请求后,所做的第一件事情便是判断该怎么处理这个请求呢?该用什么样的方法来实现用户这个请求所需要的功能呢?那么这一步就是一个"判断—分发"的过程,使请求转向服务器后端相应的方法去处理,这个功能主要是有MVC框架来完成。那么请求被转到后端相应的方法去处理,对吧?那么对于请求的处理,即是一个业务功能的实现,一个方法的实现的话,本质上就是给一个方法(函数)一些东西(参数),然后再返回执行后的结果。对于方法来说可以分为两部分:1. 业务逻辑 2. 数据交换。 也就是说方法在细分成这两部分的话 可以得到     “方法—业务逻辑   方法—数据交换”  这两个,那么我们可以将其相分离,业务逻辑的归业务层,数据交换的归DAO层,这样可以很好的分离代码,简而言之就是权责分明的部门体系,各管各的,需要用到别的部门提供的服务时把这个部门的几个相关的人叫过来就行了。

以上是作为菜鸟的我的一些理解,那么现在再想想下部该怎么做呢?有上面思考可以知道,无论是DAO还是Service层的设计,围绕的是方法(接口)的设计,因为这就是项目要实现的功能,当你不知道要怎么往下做的时候,可以回想下自己到底想要做什么?我想要实现什么样的功能呢?

在service包下建SeckillService接口,用来定义和Seckill(即秒杀商品)有关的业务操作(方法)。

SeckillService接口

 package org.seckill.service;

 import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException; import java.util.List; /**
* 业务接口:站在"使用者"角度设计接口
* 三个方面:方法定一粒度,参数,返回类型/异常
* Created by yuxue on 2016/10/15.
*/
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
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKillException,SeckillCloseException; /**
* 执行秒杀操作by 存储过程
* @param seckillId
* @param userPhone
* @param md5
*/
SeckillExecution executeSeckillProdure(long seckillId, long userPhone, String md5);
}

关于这段的代码的说明:

1.关于exportSeckillUrl(long seckillId)这个方法,这个方法的功能是:当用户点商品详情页面想要秒杀商品时,如果在秒杀时间范围之内就显示就输出秒杀接口地址让前端显示,否则输出系统时间和秒杀时间。这里中要的是它的返回值,是个Exposer类的实例,Exposer是定义在dto包下的:

业务数据传输对象Exposer

 package org.seckill.dto;

 /**暴露秒杀地址DTO
* Created by yuxue on 2016/10/15.
*/
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 seckillId,long now, long start, long end) {
this.exposed = exposed;
this.now = now;
this.seckillId=seckillId;
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 +
'}';
}
}

前面说了,dto包是用来存放业务数据传输对象的,其中大部分与业务不相关,只是service返回的数据的封装。对于exportSeckillUrl(long seckillId)这个方法,向前端输出秒杀接口地址或者系统时间和这个商品的秒杀开始/结束时间,这里麻烦的一点便在于此,对于一个业务方法来说,他的返回可能比较复杂,会有多种数据成分,那么这时候需要将这些数据封装成业务数据传输对象,这便是dto包里面的类的作用。对于这里的业务数据传输对象Exposer,封装的信息有:1. 商品是否开启标志位。2.加密字段MD5,这个字段由商品的id通过MD5加密算法生成,目的是防止数据被用户使用的第三方工具篡改以及直接拼出秒杀地址。3. 商品的id,系统时间以及秒杀开始,结束时间。对应于这些字段定义了3个构造方法,对应不同的构造情况,因为可以秒杀以及不可以秒杀这两种情况下需要向前端提供的数据不一样。

接下来要分析的是executeSeckill(long seckillId, long userPhone, String md5)这个方法,这个方法负责具体的执行秒杀操作,返回的类型也是个业务数据传输对象SeckillExecution,这里为什么不直接返回个布尔型数据来表示执行秒杀操作成功与否呢?为什么要用个复杂的封装类型呢?我自己理解是:对于一个方法的返回值的设计,你要看是谁调用了这个方法。在web项目中,调用Service层的业务的是web层,这层的具体任务就根据前端的请求调用相应的service来处理,处理完之后从service层中拿处理结果数据给前端显示。对于执行秒杀操作这个业务方法,他的具体方法负责执行秒杀操作,对于调用它的web层,应给他提供执行秒杀操作后的相关信息,好让它传给前端显示,所以这里如果只提供一个表示执行秒杀操作成功与否的布尔型值的话是肯定不够的。

SeckillExecution

 package org.seckill.dto;

 import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStatEnum; /**封装秒杀后执行的结果
* Created by yuxue on 2016/10/15.
*/
public class SeckillExecution { private long seckillId; //秒杀执行结果状态
private int state; //状态展示
private String stateInfo; //秒杀成功对象
private SuccessKilled successKilled; public SeckillExecution(long seckillId, SeckillStatEnum statEnum, SuccessKilled successKilled) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateInfo();
this.successKilled = successKilled;
} public SeckillExecution(long seckillId, SeckillStatEnum statEnum) {
this.seckillId = seckillId;
this.state = statEnum.getState();
this.stateInfo = statEnum.getStateInfo();
} @Override
public String toString() {
return "SeckillExecution{" +
"seckillId=" + seckillId +
", state=" + state +
", stateInfo='" + stateInfo + '\'' +
", successKilled=" + successKilled +
'}';
} 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;
}
}

还有一点是这里把执行秒杀操作后的相关信息单独封装成对象,里面有各个秒杀商品的秒杀执行结果状态,状态展示等信息,这样前端在显示商品秒杀信息的时候直接从这里去取就好了,不必再通过其他的方法去service层一个一个字段的取,使得数据之间耦合度低。

SeckillExecution里有不同的构造方法,这是为了对应不同的情况,比如说如果商品秒杀成功了,那么则需要秒杀商品的id,状态展示,以及秒杀成功对象这三个字段,如果秒杀失败,则只需要秒杀商品的id,状态展示便可以了。

这里的秒杀执行结果状态使用枚举类演示的,开发过程有个原则就是如果程序中用到了一些常量,那么最好将这些常量放到一处,在程序中引用这些常量即可,这样便于修改。

在在org.seckill目录下新建enums包,存放我们建的枚举类。

枚举类SeckillStatEnum

 package org.seckill.enums;

 /**
* 使用枚举表述常量数据
* Created by yuxue on 2016/10/15.
*/
public enum SeckillStatEnum {
//枚举的使用
SUCCESS(1,"秒杀成功"),
END(0,"秒杀结束"),
REPEAT_KILL(-1,"重复秒杀"),
INNER_ERROR(-2,"系统异常"),
DATA_REWRITE(-3,"数据篡改"); private int state; private String stateInfo; SeckillStatEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
} public int getState() {
return state;
} public String getStateInfo() {
return stateInfo;
} public static SeckillStatEnum stateOf(int index){
for(SeckillStatEnum state:values()){
if(state.getState()==index){
return state;
}
}
return null;
}
}

接口和枚举定义完了,现在到了接口的实现

SeckillServiceImpl

 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.SeckillStateEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.SeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils; import java.util.Date;
import java.util.List; /**
* Created by yuxue on 2016/11/7.
*/
@Service
public class SeckillServiceImpl implements SeckillService{
private Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired
SeckillDao seckillDao;
@Autowired
SuccessKilledDao successKilledDao; //md5盐值字符串,用于混淆MD5
private String salty="sfsafas((888__```"; public List<Seckill> getSeckillList( ) {
return seckillDao.queryAllSeckill(0,4);
} public Seckill querySeckill(int seckillId) {
return seckillDao.queryById(seckillId);
} //暴露秒杀接口地址的实现
public Exposer exportSeckillUrl(long seckillId) {
Seckill seckill=seckillDao.queryById(seckillId);
if(seckill==null){
return new Exposer(false,seckillId);
}
Date now=new Date();
Date start=seckill.getStartTime();
Date end=seckill.getEndTime();
if(now.getTime()<start.getTime()||now.getTime()>end.getTime()){
return new Exposer(false,seckillId,now,start,end);
}
String md5=getMD5(seckillId);
return new Exposer(true,md5,seckillId);
} //根据秒杀商品id来生成MD5密钥
private String getMD5(long seckillId){
//拼接规则
String base=seckillId+"/"+salty;
String MD5= DigestUtils.md5DigestAsHex(base.getBytes());
return MD5;
} //执行秒杀方法的实现
@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");
}
Date now=new Date();
try {
int count = seckillDao.reduceNumber(seckillId, now);
if (count <= 0) {
throw new SeckillCloseException("秒杀关闭");
} else {
int update = successKilledDao.insertSuccessSeckilled(seckillId, userPhone);
if (update <= 0) {
throw new RepeatKillException("重复秒杀");
}else{
SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS,successKilled);
}
}
}catch (SeckillCloseException e){
throw e;
}catch (RepeatKillException e){
throw e;
}catch (Exception e){
logger.error(e.getMessage(),e);
throw new SeckillException("Seckill inner error"+e.getMessage());
}
}
}

分析:

1. private Logger logger = LoggerFactory.getLogger(this.getClass()); 使用日志。

2. String MD5= DigestUtils.md5DigestAsHex(base.getBytes());使用java提供的API来生成MD5密钥

3. 定义了3种异常SeckillException, RepeatKillException, SeckillCloseException分别是秒杀异常,重复秒杀,秒杀关闭。注意这里并不是程序的运行异常,是我们自定义的业务异常,这是一种思路:将业务中可能出现的我们不允许的部分如重复秒杀

等作为异常抛出再对应与相对的异常捕捉,分类处理。这个项目里首先是将所有的业务异常定义为SeckillException,在以它为父类细化为RepeatKillException和SeckillCloseException这两个异常。

4. @Transactional注解将执行秒杀这个方法声明为一个事务。

在org.seckill目录下新建exception包用来存放我们的自定义的异常

SeckillException

 package org.seckill.exception;

 /**
* 秒杀相关业务异常
* Created by yuxue on 2016/10/15.
*/
public class SeckillException extends RuntimeException{
public SeckillException(String message) {
super(message);
} public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}

注意这里要继承RuntimeException,因为对于java事务来说只有运行时异常时它才会回滚。

RepeatKillException

 package org.seckill.exception;

 /**重复秒杀异常(运行期异常)
*
*
* Spring事务只会接收运行期异常并回滚
* Created by yuxue on 2016/10/15.
*/
public class RepeatKillException extends SeckillException{ public RepeatKillException(String message){
super(message);
} public RepeatKillException(String message, Throwable cause){
super(message,cause);
}

SeckillCloseException

 package org.seckill.exception;

 /**
* Created by yuxue on 2016/10/15.
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
} public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}

第二步:基于Spring托管Service实现类,使用声明式事务

让Spring的IOC容器托管Service实现类,主要的是一些配置工作。通过对象工厂和依赖管理来达到一致性的访问接口。

在resources目录的spring文件目录下新建spring-service.xml配置文件,区别于spring-dao.xml,表明这个配置文件是用来配置service层的。

spring-service.xml

 <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-3.0.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-3.0.xsd">
<!--扫描service包下所有使用注解的类型-->
<context:component-scan base-package="org.seckill.service"/> <!--配置事务管理-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="datasource"/>
</bean> <!--配置基于注解的声明式事务
默认使用注解来管理事务行为
-->
<tx:annotation-driven transaction-manager="transactionManager"/> </beans>

使用注解控制事务方法的优点以及注意的事项:

1. 开发团队一致的约定。

2. 保证事务方法的执行时间经可能的短,不要穿插其他网络操作RPC/HTTP请求或者剥离到事务方法外部,使得这个事务方法是个比较干净的对数据库的操作。

3. 不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制。

第三步: Service层集成测试

下面是测试用例SeckillServiceTest

 package org.seckill.service;

 import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
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 yuxue on 2016/10/15.
*/
@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() throws Exception {
List<Seckill> list=seckillService.getSeckillList();
logger.info("list={}",list);//这里的{}是个占位符
} @Test
public void getById() throws Exception {
long id=1004;
Seckill seckill=seckillService.getById(id);
logger.info("seckill={}",seckill);
} //集成测试代码完整逻辑,注意可重复执行
@Test
public void exportSeckillLogic() throws Exception {
long id=1005;
Exposer exposer=seckillService.exportSeckillUrl(id);
if(exposer.isExposed()) {
logger.info("exposer={}", exposer);
long phone=243242343L;
String md5=exposer.getMd5();
try{
SeckillExecution seckillExecution=seckillService.executeSeckill(id,phone,md5);
logger.info("result={}",seckillExecution);
}catch (RepeatKillException e){
logger.error(e.getMessage());
}catch(SeckillCloseException e){
logger.error(e.getMessage());
}
}else{
logger.warn("exposer={}",exposer);
}
}
}

关于遇到的问题:

在自动注入seckillService时我把接口写成了实现类SeckillServiceImpl结果报错了,即spring在这里要注入接口,而注入接口的实现类就会报错,如果在SeckillServiceImpl中将implements SeckillService删除的话便能执行通过。网上搜了下这其中的原因,总结如下:

1. Spring的依赖注入功能使用Spring的动态代理机制来实现,而spring动态代理功能的实现是基于Java的动态代理机制的,jdk规定动态代理必须用接口,反而类注入则要通过cglib进行动态代理。

2. 为什么在SeckillServiceImpl中将implements SeckillService删除的话便能执行通过?这里或许要涉及到在有接口和无接口情况下,Spring动态代理机制执行的不同。于是自己尝试了下,其结果:

(1)如果有接口,则注入后类型是

SSM框架学习之高并发秒杀业务--笔记3-- Service层

说明是由Java的动态代理机制Proxy.newProxyInstance()方法创建一个代理对象来代理指定的类,个方法有三个参数:newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h),第一个参数为要代理的类,第二     个参数为这个类的接口(。。),第三个暂且不管,说明java实现动态代理的时候要求必须有接口类。

(2)如果没有用接口,即在SeckillServiceImpl中将implements SeckillService删除然后用private SeckillServiceImpl这种方式注入的话,则注入后类型是

SSM框架学习之高并发秒杀业务--笔记3-- Service层

可见如果是实现类的方式的话,Spring的动态代理机制是使用cglib进行动态代理的,关于这其中具体的技术细节还要日后仔细分析才行。

Service层的学习总结:

这节真的学到了好多好多啊,照着视频敲的确没什么问题,自己手打就会各种出错,再次强调:学编程一定要自己手敲代码,这样才能发现自己不懂的地方。这节中的一些分析是基于我个人的理解,因为我是个菜鸟所以可能有些错误,所以希望技术大神发现的话能指点指点,非常感谢。其中的一些技术细节如动态代理什么的以后还要写写博文来仔细分析分析下,多敲敲代码,多思考,这样才能提高自己的技术水平。下一节开始web层的设计与开发。