開始Service層的編碼之前谒亦,我們首先需要進行Dao層編碼之后的思考:在Dao層我們只完成了針對表的相關操作包括寫了接口方法和映射文件中的sql語句竭宰,并沒有編寫邏輯的代碼,例如對多個Dao層方法的拼接份招,當我們用戶成功秒殺商品時我們需要進行商品的減庫存操作(調用SeckillDao接口)和增加用戶明細(調用SuccessKilledDao接口)切揭,這些邏輯我們都需要在Service層完成。這也是一些初學者容易出現(xiàn)的錯誤锁摔,他們喜歡在Dao層進行邏輯的編寫廓旬,其實Dao就是數(shù)據(jù)訪問的縮寫,它只進行數(shù)據(jù)的訪問操作谐腰,接下來我們便進行Service層代碼的編寫孕豹。
1.秒殺Service接口設計
在cn.codingxiaxw包下創(chuàng)建一個service包用于存放我們的Service接口和其實現(xiàn)類,創(chuàng)建一個exception包用于存放service層出現(xiàn)的異常例如重復秒殺商品異常怔蚌、秒殺已關閉等異常巩步,一個dto包作為傳輸層,dto和entity的區(qū)別在于:entity用于業(yè)務數(shù)據(jù)的封裝,而dto用于完成web和service層的數(shù)據(jù)傳遞桦踊。
首先創(chuàng)建我們Service接口椅野,里面的方法應該是按"使用者"(程序員)的角度去設計,SeckillService.java,代碼如下:
public interface SeckillService {
/**
* 查詢全部的秒殺記錄
* @return
*/
List<Seckill> getSeckillList();
/**
*查詢單個秒殺記錄
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
//再往下竟闪,是我們最重要的行為的一些接口
/**
* 在秒殺開啟時輸出秒殺接口的地址离福,否則輸出系統(tǒng)時間和秒殺時間
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 執(zhí)行秒殺操作,有可能失敗炼蛤,有可能成功妖爷,所以要拋出我們允許的異常
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
SeckillExecution executeSeckill(long seckillId,long userPhone,String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
}
該接口中前面兩個方法返回的都是跟我們業(yè)務相關的對象,而后兩個方法返回的對象與業(yè)務不相關理朋,這兩個對象我們用于封裝service和web層傳遞的數(shù)據(jù)絮识,方法的作用我們已在注釋中給出。相應在的dto包中創(chuàng)建Exposer.java嗽上,用于封裝秒殺的地址信息次舌,各個屬性的作用在代碼中已給出注釋,代碼如下:
/**
* Created by codingBoy on 16/11/27.
* 暴露秒殺地址(接口)DTO
*/
public class Exposer {
//是否開啟秒殺
private boolean exposed;
//對秒殺地址加密措施
private String md5;
//id為seckillId的商品的秒殺地址
private long seckillId;
//系統(tǒng)當前時間(毫秒)
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.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 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兽愤,用于判斷秒殺是否成功彼念,成功就返回秒殺成功的所有信息(包括秒殺的商品id、秒殺成功狀態(tài)浅萧、成功信息逐沙、用戶明細),失敗就拋出一個我們允許的異常(重復秒殺異常洼畅、秒殺結束異常),代碼如下:
/**
* 封裝執(zhí)行秒殺后的結果:是否秒殺成功
* Created by codingBoy on 16/11/27.
*/
public class SeckillExecution {
private long seckillId;
//秒殺執(zhí)行結果的狀態(tài)
private int state;
//狀態(tài)的明文標識
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;
}
}
然后需要創(chuàng)建我們在秒殺業(yè)務過程中允許的異常,重復秒殺異常RepeatKillException.java:
/**
* 重復秒殺異常土思,是一個運行期異常务热,不需要我們手動try catch
* Mysql只支持運行期異常的回滾操作
* Created by codingBoy on 16/11/27.
*/
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
秒殺關閉異常SeckillCloseException.java:
/**
* 秒殺關閉異常,當秒殺結束時用戶還要進行秒殺就會出現(xiàn)這個異常
* Created by codingBoy on 16/11/27.
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
和一個異常包含與秒殺業(yè)務所有出現(xiàn)的異常SeckillException.java:
/**
* 秒殺相關的所有業(yè)務異常
* Created by codingBoy on 16/11/27.
*/
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
到此己儒,接口的工作便完成,接下來進行接口實現(xiàn)類的編碼工作捆毫。
2.秒殺Service接口的實現(xiàn)
在service包下創(chuàng)建impl包存放它的實現(xiàn)類闪湾,SeckillServiceImpl.java,內容如下:
public class SeckillServiceImpl implements SeckillService
{
//日志對象
private Logger logger= LoggerFactory.getLogger(this.getClass());
//加入一個混淆字符串(秒殺接口)的salt绩卤,為了我避免用戶猜出我們的md5值途样,值任意給,越復雜越好
private final String salt="shsdssljdd'l.";
//注入Service依賴
@Autowired //@Resource
private SeckillDao seckillDao;
@Autowired //@Resource
private SuccessKilledDao successKilledDao;
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) //說明查不到這個秒殺產(chǎn)品的記錄
{
return new Exposer(false,seckillId);
}
//若是秒殺未開啟
Date startTime=seckill.getStartTime();
Date endTime=seckill.getEndTime();
//系統(tǒng)當前時間
Date nowTime=new Date();
if (startTime.getTime()>nowTime.getTime() || endTime.getTime()<nowTime.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;
}
//秒殺是否成功,成功:減庫存凛驮,增加明細裆站;失敗:拋出異常,事務回滾
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");//秒殺數(shù)據(jù)被重寫了
}
//執(zhí)行秒殺邏輯:減庫存+增加購買明細
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 new SeckillExecution(seckillId,1,"秒殺成功",successKilled);
代碼中肩袍,我們返回的state和stateInfo參數(shù)信息應該是輸出給前端的杭棵,但是我們不想在我們的return代碼中硬編碼這兩個參數(shù),所以我們應該考慮用枚舉的方式將這些常量封裝起來氛赐,在cn.codingxiaxw包下新建一個枚舉包enums魂爪,創(chuàng)建一個枚舉類型SeckillStatEnum.java,內容如下:
public enum SeckillStatEnum {
SUCCESS(1,"秒殺成功"),
END(0,"秒殺結束"),
REPEAT_KILL(-1,"重復秒殺"),
INNER_ERROR(-2,"系統(tǒng)異常"),
DATE_REWRITE(-3,"數(shù)據(jù)篡改");
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;
}
}
然后修改執(zhí)行秒殺操作的非業(yè)務類SeckillExecution.java里面涉及到state和stateInfo參數(shù)的構造方法:
//秒殺成功返回所有信息
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();
}
然后便可修改實現(xiàn)類方法中的返回語句為:return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);
艰管,保證了一些常用常量數(shù)據(jù)被封裝在枚舉類型里甫窟。
目前為止我們Service的實現(xiàn)全部完成,接下來要將Service交給Spring的容器托管蛙婴,進行一些配置粗井。
3.使用Spring托管Service依賴配置
在spring包下創(chuàng)建一個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.xsd">
<!--掃描service包下所有使用注解的類型-->
<context:component-scan base-package="cn.codingxiaxw.service"/>
</beans>
然后采用注解的方式將Service的實現(xiàn)類加入到Spring IOC容器中:
//@Component @Service @Dao @Controller
@Service
public class SeckillServiceImpl implements SeckillService
下面我們來運用Spring的聲明式事務對我們項目中的事務進行管理街图。
4.使用Spring聲明式事務配置
聲明式事務的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空間浇衬,這種配置的好處就是一次配置永久生效。3.注解@Transactional的方式餐济。在實際開發(fā)中耘擂,建議使用第三種對我們的事務進行控制,優(yōu)點見下面代碼中的注釋絮姆。下面讓我們來配置聲明式事務醉冤,在spring-service.xml中添加對事務的配置:
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入數(shù)據(jù)庫連接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置基于注解的聲明式事務
默認使用注解來管理事務行為-->
<tx:annotation-driven transaction-manager="transactionManager"/>
然后在Service實現(xiàn)類的方法中,在需要進行事務聲明的方法上加上事務的注解:
//秒殺是否成功篙悯,成功:減庫存蚁阳,增加明細;失敗:拋出異常鸽照,事務回滾
@Transactional
/**
* 使用注解控制事務方法的優(yōu)點:
* 1.開發(fā)團隊達成一致約定螺捐,明確標注事務方法的編程風格
* 2.保證事務方法的執(zhí)行時間盡可能短,不要穿插其他網(wǎng)絡操作RPC/HTTP請求或者剝離到事務方法外部
* 3.不是所有的方法都需要事務矮燎,如只有一條修改操作定血、只讀操作不要事務控制
*/
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, RepeatKillException, SeckillCloseException {}
下面針對我們之前做的業(yè)務實現(xiàn)類來做集成測試。
5.使用集成測試Service邏輯
在SeckillService接口中使用IDEA快捷鍵shift+command+T
诞外,快速生成junit測試類澜沟。Service實現(xiàn)類中前面兩個方法很好實現(xiàn),獲取列表或者列表中的一個商品的信息即可峡谊,測試如下:
@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 {
List<Seckill> seckills=seckillService.getSeckillList();
System.out.println(seckills);
}
@Test
public void getById() throws Exception {
long seckillId=1000;
Seckill seckill=seckillService.getById(seckillId);
System.out.println(seckill);
}
}
重點就是exportSeckillUrl()
方法和executeSeckill()
方法的測試茫虽,接下來我們進行exportSeckillUrl()
方法的測試刊苍,如下:
@Test
public void exportSeckillUrl() throws Exception {
long seckillId=1000;
Exposer exposer=seckillService.exportSeckillUrl(seckillId);
System.out.println(exposer);
}
控制臺中輸入如下信息:
Exposer{exposed=false, md5='null', seckillId=1000, now=1480322072410, start=1451577600000, end=1451664000000}
沒有給我們返回id為1000的商品秒殺地址,是因為我們當前的時間并不在秒殺時間開啟之內席噩,所以該商品還沒有開啟班缰。需要修改數(shù)據(jù)庫中該商品秒殺活動的時間在我們測試時的當前時間之內,然后再進行該方法的測試悼枢,控制臺中輸出如下信息:
Exposer{exposed=true, md5='bf204e2683e7452aa7db1a50b5713bae', seckillId=1000, now=0, start=0, end=0}
可知開啟了id為1000的商品的秒殺埠忘,并給我們輸出了該商品的秒殺地址。而第四個方法的測試就需要傳入該地址讓用戶得到才能判斷該用戶是否秒殺到該地址的商品馒索,然后進行第四個方法的測試,如下:
@Test
public void executeSeckill() throws Exception {
long seckillId=1000;
long userPhone=13476191876L;
String md5="bf204e2683e7452aa7db1a50b5713bae";
SeckillExecution seckillExecution=seckillService.executeSeckill(seckillId,userPhone,md5);
System.out.println(seckillExecution);
}
控制臺輸出信息:
SeckillExecution{seckillId=1000, state=1, stateInfo='秒殺成功', successKilled=SuccessKilled{seckillId=1000, userPhone=13476191876, state=0, createTime=Mon Nov 28 16:45:38 CST 2016}}
證明電話為13476191876的用戶成功秒殺到了該商品莹妒,查看數(shù)據(jù)庫,該用戶秒殺商品的明細信息已經(jīng)被插入明細表绰上,說明我們的業(yè)務邏輯沒有問題旨怠。但其實這樣寫測試方法還有點問題,此時再次執(zhí)行該方法蜈块,控制臺報錯鉴腻,因為用戶重復秒殺了。我們應該在該測試方法中添加try catch,將程序允許的異常包起來而不去向上拋給junit百揭,更改測試代碼如下:
@Test
public void executeSeckill() throws Exception {
long seckillId=1000;
long userPhone=13476191876L;
String md5="bf204e2683e7452aa7db1a50b5713bae";
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
System.out.println(seckillExecution);
}catch (RepeatKillException e)
{
e.printStackTrace();
}catch (SeckillCloseException e1)
{
e1.printStackTrace();
}
}
這樣再測試該方法爽哎,junit便不會再在控制臺中報錯浪默,而是認為這是我們系統(tǒng)允許出現(xiàn)的異常呀枢。由上分析可知,第四個方法只有拿到了第三個方法暴露的秒殺商品的地址后才能進行測試柿顶,也就是說只有在第三個方法運行后才能運行測試第四個方法祈秕,而實際開發(fā)中我們不是這樣的渺贤,需要將第三個測試方法和第四個方法合并到一個方法從而組成一個完整的邏輯流程:
@Test//完整邏輯代碼測試,注意可重復執(zhí)行
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);
}
}
運行該測試類请毛,控制臺成功輸出信息志鞍,庫存會減少,明細表也會增加內容获印。重復執(zhí)行述雾,控制臺不會報錯,只是會拋出一個允許的重復秒殺異常兼丰。
目前為止,Dao層和Service層的集成測試我們都已經(jīng)完成唆缴,接下來進行Web層的開發(fā)編碼工作鳍征,請查看我的下篇文章Java高并發(fā)秒殺API之Web層開發(fā)。
6.聯(lián)系
If you have some questions after you see this article,you can tell your doubts in the comments area or you can find some info by clicking these links.