上一篇講述了DAO 層,mybatis實現數據庫的連接,DAO層接口設計,以及mybtis和spring的整合。DAO層採用接口設計方式實現,接口和SQL實現的分離,方便維護。DAO層所負責的僅僅是接口的設計和實現,而負責的邏輯即一個或多個DAO層接口的拼接是在Sevice層中完成。這篇文章接上篇文章,主要講述Service層的實現、和Spring的整合以及聲明如何聲明事物。
一、Service層接口設計
業務接口設計應當站在“使用者”角度設計接口,應遵循三個規範:合理的命令,明確的參數,返回結果(正常接口/異常結果)。本例子採用的Java高併發的秒殺API系列課程的例子,創建設計的業務邏輯接口如下:
public interface SeckillService {
/**
* 查詢所有秒殺記錄
* @return
*/
List<Seckill> getSerkillList();
/**
* 查詢單個秒殺記錄
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 秒殺開啓時輸出秒殺接口地址,
* 否則輸出系統時間和秒殺時間
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
*執行秒殺接口
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException;
二、Service接口的實現
直接上代碼了,在這裏講下秒殺業務的邏輯:首先是獲取秒殺列表,點擊列表進入秒殺詳情頁,這時獲取系統時間,如果秒殺開始,獲取秒殺地址,點擊秒殺,執行秒殺。所以業務邏輯也只設計了這相關的4個業務邏輯。其中使用了dto層去傳遞響應數據,以及自定義異常,所有的異常都繼承運行異常,這是爲了方便spring自動回滾,這兩個知識點,自行看源碼。
package org.forezp.service.impl;
@Service
public class SeckillServiceImpl implements SeckillService{
private Logger logger= LoggerFactory.getLogger(this.getClass());
//注入service依賴
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
//MD5鹽值字符串,用戶混淆MD5
private final String slat="sfsa=32q4r23234215ERWERT^**%^SDF";
public List<Seckill> getSerkillList() {
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){
return new Exposer(false,seckillId);
}
Date startTime=seckill.getStartTime();
Date endTime=seckill.getEndTime();
//系統當前時間
Date nowTime=new Date();
if(nowTime.getTime()<startTime.getTime()||nowTime.getTime()>endTime.getTime()){
return new Exposer(false,seckillId,nowTime.getTime(),startTime.getTime(),endTime.getTime());
}
String md5=getMD5(seckillId);
return new Exposer(true,md5,seckillId);
}
private String getMD5(long seckillId){
String base=seckillId+"/"+slat;
String md5= DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
@Transactional
/**
*使用註解控制事務方法的優點
* 1:開發團隊達成一致約定,明確標註事務方法的編程風格
* 2:保證事務方法的執行時間儘可能短,不要穿插其他網絡請求,RPC/HTTP請求或者剝離到事務方法外
* 3:不是所有的方法都需要事務,如只有一條修改操作,只讀操作不需要事務控制
*/
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 nowTime=new Date();
try {
//記錄購買行爲
int insertCount=successKilledDao.insertSuccessKilled(seckillId,userPhone);
//唯一:seckillId,userphone
if(insertCount<=0){
//重複秒殺
throw new RepeatKillException("seckill repeated");
}else{
//減庫存,熱點商品競爭
int updateCount=seckillDao.reduceNumber(seckillId,nowTime);
if(updateCount<=0){
//沒有更新到記錄,秒殺結束 rollback
throw new SeckillCloseException("seckill is closed");
}else{
//秒殺成功 commit
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);
//所有的編譯期異常,轉化爲運行期異常(運行時異常,spring可以做rollback操作)
throw new SeckillException("seckill inner error:"+e.getMessage());
}
}
//拋出異常是爲了告訴spring是否rollback,此處使用存儲過程的話,就不需要拋異常了
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
if(md5 ==null || !md5.equals(getMD5(seckillId))){
return new SeckillExecution(seckillId,SeckillStatEnum.DATA_REWRITE);
}
Date killTime=new Date();
Map<String,Object> map=new HashMap<String, Object>();
map.put("seckillId",seckillId);
map.put("phone",userPhone);
map.put("killTime",killTime);
map.put("result",null);
//執行存儲過程,result被賦值
try {
seckillDao.killByProcedure(map);
int result=(Integer) map.get("result");
if(result==1){
SuccessKilled successKilled=successKilledDao.queryByIdWithSeckill(seckillId,userPhone);
return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS,successKilled);
}else{
return new SeckillExecution(seckillId,SeckillStatEnum.stateof(result));
}
} catch (Exception e) {
logger.error(e.getMessage(),e);
return new SeckillExecution(seckillId,SeckillStatEnum.INNER_ERROE);
}
}
}
三、Sping託管 service的實現類
和上一篇文章使用spring託管dao接口一樣,這裏也需要用 spring 託管service. spring ioc 使用對象工程模式,對所有的注入的依賴進行了管理,暴露出了一致性的訪問接口,當我們需要某個對象時,直接從spring ioc中取就行了,不需要new,也不需要對它們的生命週期進行管理。更爲重要的是spring 自動組裝依賴,比如最終的接口controller依賴service,而service依賴dao,dao依賴sessionfactory,而sessionfactory依賴datasource,這些層層依賴是通過spring管理並層層組裝,只要我們簡單配置和註解就可以方便的使用,代碼的分層和編程的藝術在spring框架中展現得淋漓盡至。
本項目採用spring ioc :
1.xml配置
2.包掃描
3.annotation註解。
創建sping-service.xml
採用包掃描+註解方式,首先在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="org.forezp.service"/>
然後在org,forezp.service包下的類採用註解。比如@Service 註解聲明是一個service, @Autowired注入service 所需依賴。
@Service//聲明是一個service
public class SeckillServiceImpl implements SeckillService{
//注入service依賴
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
}
只需要一個包掃描和幾個簡單的註解就可以將service註解到spring ioc容器中。
四、spring聲明式事物
在秒殺案例中,我們需要採用事物來防止數據的正確性,防止重複秒殺,防止庫存不足、庫存剩餘等情況。一般使用事物需要開啓事物/經常一些列的操作,提交或者回滾。spring聲明式事物,就是將事物的開啓、提交等託管給spring管理,我們只需要注重如何修改數據。
配置spring 聲明式事物
在spring-service.xml中配置:
<!--配置事務管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入數據庫連接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置基於註解的聲明式事務
默認使用註解來管理事務行爲
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
在需要事物的業務邏輯下加 @Transactional註解。
比如在開啓秒殺方法:
@Transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
if(md5==null ||!md5.equals(getMD5(seckillId))){
}
注意:
1開發團隊達成一致約定,明確標註事務方法的編程風格
2:保證事務方法的執行時間儘可能短,不要穿插其他網絡請求,RPC/HTTP請求或者剝離到事務方法外
3:不是所有的方法都需要事務,如只有一條修改操作,只讀操作不需要事務控制
五、單元測試
需要配置:
@ContextConfiguration({
“classpath:spring/spring-dao.xml”,
“classpath:spring/spring-service.xml”
})
直接上代碼:
@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 getSerkillList() throws Exception {
List<Seckill> list=seckillService.getSerkillList();
System.out.println(list);
//執行結果[Seckill{seckillId=1000, name='1000元秒殺iphone6'..... 省略。。。]
}
@Test
public void getById() throws Exception {
long id=1000;
Seckill seckill=seckillService.getById(id);
System.out.println(seckill);
//執行結果:Seckill{seckillId=1000, name='1000元秒殺iphone6', number=100, startTime=Sun Nov 01 00:00:00 CST 2015,。。。。}
}
@Test
public void exportSeckillUrl() throws Exception {
long id=1000;
Exposer exposer=seckillService.exportSeckillUrl(id);
System.out.println(exposer);
}
@Test
public void executeSeckill() throws Exception {
long id=1000;
long phone=13502171122L;
String md5="e83eef2cc6b033ca0848878afc20e80d";
SeckillExecution execution=seckillService.executeSeckill(id,phone,md5);
System.out.println(execution);
}
}
這篇文章主要講了service業務接口的編寫和實現,以及採用xml和註解方式講service 注入到spring ioc,以及聲明式事物,不得不感嘆spring 的強大。下一篇文章講講述 web層的開發,spring mvc的相關配置。