分層的必要性
一些初學(xué)者容易出現(xiàn)的錯誤,就是喜歡在DAO層進(jìn)行邏輯的編寫,其實DAO就是數(shù)據(jù)訪問的縮寫,它只進(jìn)行數(shù)據(jù)的訪問操作。
業(yè)務(wù)接口的編寫
初學(xué)者總是關(guān)注細(xì)節(jié),關(guān)注接口如何去實現(xiàn),這樣設(shè)計出來的接口往往比較冗余。業(yè)務(wù)接口的編寫要站在“使用者”的角度定義,三個方面:方法定義的粒度、參數(shù)、返回值。
DTO與entity的區(qū)別
DTO數(shù)據(jù)傳輸層:用于Web層和Service層之間傳遞的數(shù)據(jù)封裝。
entity:用于業(yè)務(wù)數(shù)據(jù)的封裝,比如數(shù)據(jù)庫中的數(shù)據(jù)。
關(guān)于秒殺地址的暴露
MD5加密
Spring提供了MD5生成工具。代碼如下:
DigestUtils.md5DigestAsHex();
MD5鹽值字符串(salt),用于混淆MD5,添加MD5反編譯難度
在src/main/java
包下建立com.lewis.service
包,用來存放Service接口;在src/main/java
包下建立com.lewis.exception
包,用來存放Service層出現(xiàn)的異常類:比如重復(fù)秒殺異常、秒殺已關(guān)閉異常;在src/main/java
包下建立com.lewis.dto
包,用來封裝Web層和Service層之間傳遞的數(shù)據(jù)。
定義SeckillService接口
/** * 業(yè)務(wù)接口:站在使用者(程序員)的角度設(shè)計接口 三個方面:1.方法定義粒度,方法定義的要非常清楚2.參數(shù),要越簡練越好 3.返回類型(return * 類型一定要友好/或者return異常,我們允許的異常) */public interface SeckillService { /** * 查詢?nèi)康拿霘⒂涗? * * @return */ List<Seckill> getSeckillList(); /** * 查詢單個秒殺記錄 * * @param seckillId * @return */ Seckill getById(long seckillId); // 再往下,是我們最重要的行為的一些接口 /** * 在秒殺開啟時輸出秒殺接口的地址,否則輸出系統(tǒng)時間和秒殺時間 * * @param seckillId 秒殺商品Id * @return 根據(jù)對應(yīng)的狀態(tài)返回對應(yīng)的狀態(tài)實體 */ Exposer exportSeckillUrl(long seckillId); /** * 執(zhí)行秒殺操作,有可能失敗,有可能成功,所以要拋出我們允許的異常 * * @param seckillId 秒殺的商品ID * @param userPhone 手機(jī)號碼 * @param md5 md5加密值 * @return 根據(jù)不同的結(jié)果返回不同的實體信息 */ SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException;}
在dto包中創(chuàng)建Exposer.java,用于封裝秒殺的地址信息
/** * 暴露秒殺地址(接口)DTO */public class Exposer { // 是否開啟秒殺 private boolean exposed; // 加密措施 private String md5; //id為seckillId的商品的秒殺地址 private long seckillId; // 系統(tǒng)當(dāng)前時間(毫秒) private long now; // 秒殺的開啟時間 private long start; // 秒殺的結(jié)束時間 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; } @Override public String toString() { return "Exposer{" + "exposed=" + exposed + ", md5='" + md5 + '\'' + ", seckillId=" + seckillId + ", now=" + now + ", start=" + start + ", end=" + end + '}'; }}
在dto包中創(chuàng)建SeckillExecution.java,用于封裝秒殺是否成功的結(jié)果(該對象用來返回給頁面)
/** * 封裝執(zhí)行秒殺后的結(jié)果:是否秒殺成功 */public class SeckillExecution { private long seckillId; //秒殺執(zhí)行結(jié)果的狀態(tài) private int state; //狀態(tài)的明文標(biāo)識 private String stateInfo; //當(dāng)秒殺成功時,需要傳遞秒殺成功的對象回去 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; }}
在exception包中創(chuàng)建秒殺過程中可能出現(xiàn)的異常類
定義一個基礎(chǔ)的異常類SeckillException,繼承自RuntimeException
/** * 秒殺相關(guān)的所有業(yè)務(wù)異常 */public class SeckillException extends RuntimeException { public SeckillException(String message) { super(message); } public SeckillException(String message, Throwable cause) { super(message, cause); }}
重復(fù)秒殺異常,繼承自SeckillException
/** * 重復(fù)秒殺異常,是一個運行期異常,不需要我們手動try catch * Mysql只支持運行期異常的回滾操作 */public class RepeatKillException extends SeckillException { public RepeatKillException(String message) { super(message); } public RepeatKillException(String message, Throwable cause) { super(message, cause); }}
秒殺已關(guān)閉異常,繼承自SeckillException
/** * 秒殺關(guān)閉異常,當(dāng)秒殺結(jié)束時用戶還要進(jìn)行秒殺就會出現(xiàn)這個異常 */public class SeckillCloseException extends SeckillException{ public SeckillCloseException(String message) { super(message); } public SeckillCloseException(String message, Throwable cause) { super(message, cause); }}
在com.lewis.service
包下再建立impl
包,用來存放接口的實現(xiàn)類SeckillServiceImpl
public class SeckillServiceImpl implements SeckillService{ //日志對象 private Logger logger= LoggerFactory.getLogger(this.getClass()); //加入一個混淆字符串(秒殺接口)的salt,為了我避免用戶猜出我們的md5值,值任意給,越復(fù)雜越好 private final String salt="aksehiucka24sf*&%&^^#^%$"; //注入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)當(dā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; } //秒殺是否成功,成功:減庫存,增加明細(xì);失敗:拋出異常,事務(wù)回滾 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í)行秒殺邏輯:減庫存+增加購買明細(xì) Date nowTime=new Date(); try{ //減庫存 int updateCount=seckillDao.reduceNumber(seckillId,nowTime); if (updateCount<=0) { //沒有更新庫存記錄,說明秒殺結(jié)束 throw new SeckillCloseException("seckill is closed"); }else { //否則更新了庫存,秒殺成功,增加明細(xì) int insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone); //看是否該明細(xì)被重復(fù)插入,即用戶是否重復(fù)秒殺 if (insertCount<=0) { throw new RepeatKillException("seckill repeated"); }else { //秒殺成功,得到成功插入的明細(xì)記錄,并返回成功秒殺的信息 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); //將編譯期異常轉(zhuǎn)化為運行期異常 throw new SeckillException("seckill inner error :"+e.getMessage()); } }}
在以上代碼中,我們捕獲了運行時異常,原因是Spring的事務(wù)默認(rèn)是發(fā)生了RuntimeException才會回滾,發(fā)生了其他異常不會回滾,所以在最后的catch塊里通過throw new SeckillException("seckill inner error :"+e.getMessage());
將編譯期異常轉(zhuǎn)化為運行期異常。
另外,在代碼里還存在著硬編碼的情況,比如秒殺結(jié)果返回的state和stateInfo參數(shù)信息是輸出給前端的,這些字符串應(yīng)該考慮用常量枚舉類封裝起來,方便重復(fù)利用,也易于維護(hù)。
在
src/main/java
包下新建一個枚舉包com.lewis.enums
包,在該包下創(chuàng)建一個枚舉類型SeckillStatEnum
public enum SeckillStatEnum { SUCCESS(1,"秒殺成功"), END(0,"秒殺結(jié)束"), REPEAT_KILL(-1,"重復(fù)秒殺"), 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; }}
創(chuàng)建了枚舉類型后,就需要修改之前硬編碼的地方,修改
SeckillExecution
涉及到state和stateInfo參數(shù)的構(gòu)造方法
//秒殺成功返回所有信息 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(); }
接著把SeckillServiceImpl
里返回的秒殺成功信息的return new SeckillExecution(seckillId,1,"秒殺成功",successKilled);
改成return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);
在之前創(chuàng)建的
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" 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包下所有使用注解的類型 --> <context:component-scan base-package="com.lewis.service" /> <!--配置事務(wù)管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入數(shù)據(jù)庫連接池 --> <property name="dataSource" ref="dataSource" /> </bean> <!--配置基于注解的聲明式事務(wù) 默認(rèn)使用注解來管理事務(wù)行為 --> <tx:annotation-driven transaction-manager="transactionManager" /></beans>
事務(wù)管理器
MyBatis采用的是JDBC的事務(wù)管理器
Hibernate采用的是Hibernate的事務(wù)管理器
通過注解的方式將Service的實現(xiàn)類(注意,不是Service接口)加入到Spring IoC容器中
@Servicepublic class SeckillServiceImpl implements SeckillService;
在需要進(jìn)行事務(wù)聲明的方法上加上事務(wù)的注解@Transactional
@Transactionalpublic SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {}
Spring的聲明式事務(wù)管理
Java異常分編譯期異常和運行期異常,運行期異常不需要手工try-catch,Spring的的聲明式事務(wù)只接收運行期異常回滾策略,非運行期異常不會幫我們回滾。
Spring一共有7個事務(wù)傳播行為,默認(rèn)的事務(wù)傳播行為是PROPAGATION_REQUIRED
,詳情可以參考這篇文章
使用注解控制事務(wù)方法的優(yōu)點(對于秒殺這種對事務(wù)延遲要求高的業(yè)務(wù)場景尤為重要)
為什么使用IoC(控制反轉(zhuǎn))
Spring基于注解的事務(wù)操作
不能被Spring AOP事務(wù)增強(qiáng)的方法
序號 | 動態(tài)代理策略 | 不能被事務(wù)增強(qiáng)的方法 |
---|---|---|
1 | 基于接口的動態(tài)代理 | 除了public以外的所有方法,并且public static的方法也不能被增強(qiáng) |
2 | 基于Cglib的動態(tài)代理 | private、static、final的方法 |
關(guān)于Spring的組件注解、注入注解
通過Spring提供的組件自動掃描機(jī)制,可以在類路徑下尋找標(biāo)注了上述注解的類,并把這些類納入進(jìn)spring容器中管理,這些注解的作用和在xml文件中使用bean節(jié)點配置組件時一樣的。
<context:component-scan base-package=”xxx.xxx.xxx”>
component-scan
標(biāo)簽?zāi)J(rèn)情況下自動掃描指定路徑下的包(含所有子包),將帶有@Component、@Repository、@Service、@Controller標(biāo)簽的類自動注冊到spring容器。getBean的默認(rèn)名稱是類名(頭字母小寫),如果想自定義,可以@Service(“aaaaa”)這樣來指定。這種bean默認(rèn)是“singleton”的,如果想改變,可以使用@Scope(“prototype”)來改變。
當(dāng)使用<context:component-scan/>
后,就可以將<context:annotation-config/>
移除了,前者包含了后者。
另外,@Resource,@Inject 是J2EE規(guī)范的一些注解
@Autowired是Spring的注解,可以對類成員變量、方法及構(gòu)造函數(shù)進(jìn)行標(biāo)注,完成自動裝配的工作。通過 @Autowired的使用來消除setter/getter方法,默認(rèn)按類型裝配,如果想使用名稱裝配可以結(jié)合@Qualifier注解進(jìn)行使用,如下:
@Autowired() @Qualifier("baseDao") private BaseDao baseDao;
與@Autowired類似的是@Resource,@Resource屬于J2EE規(guī)范,默認(rèn)安照名稱進(jìn)行裝配,名稱可以通過name屬性進(jìn)行指定,如果沒有指定name屬性,當(dāng)注解寫在字段上時,默認(rèn)取字段名進(jìn)行按照名稱查找,如果注解寫在setter方法上默認(rèn)取屬性名進(jìn)行裝配。當(dāng)找不到與名稱匹配的bean時才按照類型進(jìn)行裝配。但是需要注意的是,如果name屬性一旦指定,就只會按照名稱進(jìn)行裝配。
@Resource(name="baseDao") private BaseDao baseDao;
而@Inject與@Autowired類似,也是根據(jù)類型注入,也可以通過@Named注解來按照name注入,此時只會按照名稱進(jìn)行裝配。
@Inject @Named("baseDao")private BaseDao baseDao;
使用logback來輸出日志信息,在
resources
包下創(chuàng)建logback.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>
通過IDE工具快速生成Junit單元測試,然后在各個方法里寫測試代碼。
@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 testGetSeckillList() throws Exception { List<Seckill> list=seckillService.getSeckillList(); logger.info("list={}", list); } @Test public void testGetById() throws Exception { long seckillId=1000; Seckill seckill=seckillService.getById(seckillId); logger.info("seckill={}", seckill); }}
在測試通過了這兩個方法后,開始對后兩個業(yè)務(wù)邏輯方法的測試,首先測試testExportSeckillUrl()
@Testpublic void testExportSeckillUrl() throws Exception { long seckillId=1000; Exposer exposer=seckillService.exportSeckillUrl(seckillId); logger.info("exposer={}", exposer);}
會發(fā)現(xiàn)沒有返回商品的秒殺地址,因為我們數(shù)據(jù)庫的秒殺時間和結(jié)束秒殺時間沒有修改,所以判斷當(dāng)前商品的秒殺已結(jié)束。將數(shù)據(jù)庫中的秒殺時間和結(jié)束秒殺時間修改成滿足我們當(dāng)前的時間的范圍,重新測試該方法,可以獲取到該商品的秒殺地址。而第四個方法的測試需要使用到該地址(md5),將該值傳入到testExecuteSeckill()
中進(jìn)行測試:
@Testpublic void testExecuteSeckill() throws Exception { long seckillId=1000; long userPhone=13476191876L; String md5="70b9564762568e9ff29a4a949f8f6de4"; SeckillExecution execution=seckillService.executeSeckill(seckillId,userPhone,md5); logger.info("result={}", execution);}
需要注意的是,該方法是會產(chǎn)生異常的,比如我們重復(fù)運行該方法,會報錯,因為用戶進(jìn)行了重復(fù)秒殺,所以我們需要手動try-catch,將程序允許的異常包起來而不去向上拋給junit,更改測試代碼如下:
@Testpublic void testExecuteSeckill() throws Exception { long seckillId=1000; long userPhone=13476191876L; String md5="70b9564762568e9ff29a4a949f8f6de4"; try { SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); logger.info("result={}", execution); }catch (RepeatKillException e) { logger.error(e.getMessage()); }catch (SeckillCloseException e1) { logger.error(e1.getMessage()); }}
在測試過程中,第四個方法使用到了第三個方法返回的秒殺地址,在實際開發(fā)中,我們需要將第三個和第四個方法合并成一個完整邏輯的方法:
//集成測試代碼完整邏輯,注意可重復(fù)執(zhí)行@Testpublic void testSeckillLogic() throws Exception { long seckillId=1000; Exposer exposer=seckillService.exportSeckillUrl(seckillId); if (exposer.isExposed()) { logger.info("exposer={}", exposer); long userPhone=13476191876L; String md5=exposer.getMd5(); try { SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5); logger.info("result={}", execution); }catch (RepeatKillException e) { logger.error(e.getMessage()); }catch (SeckillCloseException e1) { logger.error(e1.getMessage()); } }else { //秒殺未開啟 logger.warn("exposer={}", exposer); }}
我們可以在SeckillServiceTest類里面加上@Transational注解,原因是:
@Transactional注解是表明此測試類的事務(wù)啟用,這樣所有的測試方案都會自動的 rollback,即不用自己清除自己所做的任何對數(shù)據(jù)庫的變更了。
日志無法打印的問題
在pom.xml中加上
<dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.9</version></dependency>
存在的坑
沒有引入AOP的xsd會報錯
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance http://www.springmodules.org/schema/cache/springmodules-cache.xsd http://www.springmodules.org/schema/cache/springmodules-ehcache.xsd"
相關(guān)鏈接
Spring事務(wù)異?;貪L,捕獲異常不拋出就不會回滾
本節(jié)結(jié)語
至此,關(guān)于Java高并發(fā)秒殺API的Service層的開發(fā)與測試已經(jīng)完成,接下來進(jìn)行Web層的開發(fā),詳情請參考下一篇文章。
上一篇文章:Java高并發(fā)秒殺API(一)之業(yè)務(wù)分析與DAO層
下一篇文章:Java高并發(fā)秒殺API(三)之Web層