首先先贏明白service層的作用:Service層主要負責(zé)業(yè)務(wù)模塊的邏輯應(yīng)用設(shè)計秉颗,同樣是先設(shè)計接口卷要,再設(shè)計其實現(xiàn)的類辉懒,接著再是spring的配置其實現(xiàn)的關(guān)聯(lián)屈糊,這樣就可以在應(yīng)用中調(diào)用service接口來進行業(yè)務(wù)處理
1玩徊、service接口設(shè)計
~租悄、前臺頁面需要顯示參與秒殺商品的信息:getSeckillList(showPage):根據(jù)現(xiàn)實的頁碼進行列表查詢,查詢秒殺記錄
~恩袱、點擊相應(yīng)連接后前臺顯示具體商品信息:getById(sekillId):查詢單個秒殺記錄
package com.seckill.service;
public interface SeckillService {
/**
* 查詢所有秒殺記錄
* @return
*/
List<Seckill> getSeckillList(int showPage);
/**
* 在每頁顯示5條數(shù)據(jù)情況下
* 獲得總頁數(shù)
*
* @return
*/
int getPageCount();
/**
* 查詢單個秒殺記錄
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
/**
* 秒殺開啟時輸出秒殺接口地址
* 否則輸出系統(tǒng)時間和秒殺時間
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 執(zhí)行秒殺操作泣棋,有可能是失敗的,失敗的時候就拋出異常
* @param seckillId 秒殺的商品Id
* @param userPhone 手機號碼
* @param md5 md5加密值
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,SeckillCloseException,RepeaKillException;
/**
* 執(zhí)行存儲過程操作
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
SeckillExecution executeSeckillProcedure(long seckillId, long userPhone,String md5);
}
當(dāng)開始編寫service接口時畔塔,需要思考一個問題潭辈,那就是后臺反饋給前臺的數(shù)據(jù)是一個怎樣的json數(shù)據(jù)?數(shù)字狀態(tài)碼澈吨,或者是文字把敢?
這里先解釋幾種對象:
~、PO: 也就是我們在為每一張數(shù)據(jù)庫表寫一個實體的類
~棚辽、VO, 對某個頁面或者展現(xiàn)層所需要的數(shù)據(jù),封裝成一個實體類
~技竟、BO, 就是業(yè)務(wù)對象,我也不是很了解
~、DTO, 跟VO的概念有點混淆,也是相當(dāng)于頁面需要的數(shù)據(jù)封裝成一個實體類
~屈藐、POJO, 簡單的無規(guī)則java對象
了解了這幾種對象榔组,便可以明白,我們需要向前臺返回的可以是DTO對象联逻,后臺對所需要返回的數(shù)據(jù)進行封裝搓扯。
因此對于service接口中的Esposer和SeckillExecution 對象便可以解釋,以及相應(yīng)的異常對象
com.seckill.dto.Exposer
com.seckill.dto.SeckillExecution
/**
* 秒殺開啟時輸出秒殺接口地址
* 否則輸出系統(tǒng)時間和秒殺時間
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 執(zhí)行秒殺操作,有可能是失敗的包归,失敗的時候就拋出異常
* @param seckillId 秒殺的商品Id
* @param userPhone 手機號碼
* @param md5 md5加密值
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,SeckillCloseException,RepeaKillException;
Exposer如下:(編寫時也要想好Exposer包含的變量以及不同情況下對應(yīng)的構(gòu)造函數(shù))
package com.seckill.dto;
public class Exposer {
@Override
public String toString() {
return "Exposer [exposed=" + exposed + ", md5=" + md5 + ", seckillId=" + seckillId + ", now=" + now + ", start="
+ start + ", end=" + end + "]";
}
//無參構(gòu)造函數(shù)
public Exposer() {
// Auto-generated constructor stub
}
//是否開啟秒殺
private boolean exposed;
// 一種加密措施
private String md5;
//秒殺商品id
private long seckillId;
// 系統(tǒng)當(dāng)前時間
private long now;
//開啟時間
private long start;
//結(jié)束時間
private long end;
/**
* 當(dāng)秒殺時間不符合時锨推,返回的數(shù)據(jù)
* @param exposed false狀態(tài)
* @param seckillId 商品ID
* @param now 當(dāng)前系統(tǒng)時間(服務(wù)器時間)
* @param start 秒殺開啟時間
* @param end 秒殺結(jié)束時間
*/
public Exposer(boolean exposed, Long seckillId,long now, long start, long end) {
super();
this.seckillId = seckillId;
this.exposed = exposed;
this.now = now;
this.start = start;
this.end = end;
}
/**
* 該商品符合秒殺時返回的數(shù)據(jù)
* @param exposed 狀態(tài)
* @param md5 一種鹽值加密數(shù)據(jù)
* @param seckillId 秒殺商品id
*/
public Exposer(boolean exposed, String md5, long seckillId) {
super();
this.exposed = exposed;
this.md5 = md5;
this.seckillId = seckillId;
}
/**
* 秒殺商品不存在時返回的數(shù)據(jù)
* @param exposed
* @param seckillId
*/
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:執(zhí)行秒殺后后臺返回給前臺的數(shù)據(jù)封裝對象
package com.seckill.dto;
import com.seckill.entity.SuccessKilled;
import com.seckill.enums.SeckillStateEnum;
public class SeckillExecution {
public SeckillExecution() {
//Auto-generated constructor stub
}
// 商品Id
private long seckillId;
// 秒殺結(jié)果的狀態(tài)
private int state;
/* 狀態(tài)的明文標(biāo)示 */
private String stateInfo;
/* 當(dāng)秒殺成功時,需要傳遞秒殺結(jié)果的對象回去 */
private SuccessKilled successKilled;
/* 秒殺成功返回的實體 */
public SeckillExecution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
super();
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
this.successKilled = successKilled;
}
/* 秒殺失敗時返回的實體,沒有秒殺結(jié)果的對象 */
public SeckillExecution(long seckillId, SeckillStateEnum stateEnum) {
super();
this.seckillId = seckillId;
this.state = stateEnum.getState();
this.stateInfo = stateEnum.getStateInfo();
}
@Override
public String toString() {
return "SeckillException [seckillId=" + seckillId + ", state=" + state + ", stateIofo=" + 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;
}
}
定義秒殺過程中可能會出現(xiàn)的異常:
~换可、定義一個基礎(chǔ)異常椎椰,所有的異常都繼承這個異常
SeckillException :
package com.seckill.exception;
public class SeckillException extends RuntimeException {
public SeckillException() {
}
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
~、定義重復(fù)秒殺異常:
package com.seckill.exception;
/**
* 重復(fù)秒殺異常沾鳄,不需要我們手動去try catch
* @author hyh47
*
*/
public class RepeaKillException extends SeckillException{
public RepeaKillException() {
}
public RepeaKillException(String message){
super(message);
}
public RepeaKillException(String message,Throwable cause){
super(message, cause);
}
}
~慨飘、定義秒殺關(guān)閉異常
package com.seckill.exception;
/**
* 秒殺已經(jīng)關(guān)閉異常,當(dāng)秒殺結(jié)束就會出現(xiàn)這個異常
* @author hyh47
*
*/
public class SeckillCloseException extends SeckillException{
public SeckillCloseException() {
}
public SeckillCloseException(String message){
super(message);
}
public SeckillCloseException(String message, Throwable cause){
super(message, cause);
}
}
2译荞、service接口實現(xiàn)
@Service
public class SeckillServiceImpl implements SeckillService {
/*日志記錄*/
private org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
/*自定義用于md5加密的鹽值*/
private final String salt = "ljflajvoia332131ADSFJJL(&(*(*#@";
final int pageSize = 5;//單頁顯示的數(shù)量
//注入service依賴
@Autowired
private SeckillDao seckillDao;
@Autowired
private SuccessKilledDao successKilledDao;
@Autowired
private RedisDao redisDao;
public SeckillServiceImpl() {
// Auto-generated constructor stub
}
/**
* 查詢?nèi)康拿霘⒂涗? *
* @return 數(shù)據(jù)庫中所有的秒殺記錄
*/
/*
pageSize(每個頁面所顯示的記錄數(shù))瓤的、
pageCount(頁面的總數(shù))、
showPage(目前顯示第幾頁)吞歼、
recordCount(總的記錄數(shù))
*/
@Override
public List<Seckill> getSeckillList(int showPage) {
// Auto-generated method stub
return seckillDao.queryAll((showPage - 1) * pageSize, pageSize);
}
@Override
public int getPageCount() {
int recordCount = seckillDao.querySeckillNumber();//總數(shù)
int pageCount = (recordCount % pageSize == 0) ? (recordCount / pageSize) : (recordCount / pageSize + 1);
return pageCount;
}
/**
* 查詢秒殺記錄圈膏,通過商品Id
*/
@Override
public Seckill getById(long seckillId) {
// Auto-generated method stub
return seckillDao.queryById(seckillId);
}
@Override
@Transactional
/**
* 使用注解控制事務(wù)方法的優(yōu)點:
* 1:開發(fā)團隊打成一致約定,明確標(biāo)注事務(wù)方法的編程風(fēng)格
* 2:保證事務(wù)方法的執(zhí)行時間盡可能短篙骡,不穿插其他網(wǎng)絡(luò)操作稽坤,RPC/HTTP請求或者剝離到事務(wù)方法外部
* 3:不是所有的方法都需要事務(wù),比如只有一條修改操作医增,或者只讀操作不需要事務(wù)控制
*/
public Exposer exportSeckillUrl(long seckillId) {
// Auto-generated method stub
/*Seckill seckill = seckillDao.queryById(seckillId);
if (seckill == null){
return new Exposer(false,seckillId);
}*/
Seckill seckill = redisDao.getSeckill(seckillId);
if (seckill == null) {
//訪問數(shù)據(jù)庫讀取數(shù)據(jù)
seckill = seckillDao.queryById(seckillId);
if (seckill == null) {
return new Exposer(false, seckillId);
} else {
//放入redis中
redisDao.putSeckill(seckill);
}
}
Date startTime = seckill.getStartTime();
Date endTime = seckill.getEndTime();
//當(dāng)前系統(tǒng)時間
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 + "/" + salt;
String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
return md5;
}
@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException, SeckillCloseException, RepeaKillException {
//在這一系列操作中慎皱,對應(yīng)事務(wù),return是commit,拋異常是rollback
try {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
logger.error("秒殺數(shù)據(jù)被篡改");
throw new SeckillException("seckill data rewrite");
}
//執(zhí)行秒殺邏輯: 減庫存+記錄購買行為
Date nowTime = new Date();
//記錄購買行為
int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
//唯一:seckillId,userPhone
if (insertCount <= 0) {
logger.warn("不允許重復(fù)秒殺");
throw new RepeaKillException("repeaKill !!");
} else {
//減庫存,熱點商品競爭
int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
if (updateCount <= 0) {
//沒有更新記錄叶骨,秒殺結(jié)束,可能時間結(jié)束也可能庫存為零茫多,rollback
logger.warn("沒有更新數(shù)據(jù)庫記錄,秒殺已經(jīng)結(jié)束了");
throw new SeckillCloseException("seckill is closed");
} else {
//秒殺成功,commit
SuccessKilled successkilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successkilled);
}
}
} catch (SeckillCloseException | RepeaKillException e1) {
throw e1;
} catch (Exception e) {
logger.error(e.getMessage(), e);
throw new SeckillException("seckill inner error:" + e.getMessage());
}
}
@Override
public SeckillExecution executeSeckillProcedure(long seckillId, long userPhone, String md5) {
if (md5 == null || !md5.equals(getMD5(seckillId))) {
return new SeckillExecution(seckillId, SeckillStateEnum.DATE_PEWRITER);
}
Date killTime = new Date();
Map<String, Object> map = new HashMap<>();
map.put("seckillId", seckillId);
map.put("phone", userPhone);
map.put("killTime", killTime);
map.put("result", null);
//執(zhí)行存儲過程忽刽,result被復(fù)制
try {
seckillDao.killByProcedure(map);
//獲取result
int result = MapUtils.getInteger(map, "result", -2);
if (result == 1) {
SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
return new SeckillExecution(seckillId, SeckillStateEnum.SUCCESS, successKilled);
} else {
return new SeckillExecution(seckillId, SeckillStateEnum.stateOf(result));
}
} catch (Exception e) {
logger.error(e.getMessage(), e);
return new SeckillExecution(seckillId, SeckillStateEnum.INNER_ERROR);
}
}
}
在這里我們捕獲了運行時異常,這樣做的原因就是Spring的事物默認就是發(fā)生了RuntimeException才會回滾,可以檢測出來的異常是不會導(dǎo)致事物的回滾的,這樣的目的就是你明知道這里會發(fā)生異常,所以你一定要進行處理.如果只是為了讓編譯通過的話,那捕獲異常也沒多意思,所以這里要注意事物的回滾.
然后我們還發(fā)現(xiàn)這里存在硬編碼的現(xiàn)象,就是返回各種字符常量,例如秒殺成功,秒殺失敗等等,這些字符串時可以被重復(fù)使用的,而且這樣維護起來也不方便,要到處去類里面尋找這樣的字符串,所有我們使用枚舉類來管理這樣狀態(tài),在com.seckill包下建立enum包,專門放置枚舉類,然后再建立SeckillStateEnum枚舉類:
此處關(guān)于秒殺結(jié)果的集中狀態(tài)天揖,采用枚舉類進行封裝:
com.seckill.enums.SeckillStateEnum
package com.seckill.enums;
/**
* 使用枚舉表述常量數(shù)據(jù)字段
* @author hyh47
*
*/
public enum SeckillStateEnum {
SUCCESS(1,"秒殺成功"),
END(0,"秒殺結(jié)束"),
REPEAT_KILL(-1,"重復(fù)秒殺"),
INNER_ERROR(-2,"系統(tǒng)異常"),
DATE_PEWRITER(-3,"數(shù)據(jù)篡改");
private int state;
private String stateInfo;
public int getState() {
return state;
}
public String getStateInfo() {
return stateInfo;
}
private SeckillStateEnum(int state, String stateInfo) {
this.state = state;
this.stateInfo = stateInfo;
}
public static SeckillStateEnum stateOf(int index){
for (SeckillStateEnum state: values()){
if (state.getState() == index){
return state;
}
}
return null;
}
}
3、進行service層的spring配置跪帝,注入service
~今膊、掃描service包下的注解
~、配置事務(wù)伞剑,在事務(wù)中注入數(shù)據(jù)庫連接池
~斑唬、開啟基于注解的聲明式事務(wù)
<?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-4.3.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.3.xsd ">
<!--掃描service包下所有使用注解的類型 -->
<context:component-scan base-package="com.seckill.service"></context:component-scan>
<!-- 配置事務(wù)管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- 注入數(shù)據(jù)庫連接池 -->
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 開啟基于注解的聲明式事務(wù) -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在配置過程中應(yīng)該注意xml的頭信息
4、service層的測試
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration({"classpath:spring/spring-dao.xml", "classpath:spring/spring-service.xml"})
public class SeckillServiceTest {
private org.slf4j.Logger logger = LoggerFactory.getLogger(this.getClass());
@Resource
private SeckillService seckillService;
@Test
public void executeSeckillProcedure() throws Exception {
long seckillId = 1001L;
long userphone = 18126745520L;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
if (exposer.isExposed()) {
String md5 = exposer.getMd5();
SeckillExecution execution = seckillService.executeSeckillProcedure(seckillId, userphone, md5);
logger.info(execution.getStateInfo());
}
}
@Test
public void getPageCount() throws Exception {
System.out.println(seckillService.getPageCount());
}
@Test
public void getSeckillList() throws Exception {
List<Seckill> list = seckillService.getSeckillList(1);
logger.info(list.toString());
System.out.println(list.toString());
}
@Test
public void getById() throws Exception {
long seckillId = 1000L;
Seckill seckill = seckillService.getById(seckillId);
System.out.println(seckill.toString());
}
@Test
public void exportSeckillUrl() throws Exception {
long seckillId = 1000L;
Exposer exposer = seckillService.exportSeckillUrl(seckillId);
System.out.println(exposer.toString());
}
@Test
public void executeSeckill() throws Exception {
long seckillId = 1000L;
long userPhone = 125906441181L;
String md5 = "ab977232c7bfb60cf33df852e171edd9";
try {
SeckillExecution seckillExecution = seckillService.executeSeckill(seckillId, userPhone, md5);
logger.info("result={}", seckillExecution);
} catch (SeckillCloseException e) {
logger.error(e.getMessage());
} catch (RepeaKillException e) {
logger.error(e.getMessage());
} catch (SeckillException e) {
logger.error(e.getMessage());
}
}