開始Service層的編碼以前,咱們首先須要進行Dao層編碼以後的思考:在Dao層咱們只完成了針對表的相關操做包括寫了接口方法和映射文件中的sql語句,並無編寫邏輯的代碼,例如對多個Dao層方法的拼接,當咱們用戶成功秒殺商品時咱們須要進行商品的減庫存操做(調用SeckillDao接口)和增長用戶明細(調用SuccessKilledDao接口),這些邏輯咱們都須要在Service層完成。這也是一些初學者容易出現的錯誤,他們喜歡在Dao層進行邏輯的編寫,其實Dao就是數據訪問的縮寫,它只進行數據的訪問操做,接下來咱們便進行Service層代碼的編寫。html
在org.myseckill下建立一個service包用於存放咱們的Service接口和其實現類,建立一個exception包用於存放service層出現的異常例如重複秒殺商品異常、秒殺已關閉等容許出現的異常,一個dto包做爲數據傳輸層,dto和entity的區別在於:entity用於業務數據的封裝,而dto關注的是web和service層的數據傳遞。前端
首先建立咱們Service接口,裏面的方法應該是按」使用者」的角度去設計,SeckillService.java,代碼以下:java
/** * 該接口中前面兩個方法返回的都是跟咱們業務相關的對象,然後兩個方法返回的對象與業務不相關,這兩個對象咱們用於封裝service和web層傳遞的數據 * 業務接口,站在「使用者」的角度設計接口,而不是如何實現 * 三個方面:方法定義粒度,參數(越簡練越直接越好),返回類型(retrun 類型(要友好)/異常(有的業務容許拋出異常)) * @author TwoHeads * */
public interface SeckillService { /** * 查詢全部的秒殺記錄 * @return
*/ List<Seckill> getSeckillList(); /** *查詢單個秒殺記錄 * @param seckillId * @return
*/ Seckill getById(long seckillId); /** * 秒殺開啓時輸出秒殺接口地址, * 不然輸出系統時間和秒殺時間 * 防止用戶提早拼接出秒殺url經過插件進行秒殺 * @param seckillId */ Exposer exportSeckillUrl(long seckillId); /** * 執行秒殺操做,若是傳入的md5與內部的不相符,說明用戶的url被篡改了,此時拒絕執行秒殺 * 有可能失敗,有可能成功,因此要拋出咱們容許的異常 * @param seckillId * @param userPhone * @param md5 */ SeckillExecution executeSeckill(long seckillId,long userPhone,String md5) throws SeckillException,SeckillCloseException,RepeatKillException; }
相應在的dto包中建立Exposer.java,用於封裝秒殺的地址信息,代碼以下:web
/** * 暴露秒殺地址DTO(數據傳輸層) * @author TwoHeads * */
public class Exposer { //是否開啓秒殺
private boolean exposed; //對秒殺地址加密措施
private String md5; //id爲seckillId的商品的秒殺地址
private long seckillId; //系統當前時間(毫秒)
private long now; //秒殺的開啓時間
private long start; //秒殺的結束時間
private long end; /** * 不一樣的構造方法方便對象初始化 * @param exposed * @param md5 * @param seckillId */
public Exposer(boolean exposed, String md5, long seckillId) { super(); this.exposed = exposed; this.md5 = md5; this.seckillId = seckillId; } public Exposer(long now, long start, long end) { super(); this.now = now; this.start = start; this.end = end; } public Exposer(boolean exposed, long seckillId) { super(); 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:spring
/** * 封裝秒殺執行後的結果 * 用於判斷秒殺是否成功,成功就返回秒殺成功的全部信息(秒殺的商品id、秒殺成功狀態、成功信息、用戶明細), * 失敗就拋出一個咱們容許的異常(重複秒殺異常、秒殺結束異常) * @author TwoHeads * */
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; } }
而後須要建立咱們在秒殺業務過程當中容許的異常,重複秒殺異常RepeatKillException.java:sql
/** * 重複秒殺異常(運行期異常) * @author TwoHeads * */
public class RepeatKillException extends SeckillException{ public RepeatKillException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub
} public RepeatKillException(String message) { super(message); // TODO Auto-generated constructor stub
} }
秒殺關閉異常SeckillCloseException.java:數據庫
/** * 秒殺關閉異常(關閉了還執行秒殺) * @author TwoHeads * */
public class SeckillCloseException extends SeckillException{ public SeckillCloseException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub
} public SeckillCloseException(String message) { super(message); // TODO Auto-generated constructor stub
} }
和一個異常包含與秒殺業務全部出現的異常SeckillException.java:apache
public class SeckillException extends RuntimeException{ public SeckillException(String message, Throwable cause) { super(message, cause); // TODO Auto-generated constructor stub
} public SeckillException(String message) { super(message); // TODO Auto-generated constructor stub
} }
在service包下建立impl包存放它的實現類,SeckillServiceImpl.java,內容以下:編程
public class SeckillServiceImpl implements SeckillService{ //日誌對象slf4g
private Logger logger = LoggerFactory.getLogger(this.getClass()); private SeckillDao seckillDao; private SuccessKilledDao successKilledDao; //md5鹽值字符串,用於混淆md5
private final String slat = "asdfasvrg54mbesognoamg;s'afmaslgma"; @Override public List<Seckill> getSeckillList() { return seckillDao.queryAll(0, 4); } @Override public Seckill getById(long seckillId) { return seckillDao.queryById(seckillId); } @Override public Exposer exportSeckillUrl(long seckillId) { Seckill seckill = seckillDao.queryById(seckillId); if(seckill == null) { return new Exposer(false,seckillId); } //若是seckill不爲空,則拿到它的開始時間和結束時間
Date startTime = seckill.getStartTime(); Date endTime = seckill.getEndTime(); //系統當前時間
Date nowTime = new Date(); //Date類型要用getTime()獲取時間
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); //getMD5方法寫在下面
return new Exposer(true,md5,seckillId); } private String getMD5(long seckillId){ String base = seckillId + "/" + slat; //spring的工具包,用於生成md5
String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } @Override public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException { //將用戶傳來的md5與內部的md5比較
if(md5 == null || md5.equals(getMD5(seckillId)) == false) { throw new SeckillException("seckill data rewrite"); } //執行秒殺邏輯,減庫存+記錄購買行爲
Date nowDate = new Date(); try { // 減庫存
int updateCount = seckillDao.reduceNumber(seckillId, nowDate); if (updateCount <= 0) { // 沒有更新到記錄,秒殺結束。咱們不關心是庫存沒有了仍是秒殺時間已通過了,併發量很高的狀況下具體狀況很難預料,而用戶只關心秒殺成功與否
throw new SeckillCloseException("seckill is closed"); } else { // 減記錄成功,記錄購買行爲
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); // 惟一:seckillId,userPhone
if (insertCount <= 0) { // 說明出現主鍵衝突,插入失敗,發生了重複秒殺
throw new RepeatKillException("seckill repeated"); } else { // 秒殺成功
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()); } } }
上述代碼中return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, successKilled);原本是return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);
網絡
咱們返回的state和stateInfo參數信息應該是輸出給前端的,可是咱們不想在咱們的return代碼中硬編碼這兩個參數,因此咱們應該考慮用枚舉的方式將這些常量封裝起來,在org.myseckill包下新建一個枚舉包enums,建立一個枚舉類型SeckillStatEnum.java,內容以下:
/** * 使用枚舉表示常量數據字段 * 封裝state和stateInfo * @author TwoHeads * */
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; } }
而後修改執行秒殺操做的非業務類SeckillExecution.java裏面涉及到state和stateInfo參數的構造方法:
//不一樣的構造方法,秒殺成功返回全部信息
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(); }
使一些經常使用常量數據被封裝在枚舉類型裏。
目前Service的實現所有完成,接下來要將Service交給Spring的容器託管,進行一些配置。
第三種不經常使用
這也是大多數使用spring的方式
在spring包下建立一個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-4.1.xsd">
<!--掃描service包下全部使用註解的類型-->
<context:component-scan base-package="org.myseckill.service"></context:component-scan>
</beans>
而後採用註解的方式將Service的實現類加入到Spring IOC容器中:
//註解有 @Component @Service @Dao @Controller(web層),這裏已知是service層
@Service public class SeckillServiceImpl implements SeckillService{ //日誌對象slf4g
private Logger logger = LoggerFactory.getLogger(this.getClass()); //注入service的依賴
@Autowired private SeckillDao seckillDao; @Autowired private SuccessKilledDao successKilledDao;
聲明式事務的使用方式:1.早期使用的方式:ProxyFactoryBean+XMl.2.tx:advice+aop命名空間,這種配置的好處就是一次配置永久生效。3.註解@Transactional的方式。在實際開發中,建議使用第三種對咱們的事務進行控制
聲明式事務參看blog http://blog.csdn.net/bao19901210/article/details/41724355
spring支持編程式事務管理和聲明式事務管理兩種方式。
編程式事務管理使用TransactionTemplate或者直接使用底層的PlatformTransactionManager。對於編程式事務管理,spring推薦使用TransactionTemplate。
聲明式事務管理創建在AOP之上的。其本質是對方法先後進行攔截,而後在目標方法開始以前建立或者加入一個事務,在執行完目標方法以後根據執行狀況提交或者回滾事務。聲明式事務最大的優勢就是不須要經過編程的方式管理事務,這樣就不須要在業務邏輯代碼中摻瑣事務管理的代碼,只需在配置文件中作相關的事務規則聲明(或經過基於@Transactional註解的方式),即可以將事務規則應用到業務邏輯中。
顯然聲明式事務管理要優於編程式事務管理,這正是spring倡導的非侵入式的開發方式。聲明式事務管理使業務代碼不受污染,一個普通的POJO對象,只要加上註解就能夠得到徹底的事務支持。和編程式事務相比,聲明式事務惟一不足地方是,後者的最細粒度只能做用到方法級別,沒法作到像編程式事務那樣能夠做用到代碼塊級別。可是即使有這樣的需求,也存在不少變通的方法,好比,能夠將須要進行事務管理的代碼塊獨立爲方法等等。
事務隔離級別
隔離級別是指若干個併發的事務之間的隔離程度。TransactionDefinition 接口中定義了五個表示隔離級別的常量:
事務傳播行爲
所謂事務的傳播行爲是指,若是在開始當前事務以前,一個事務上下文已經存在,此時有若干選項能夠指定一個事務性方法的執行行爲。在TransactionDefinition定義中包括了以下幾個表示傳播行爲的常量:
配置聲明式事務,在spring-service.xml中添加對事務的配置:
<!--掃描service包下全部使用註解的類型-->
<context:component-scan base-package="org.myseckill.service"></context:component-scan>
<!-- 配置事務管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入數據庫鏈接池 -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- 配置基於屬性的聲明式事務 默認使用註解來管理事務行爲 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
而後在Service實現類的方法中,在須要進行事務聲明的方法上加上事務的註解:
@Override @Transactional /** * 使用註解控制事務方法的優勢: 1.開發團隊達成一致約定,明確標註事務方法的編程風格 * 2.保證事務方法的執行時間儘量短,不要穿插其餘網絡操做RPC/HTTP請求或者剝離到事務方法外部 * 3.不是全部的方法都須要事務,如只有一條修改操做、只讀操做不要事務控制 */
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, SeckillCloseException, RepeatKillException { //將用戶傳來的md5與內部的md5比較
if(md5 == null || md5.equals(getMD5(seckillId)) == false) { throw new SeckillException("seckill data rewrite"); } //執行秒殺邏輯,減庫存+記錄購買行爲
Date nowDate = new Date(); try { // 減庫存
int updateCount = seckillDao.reduceNumber(seckillId, nowDate); if (updateCount <= 0) { // 沒有更新到記錄,秒殺結束。咱們不關心是庫存沒有了仍是秒殺時間已通過了,併發量很高的狀況下具體狀況很難預料,而用戶只關心秒殺成功與否
throw new SeckillCloseException("seckill is closed"); } else { // 減記錄成功,記錄購買行爲
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone); // 惟一:seckillId,userPhone
if (insertCount <= 0) { // 說明出現主鍵衝突,插入失敗,發生了重複秒殺
throw new RepeatKillException("seckill repeated"); } else { // 秒殺成功
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()); } }
在resources下新建logback.xml
在logback官網https://logback.qos.ch/manual/configuration.html找到配置文件範例粘貼到logback.xml並加入xml頭
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<!-- encoders are assigned the type ch.qos.logback.classic.encoder.PatternLayoutEncoder by default -->
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="debug">
<appender-ref ref="STDOUT" />
</root>
</configuration>
生成測試類SeckillServiceTest
@RunWith(SpringJUnit4ClassRunner.class) //告訴junit spring的配置文件,要依賴於dao的配置因此2個都要加載
@ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"}) public class SeckillServiceTest { //日誌
private final Logger logger = LoggerFactory.getLogger(this.getClass()); //依賴注入,將SeckillService注入到測試類下
@Autowired private SeckillService seckillService; @Test public void testGetSeckillList() { List<Seckill> list = seckillService.getSeckillList(); logger.info("list={}",list); //把list放入佔位符{}中
} @Test public void testGetById() { long id = 1000; Seckill seckill = seckillService.getById(id); logger.info("seckill={}",seckill); } @Test public void testExportSeckillUrl() { long id = 1000; Exposer exposer = seckillService.exportSeckillUrl(id); logger.info("exposer={}",exposer); } // 輸出exposer=Exposer [exposed=true, // md5=07cde05fe83a6df7309eb56e727bf2fd, // seckillId=1000, // now=0, start=0, end=0]
@Test public void testExecuteSeckill() { long id = 1000; long phone = 17808315995L; String md5 = "07cde05fe83a6df7309eb56e727bf2fd"; //須要用到testExportSeckillUrl獲得的md5
try { SeckillExecution excution = seckillService.executeSeckill(id, phone, md5); logger.info("excution={}",excution); } catch (RepeatKillException e) { logger.error(e.getMessage()); }catch (SeckillCloseException e) { logger.error(e.getMessage()); } } }
測試testGetSeckillList()
輸出
13:18:03.704 [main] DEBUG o.myseckill.dao.SeckillDao.queryAll - <== Total: 4
13:18:03.713 [main] DEBUG org.mybatis.spring.SqlSessionUtils - Closing non transactional SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@6dd7b5a3] 13:18:03.715 [main] INFO o.m.service.SeckillServiceTest - list=[Seckill{seckillId=1000, name='1000元秒殺iphone6', number=100, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1001, name='800元秒殺ipad', number=200, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1002, name='6600元秒殺mac book pro', number=300, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}, Seckill{seckillId=1003, name='7000元秒殺iMac', number=400, startTime=Mon Jan 01 00:00:00 CST 2018, endTime=Tue Jan 02 00:00:00 CST 2018, createTime=Fri Dec 29 23:04:08 CST 2017}]
non transactional SqlSession說明不是在事務控制下
測試testExportSeckillUrl
13:25:36.078 [main] INFO o.m.service.SeckillServiceTest - exposer=Exposer [exposed=false, md5=null, seckillId=1000, now=1517030736078, start=1514736000000, end=1514822400000]
沒有給咱們返回id爲1000的商品秒殺地址,是由於咱們當前的時間並不在秒殺時間開啓以內,因此該商品尚未開啓。
須要修改數據庫中該商品秒殺活動的時間在咱們測試時的當前時間以內,而後再進行該方法的測試,控制檯中輸出以下信息:
13:33:54.040 [main] INFO o.m.service.SeckillServiceTest - exposer=Exposer [exposed=true, md5=07cde05fe83a6df7309eb56e727bf2fd, seckillId=1000, now=0, start=0, end=0]
可知開啓了id爲1000的商品的秒殺,並給咱們輸出了該商品的秒殺地址。
測試testExecuteSeckill,須要使用剛纔獲得的md5
控制檯輸出
13:49:34.228 [main] INFO o.m.service.SeckillServiceTest - excution=SeckillExecution [seckillId=1000, state=1, stateInfo=秒殺成功, successKilled=SuccessKilled{seckillId=1000, userPhone=17808315995, state=0, createTime=Sat Jan 27 13:49:33 CST 2018}]
查看數據庫,該用戶秒殺商品的明細信息已經被插入明細表,說明咱們的業務邏輯沒有問題。但其實這樣寫測試方法還有點問題,此時再次執行該方法,控制檯報錯,由於用戶重複秒殺了。咱們應該在該測試方法中添加try catch,將程序容許的異常包起來而不去向上拋給junit,更改測試代碼以下:
@Test public void testExecuteSeckill() { long id = 1000; long phone = 17808315995L; String md5 = "07cde05fe83a6df7309eb56e727bf2fd"; //須要用到testExportSeckillUrl獲得的md5
try { SeckillExecution excution = seckillService.executeSeckill(id, phone, md5); logger.info("excution={}",excution); } catch (RepeatKillException e) { logger.error(e.getMessage()); }catch (SeckillCloseException e) { logger.error(e.getMessage()); } }
這樣再測試該方法,junit便不會再在控制檯中報錯,而是認爲這是咱們系統容許出現的異常。由上分析可知,第四個方法只有拿到了第三個方法暴露的秒殺商品的地址後才能進行測試,也就是說只有在第三個方法運行後才能運行測試第四個方法,而實際開發中咱們不是這樣的,須要將第三個測試方法和第四個方法合併到一個方法從而組成一個完整的邏輯流程:
//完整邏輯代碼測試,注意可重複執行
@Test public void testSeckillLogic() throws Exception { long id = 1000; Exposer exposer = seckillService.exportSeckillUrl(id); if(exposer.isExposed()) { logger.info("exposer={}",exposer); long phone = 17808315995L; String md5 = "07cde05fe83a6df7309eb56e727bf2fd"; try { SeckillExecution excution = seckillService.executeSeckill(id, phone, md5); logger.info("excution={}",excution); } catch (RepeatKillException e) { logger.error(e.getMessage()); }catch (SeckillCloseException e) { logger.error(e.getMessage()); } }else { //秒殺未開啓
logger.warn("exposer={}",exposer); } }
運行該測試類,控制檯成功輸出信息,庫存會減小,明細表也會增長內容。重複執行,控制檯不會報錯,只是會拋出一個容許的重複秒殺異常。
目前爲止,Dao層和Service層的集成測試咱們都已經完成,接下來進行Web層的開發編碼工做