一步一步的搭建JAVA WEB項(xiàng)目,采用Maven構(gòu)建,基于MYBatis+Spring+Spring MVC+Bootstrap技術(shù)的秒殺項(xiàng)目
學(xué)習(xí)的視頻:http://www.imooc.com/learn/587
創(chuàng)建Maven項(xiàng)目
- 創(chuàng)建目錄,執(zhí)行Maven命令
mvn archetype:generate -DgroupId=org.seckill -DartifactId=seckill -DarchetypeArtifactId=maven-archetype-webapp -DarchetypeCatalog=local
問題:Maven命令執(zhí)行到Generating Project in Batch mode 卡住,參考鏈接
- 將項(xiàng)目導(dǎo)入到IDEA工具中
- 修改項(xiàng)目配置
- 修改web.xml中的servlet版本,默認(rèn)是2.3,其不支持JSP的EL表達(dá)式。從Tomcat中的示例的web.xml中拷貝3.0的版本配置到項(xiàng)目中
- 補(bǔ)全目錄。項(xiàng)目的main目錄下創(chuàng)建java目錄,在src目錄下創(chuàng)建test目錄,test目錄下創(chuàng)建java和sources目錄
- 打開pom.xml,進(jìn)行依賴的配置
- 單元測(cè)試依賴:Junit4
- 日志依賴:slf4j+logback。(lf4j是規(guī)范/接口,log4j,common-logging,logback是日志的實(shí)現(xiàn))
- 數(shù)據(jù)庫(kù)依賴:mysql-connector-java、c3p0
- DAO框架:mybatis依賴:mybatis
- Servlet web相關(guān)依賴:standard、jstl、jackson-databind、servlet-api
- Spring依賴:spring-core、spring-beans、spring-context、spring-jdbc、spring-tx、spring-web、spring-webmvc、spring-test
<dependencies> <!---3.0使用編程方式,4.0使用注解方式--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.12</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.1</version> </dependency> <!--實(shí)現(xiàn)slf4j接口并進(jìn)行整合--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.1</version> </dependency> <!--數(shù)據(jù)庫(kù)相關(guān)依賴--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.35</version> <scope>runtime</scope> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <!--DAO框架:mybatis依賴--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.3.0</version> </dependency> <!--mybatis自身實(shí)現(xiàn)的spring的整合依賴--> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.3</version> </dependency> <!--servlet web相關(guān)依賴--> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.5.4</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> </dependency> <!--Spring依賴--> <!--spring核心--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.1.7.RELEASE</version> </dependency> <!--spring的DAO層依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.1.7.RELEASE</version> </dependency> <!--spring的WEB層依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.1.7.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.7.RELEASE</version> </dependency> <!--spring的單元測(cè)試依賴--> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.1.7.RELEASE</version> </dependency> </dependencies>
數(shù)據(jù)庫(kù)的設(shè)計(jì)
- 在項(xiàng)目main目錄下創(chuàng)建sql目錄,新建 schema.sql,作為數(shù)據(jù)庫(kù)的創(chuàng)建腳本
- 腳本代碼如下:
-- 數(shù)據(jù)庫(kù)初始化腳本 -- 創(chuàng)建數(shù)據(jù)庫(kù) CREATE DATABASE seckill; -- 使用數(shù)據(jù)庫(kù) use seckill; -- 創(chuàng)建秒殺庫(kù)存表:使用InnoDB引擎,其支持事務(wù)。主鍵自增設(shè)置為從1000開始,字符格式設(shè)置為UTF8 CREATE TABLE seckill( seckill_id bigint NOT NULL AUTO_INCREMENT COMMENT '商品庫(kù)存id', name varchar(120) NOT NULL COMMENT '商品名稱', number int NOT NULL COMMENT '庫(kù)存數(shù)量', start_time timestamp NOT NULL COMMENT '秒殺開啟時(shí)間', end_time timestamp NOT NULL COMMENT '秒殺結(jié)束時(shí)間', create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', PRIMARY KEY (seckill_id), KEY idx_start_time(start_time), KEY idx_end_time(end_time), KEY idx_create_time(create_time) )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒殺庫(kù)存表'; -- 秒殺成功明細(xì)表 CREATE TABLE success_killed( seckill_id bigint NOT NULL COMMENT '秒殺商品id', user_phone int NOT NULL COMMENT '用戶手機(jī)號(hào)', state tinyint NOT NULL COMMENT '狀態(tài)標(biāo)示:-1指無效,0指成功,1指已付款', create_time timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間', PRIMARY KEY (seckill_id,user_phone), KEY idx_create_time(create_time) )ENGINE=InnoDB AUTO_INCREMENT=1000 DEFAULT CHARSET=utf8 COMMENT='秒殺成功明細(xì)表'; -- 初始化數(shù)據(jù) INSERT INTO seckill(name,number,start_time,end_time) VALUES ('1000元秒殺iphone6',100,'2016-06-28 00:00:00','2016-06-29 00:00:00'), ('500元秒殺iphone5',200,'2016-06-28 00:00:00','2016-06-29 00:00:00'), ('200元秒殺小米4',300,'2016-06-28 00:00:00','2016-06-29 00:00:00'), ('100元秒殺紅米note',400,'2016-06-28 00:00:00','2016-06-29 00:00:00'); -- show create table seckill; -- 為什么手寫DDL,記錄每次上線的DDL修改
DAO實(shí)體和接口
創(chuàng)建實(shí)體包org.seckill.entity
創(chuàng)建DAO包org.seckill.dao
-
創(chuàng)建SecKill實(shí)體類,生成getter和setter,重寫toString
private long secKillId; private String name; private int number; private Date startTime; private Date endTime; private Date createTime;
-
創(chuàng)建SuccessKilled實(shí)體類,生成getter和setter,重寫toString
private long secKillId; private long userPhone; private short state; private Date createTime;
-
創(chuàng)建DAO接口SecKillDao,添加減庫(kù)存,根據(jù)ID查詢秒殺對(duì)象,查詢秒殺商品列表方法
/** * 減庫(kù)存 * @param secKillId * @param killTime * @return如果影響行數(shù)大于1,表示更新的記錄行數(shù) */ int reduceNumber(long secKillId,Date killTime); /** * 根據(jù)id查詢秒殺對(duì)象 * @param secKillId * @return */ SecKill queryById(long secKillId); /** * 根據(jù)偏移量查詢秒殺商品列表 * @param offset * @param limit * @return */ List<SecKill> queryAll(int offset,int limit);
-
創(chuàng)建DAO接口SuccessKilledDao,添加插入購(gòu)買明細(xì),根據(jù)ID查詢購(gòu)買明細(xì)實(shí)體的方法
/** * 插入購(gòu)買明細(xì),可過濾重復(fù) * @param secKillId * @param userPhone * @return插入的行數(shù) */ int inertSuccessKilled(long secKillId,long userPhone); /** *根據(jù)ID查詢SuccessKilled并攜帶秒殺產(chǎn)品對(duì)象實(shí)體 * @param secKillId * @return */ SuccessKilled queryByIdWithSecKill(long secKillId);
-
基于MyBaits實(shí)現(xiàn)DAO接口
- 創(chuàng)建mybatis-config.xml全局配置文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <!--配置全局屬性--> <settings> <!--使用jdbc的getGeneratedKeys獲取數(shù)據(jù)庫(kù)自增主鍵值--> <setting name="useGenerateKeys" value="true"/> <!--使用列別名替換列名 默認(rèn)為true--> <setting name="useColumnLabel" value="true"/> <!--開啟駝峰命名轉(zhuǎn)換--> <setting name="mapUnderscoreCamelCase" value="true" </settings> </configuration>
- 創(chuàng)建mapper文件夾,用于存儲(chǔ)mybatis映射文件
- 創(chuàng)建SecKilledDao.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.seckill.dao.SecKillDao"> <!--為DAO接口方法提供sql語句配置--> <update id="reduceNumber"> <!--具體的sql--> update seckill set number = number -1 where seckill_id = #{secKillId} and start_time <![CDATA[ <= ]]> #{killTime} and end_time >= #{killTime} and number > 0; </update> <select id="queryById" resultType="SecKill" parameterType="long"> select seckill_id,name,number,start_time,end_time,create_time from seckill where seckill_id = #{secKillId} </select> <select id="queryAll" resultType="SecKill"> select seckill_id,name,number,start_time,end_time,create_time from seckill order by create_time desc limit #{offset},#{limit} </select> </mapper>
- 創(chuàng)建SuccessKilledDao.xml映射文件
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="org.seckill.dao.SuccessKilledDao"> <!--為DAO接口方法提供sql語句配置--> <insert id="insertSuccessKilled"> <!--ignore忽略主鍵沖突--> insert ignore into success_killed(seckill_id,user_phone) values (#{secKilled},#{userPhone}) </insert> <select id="queryByIdWithSecKill" resultType="SuccessKilled"> <!--根據(jù)id查詢seccessKilled并攜帶seckill實(shí)體 如何告訴mybatis把結(jié)果映射到successkilled同時(shí)映射到seckill屬性 mybatis可以自由控制sql--> select sk.seckill_id, sk.user_phone, sk.create_time, sk.state, s.seckill_id "seckill.seckill_id", s.name "seckill.name", s.number "seckill.number", s.start_time "seckill.start_time", s.end_time "seckill.end_time", s.create_time "seckill.crate_time" from success_killed sk inner join seckill s on sk.seckill_id = s.seckill_id where sk.seckill_id = #{secKillId} </select> </mapper>
-
mybatis整合spring
- 創(chuàng)建spring文件,用于存儲(chǔ)spring配置文件
- 創(chuàng)建spring-dao.xml配置文件
- 創(chuàng)建jdbc.properties配置文件,用于存儲(chǔ)數(shù)據(jù)庫(kù)相關(guān)信息
``` driver=com.mysql.jdbc.Driver url=jdbc:mysql://127.0.0.1:3306/seckill?useUnicode=true&characterEncoding=utf-8 username=root password=purple ```
- 在spring-dao.xml配置文件中進(jìn)行四個(gè)步驟的配置
- 配置數(shù)據(jù)庫(kù)相關(guān)參數(shù)
- 配置數(shù)據(jù)庫(kù)連接池
- 配置sqlSessionFactory對(duì)象
- 配置掃描dao接口包,動(dòng)態(tài)實(shí)現(xiàn) dao接口,并注入到spring容器中
<?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-3.1.xsd"> <!--配置整合mybatis過程--> <!--配置數(shù)據(jù)庫(kù)相關(guān)參數(shù)--> <context:property-placeholder location="classpath:jdbc.properties"/> <!--數(shù)據(jù)庫(kù)連接池--> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${driver" /> <property name="jdbcUrl" value="${url}"/> <property name="user" value="${username}"/> <property name="password" value="${password}"/> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <!--關(guān)閉連接后不自動(dòng)commit--> <property name="autoCommitOnClose" value="false"/> <!--獲取連接超時(shí)的時(shí)間--> <property name="checkoutTimeout" value="1000"/> <!--獲取連接失敗的重試次數(shù)--> <property name="acquireRetryAttempts" value="2"/> </bean> <!--配置sqlSessionFactory對(duì)象--> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <!--注入數(shù)據(jù)庫(kù)連接池--> <property name="dataSource" value="dataSource"/> <!--配置mybatis全局配置文件--> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!--掃描entity包,使用別名--> <property name="typeAliasesPackage" value="org.seckill.entity"/> <!--掃描sql配置文件--> <property name="mapperLocations" value="mapper/*.xml"/> </bean> <!--配置掃描dao接口包,動(dòng)態(tài)實(shí)現(xiàn) dao接口,并注入到spring容器中--> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <!--注入sqlSessionFactory對(duì)象--> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <!--給出掃描Dao接口包--> <property name="basePackage" value="org.seckill.dao"/> </bean> </beans>
-
Junit4與Spring進(jìn)行整合,進(jìn)行Junit4單元測(cè)試
- 創(chuàng)建SecKillDao的單元測(cè)試類
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring/spring-dao.xml") public class SecKillDaoTest { //注入DAO實(shí)現(xiàn)類依賴 @Resource private SecKillDao secKillDao; @Test public void testReduceNumber() throws Exception { Date killTime = new Date(); int result = secKillDao.reduceNumber(1000L,killTime); System.out.println(result); } @Test public void testQueryById() throws Exception { long id = 1000; SecKill secKill = secKillDao.queryById(id); System.out.println(secKill.getName()); } @Test public void testQueryAll() throws Exception { List<SecKill> secKillList = secKillDao.queryAll(0,1000); for(SecKill row : secKillList){ System.out.println(row.toString()); } } }
- 創(chuàng)建SuccessKilledDao的單元測(cè)試類
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration("classpath:spring/spring-dao.xml") public class SuccessKilledDaoTest { @Resource private SuccessKilledDao successKilledDao; @Test public void testInertSuccessKilled() throws Exception { int result = successKilledDao.insertSuccessKilled(1000L,28059830451L); System.out.println(result); } @Test public void testQueryByIdWithSecKill() throws Exception { SuccessKilled successKilled = successKilledDao.queryByIdWithSecKill(1000L,2147483647L); System.out.println(successKilled.toString()); } }
- 學(xué)習(xí)點(diǎn)
- 單元測(cè)試類可以利用IDEA的快捷鍵,直接在要測(cè)試的類中進(jìn)行代碼的生成
- mybatis的傳參,需要在DAO接口方法的形參中使用@Param注解進(jìn)行指明
業(yè)務(wù)層設(shè)計(jì)
- 秒殺業(yè)務(wù)接口設(shè)計(jì)
創(chuàng)建業(yè)務(wù)包service
創(chuàng)建數(shù)據(jù)傳輸實(shí)體包dto
創(chuàng)建異常包exception
-
創(chuàng)建dto實(shí)體
- 創(chuàng)建暴露秒殺地址DTO:Exposer
public class Exposer { /** * 是否開啟秒殺 */ private boolean exposed; /** * 秒殺ID */ private long secKillId; /** * 一種加密措施 */ private String md5; /** *系統(tǒng)當(dāng)前時(shí)間(毫秒值) */ 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 now, long start, long end) { this.exposed = exposed; 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 long getSecKillId() { return secKillId; } public void setSecKillId(long secKillId) { this.secKillId = secKillId; this.secKillId = secKillId; } public String getMd5() { return md5; } public void setMd5(String md5) { this.md5 = md5; } 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; } }
- 創(chuàng)建封裝秒殺執(zhí)行后結(jié)果DTO:SecKillExecution
public class SecKillExecution { private long secKillId; /** * 秒殺執(zhí)行結(jié)果狀態(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)建異常類
- 創(chuàng)建業(yè)務(wù)相關(guān)異常:SecKillException
public class SecKillException extends RuntimeException{ public SecKillException(String message) { super(message); } public SecKillException(String message, Throwable cause) { super(message, cause); } }
- 創(chuàng)建重復(fù)秒殺異常類:RepeatKillException
public class RepeatKillException extends SecKillException{ public RepeatKillException(String message, Throwable cause) { super(message, cause); } public RepeatKillException(String message) { super(message); } }
- 創(chuàng)建秒殺關(guān)閉異常類:SecKillCloseExce
ption
public class SecKillCloseException extends SecKillException{ public SecKillCloseException(String message) { super(message); } public SecKillCloseException(String message, Throwable cause) { super(message, cause); } }
-
創(chuàng)建SecKillService業(yè)務(wù)接口:SecKillService
- 創(chuàng)建查詢所有的秒殺記錄方法:getSecKillList
- 創(chuàng)建查詢單個(gè)秒殺記錄方法:getById
- 創(chuàng)建秒殺開啟時(shí)輸出秒殺接口地址方法:exportSecKillUrl
- 創(chuàng)建執(zhí)行秒殺操作方法:executeSecKill
public interface SecKillService { /** * 查詢所有的秒殺記錄 * @return */ List<SecKill> getSecKillList(); /** * 查詢單個(gè)秒殺記錄 * @param secKillId * @return */ SecKill getById(long secKillId); /** * 秒殺開啟時(shí)輸出秒殺接口地址 * 否則輸出系統(tǒng)時(shí)間和秒殺時(shí)間 * 防止用戶猜測(cè)出秒殺地址的規(guī)律 * @param secKillId */ Exposer exportSecKillUrl(long secKillId); /** *執(zhí)行秒殺操作 * @param secKillId * @param userPhone * @param md5 */ SecKillExecution executeSecKill(long secKillId,long userPhone,String md5) throws SecKillException,RepeatKillException,SecKillCloseException; }
-
業(yè)務(wù)接口設(shè)計(jì)的學(xué)習(xí)點(diǎn)
- 站在使用者的角度進(jìn)行設(shè)計(jì)接口,不要冗余設(shè)計(jì)
- 方法定義粒度,目的明確。非常友好的讓使用者調(diào)用接口
- 參數(shù)要簡(jiǎn)煉
- 返回類型要清晰
-
秒殺業(yè)務(wù)接口實(shí)現(xiàn)
- 新建enums枚舉包,將數(shù)據(jù)字典放到枚舉中
- 在枚舉包下創(chuàng)建秒殺狀態(tài)枚舉:SecKillStatEnum
public enum SecKillStatEnum { SUCCESS(1,"秒殺成功"), END(0,"秒殺結(jié)束"), REPEAT(-1,"重復(fù)秒殺"), INNER_ERROR(-2,"系統(tǒng)異常"), DATA_REWRITE(-3,"數(shù)據(jù)篡改"); private int state; private String stateInfo; SecKillStatEnum(int state, String stateInfo) { this.state = state; this.stateInfo = stateInfo; } public int getState() { return state; } public String getStateInfo() { return stateInfo; } public static SecKillStatEnum stateOf(int index){ for(SecKillStatEnum state : values()) { if(state.getState() == index){ return state; } } return null; } }
- 在service包下新建impl包
- 創(chuàng)建SecKillServiceImpl實(shí)現(xiàn)類,實(shí)現(xiàn)SecKillService接口方法
public class SecKillServiceImpl implements SecKillService{ private Logger logger = LoggerFactory.getLogger(SecKillService.class); private SecKillDao secKillDao; private SuccessKilledDao successKilledDao; //混淆字符,用于混淆MD5 private final String salt = "sdlkjs#$#$dfowierlkjafdmv232k3j@@##$"; @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(null == secKill){ 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()); } //轉(zhuǎn)化特定字符串的過程,不可逆 String md5 = getMD5(secKillId); return new Exposer(true,md5,secKillId); } @Override public SecKillExecution executeSecKill(long secKillId, long userPhone, String md5) throws SecKillException, RepeatKillException, SecKillCloseException { if(null == md5 || md5.equals(getMD5(secKillId))){ throw new SecKillException("seckill datarewirte"); } try{ //執(zhí)行秒殺邏輯,減庫(kù)存,記錄購(gòu)買行為 Date nowTime = new Date(); //減庫(kù)存 int updateCount = secKillDao.reduceNumber(secKillId,nowTime); if(updateCount <= 0){ //沒有更新到記錄,秒殺結(jié)束 throw new SecKillCloseException("seckill is Closed"); }else{ //記錄購(gòu)買行為 int insertCount = successKilledDao.insertSuccessKilled(secKillId,userPhone); //唯一:secKillId,userPhone if(insertCount <= 0){ //重復(fù)秒殺 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); //所有編譯期異常,轉(zhuǎn)化為運(yùn)行期異常 throw new SecKillException("seckill inner error:" + e.getMessage()); } } /** * 生成MD5 * @param secKillId * @return */ private String getMD5(long secKillId){ String base = secKillId + "/" + salt; String md5 = DigestUtils.md5DigestAsHex(base.getBytes()); return md5; } }
-
基于Spring托管Service實(shí)現(xiàn)類
- 創(chuàng)建Spring的service配置spring-service.xml,進(jìn)行service包下的注解類型的掃描配置
<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-3.1.xsd"> <!--掃描service包下所有注解的類型--> <context:component-scan base-package="org.seckill.service"/> </beans>
- 在service實(shí)現(xiàn)類中添加上@Service的注解,在類中的dao對(duì)象添加上@Autowired的注解
-
配置并使用Spring聲明式事務(wù)
- 在spring-service.xml中添加上配置事務(wù)管理器
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!--注入數(shù)據(jù)庫(kù)連接池--> <property name="dataSource" ref="dataSource"/> </bean>
- 在spring-service.xml中添加上配置基于注解的聲明式事務(wù)
<tx:annotation-driven transaction-manager="transactionManager"/>
- 在業(yè)務(wù)類的executeSecKill方法中添加上@Transactional事務(wù)注解
- 學(xué)習(xí)點(diǎn):使用注解控制事務(wù)方法的優(yōu)點(diǎn)
- 開發(fā)團(tuán)隊(duì)達(dá)到一致約定,明確標(biāo)注事務(wù)方法的編程風(fēng)格
- 保證事務(wù)方法的執(zhí)行時(shí)間盡可能短,不要穿插其他網(wǎng)絡(luò)操作RPC/HTTP請(qǐng)求,或者剝離到事務(wù)方法外部
- 不是所有的方法都需要事務(wù),如只有一條修改操作,只讀操作就不需要事務(wù)控制
-
Service集成測(cè)試
- 添加上logback的日志配置文件logback.xml
<?xml version="1.0" encoding="UTF-8"?> <configuration debug="true"> <!-- ch.qos.logback.core.ConsoleAppender 控制臺(tái)輸出 --> <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%-5level] %d{HH:mm:ss.SSS} [%thread] %logger{36} - %msg%n</pattern> </encoder> </appender> <!-- 日志級(jí)別 --> <root> <level value="debug" /> <appender-ref ref="STDOUT" /> </root> </configuration>
- 使用IDEA為SecKillService業(yè)務(wù)接口創(chuàng)建單元測(cè)試類SecKillServiceTest
- 編寫單元測(cè)試方法
@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 testGetSecKillList() throws Exception { List<SecKill> list = secKillService.getSecKillList(); logger.info("list={}",list); } @Test public void testGetById() throws Exception { SecKill secKill = secKillService.getById(1000L); logger.info("secKill:{}",secKill); } /** * 測(cè)試完整業(yè)務(wù),注意集成測(cè)試代碼完整邏輯,注意可重復(fù)執(zhí)行 * @throws Exception */ @Test public void testSecKillLogic() throws Exception { long id = 1000L; Exposer exposer = secKillService.exportSecKillUrl(id); if(exposer.isExposed()){ logger.info("exposer={}",exposer); long phone = 18059830432L; SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,exposer.getMd5()); logger.info("secKillExecution:{}",secKillExecution); }else{ //秒殺未開始 logger.warn("exposer={}",exposer); } } @Test public void testExportSecKillUrl() throws Exception { long id = 1000L; Exposer exposer = secKillService.exportSecKillUrl(id); logger.info("exposer={}",exposer); } @Test public void testExecuteSecKill() throws Exception { long id = 1000L; long phone = 18059830452L; String md5 = "f1974250b060f51c4a8e48df67232d53"; SecKillExecution secKillExecution = secKillService.executeSecKill(id,phone,md5); logger.info("secKillExecution:{}",secKillExecution); } }
- 單元測(cè)試的學(xué)習(xí)點(diǎn)
- 集成測(cè)試的業(yè)務(wù)邏輯的完整性
- 注意測(cè)試的可重復(fù)執(zhí)行
WEB層設(shè)計(jì)
設(shè)計(jì)Restful接口
-
SpringMVC整合Spring
- 在web.xml中配置DispatcherServlet
- 創(chuàng)建web包
- 創(chuàng)建spring-web.xml配置文件
- 在spring-web.xml進(jìn)行SpringMVC的配置
- 開啟SpringMVC注解模式
- servlet-mapping映射路徑
- 配置jsp顯示viewResolver
- 掃描web相關(guān)的bean
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:conext="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/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd"> <!--配置Spring MVC--> <!--開啟SpringMVC注解模式--> <!--簡(jiǎn)化配置 1、自動(dòng)注冊(cè)DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter 2、提供一系列功能:數(shù)據(jù)綁定,數(shù)字和日期的轉(zhuǎn)化@NumberFormat,@DataTimeFormat xml,json默認(rèn)讀寫支持 --> <mvc:annotation-driven/> <!--servlet-mapping映射路徑--> <!--靜態(tài)資源默認(rèn)servlet配置 1、加入對(duì)靜態(tài)資源的處理:js,css,img 2、允許使用/做整體映射 --> <mvc:default-servlet-handler/> <!--配置jsp顯示viewResolver--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp"/> <property name="suffix" value=".jsp"/> </bean> <!--掃描web相關(guān)的bean--> <conext:component-scan base-package="org.seckill.web"/> </beans>
-
實(shí)現(xiàn)秒殺相關(guān)的Restful接口
- 創(chuàng)建控制類SecKillController,實(shí)現(xiàn)獲取列表,獲取單條數(shù)據(jù),獲取系統(tǒng)時(shí)間,獲取秒殺地址,秒殺的方法
@Controller @RequestMapping("/seckill/")//模塊/資源 public class SecKillController { private final Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private SecKillService secKillService; @RequestMapping(name="/list",method= RequestMethod.GET) public String list(Model model){ List<SecKill> list = secKillService.getSecKillList(); model.addAttribute("list",list); return "list"; } @RequestMapping(value="/{secKillId}/detail",method=RequestMethod.GET) public String detail(@PathVariable("secKillId") Long secKillId,Model model){ if(secKillId == null){ return "redirect:/seckill/list"; } SecKill secKill = secKillService.getById(secKillId); if(secKill == null){ return "redirect:/seckill/list"; } model.addAttribute("secKill",secKill); return "detail"; } @RequestMapping(value="/{secKillId}/exposer",method = RequestMethod.POST, produces = {"application/json;charset=utf-8"}) @ResponseBody public SecKillResult<Exposer> exposer(@PathVariable("secKillId") Long secKillId){ SecKillResult<Exposer> result = null; try{ Exposer exposer = secKillService.exportSecKillUrl(secKillId); result = new SecKillResult<Exposer>(true,exposer); }catch(Exception e){ logger.error(e.getMessage(),e); result = new SecKillResult<Exposer>(false,e.getMessage()); } return result; } @RequestMapping(value="/{secKillId}/{md5}/execution", method = RequestMethod.POST, produces = {"application/json;charset=utf-8"}) public SecKillResult<SecKillExecution> excute(@PathVariable("secKillId") Long secKillId, @PathVariable("md5") String md5, @CookieValue(value="killPhone",required = false) Long userPhone){ //springmvc valid if(userPhone == null){ return new SecKillResult<SecKillExecution>(false,"未注冊(cè)"); } SecKillResult<SecKillExecution> result = null; try{ SecKillExecution secKillExecution = secKillService.executeSecKill(secKillId,userPhone,md5); result = new SecKillResult<SecKillExecution>(true,secKillExecution); }catch(RepeatKillException e){ SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.REPEAT); result = new SecKillResult<SecKillExecution>(false,secKillExecution); }catch(SecKillCloseException e){ SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.END); result = new SecKillResult<SecKillExecution>(false,secKillExecution); }catch(Exception e){ logger.error(e.getMessage(),e); SecKillExecution secKillExecution = new SecKillExecution(secKillId, SecKillStatEnum.INNER_ERROR); result = new SecKillResult<SecKillExecution>(false,secKillExecution); } return result; } @RequestMapping(value="/time/now",method=RequestMethod.GET) public SecKillResult<Long> time(){ Date now = new Date(); return new SecKillResult<Long>(true,now.getTime()); } }
- 基于Bootstrap開發(fā)頁面結(jié)構(gòu)
-
創(chuàng)建jsp文件夾,創(chuàng)建common/header.jsp,common/tag.jsp,list.jsp,detail.jsp,并引入bootstrap框架,jquery、cookie、countdown插件,可以從百度和bootcss的CDN中引入插件。
創(chuàng)建js文件seckill.js,進(jìn)行登錄、計(jì)時(shí)的交互邏輯的編碼,并在詳細(xì)頁面中引入
var seckill = { //封裝秒殺相關(guān)ajax的url URL: { now: function(){ return '/seckill/time/now'; }, exposer: function(id){ return '/seckill/' + id + '/exposer'; }, execution : function(id,md5){ return '/seckill/' + id + '/' + md5 + '/execution'; } }, //處理秒殺邏輯 handleSecKillKill: function(secKillId,node){ node.hide().html('<button class="btn btn-primary btn-lg" id="killBtn">開始秒殺</button>'); $.post(seckill.URL.exposer(secKillId),{},function(result){ if(result && result.success){ var exposer = result.data; if(exposer.exposed){ //開啟秒殺 //獲取秒殺地址 var killUrl = seckill.URL.execution(secKillId,exposer.md5); console.log('killUrl:',killUrl); //綁定一次點(diǎn)擊事件 $('#killBtn').one('click',function(){ //執(zhí)行秒殺請(qǐng)求 $(this).addClass('disabled'); $.post(killUrl,{},function(result){ if(result && result.success){ var killResult = result.data; var state = killResult.state; var stateInfo = killResult.stateInfo; node.html('<span class="label label-success">'+stateInfo+'</span>'); } }); }); node.show(); }else{ //未開啟秒殺 //重新計(jì)算計(jì)時(shí)邏輯 seckill.countdown(secKillId,exposer.now,exposer.start,exposer.end); } }else{ console.error('result:',result); } }); }, //計(jì)時(shí) countdown: function(secKillId,nowTime,startTime,endTime){ var $secKillBox = $('#seckill-box'); if(nowTime > endTime){ $secKillBox.html('秒殺結(jié)束'); }else if(nowTime < startTime){ $secKillBox.html('秒殺未開始'); var killTime = new Date(startTime + 1000); $secKillBox.countdown(killTime,function(event){ var format = event.strftime('秒殺倒計(jì)時(shí):%D天 %H時(shí) %M分 %S秒'); $secKillBox.html(format); }).on('finish.countdown',function(){ //獲取秒殺地址,控制實(shí)現(xiàn)邏輯,執(zhí)行秒殺 seckill.handleSecKillKill(secKillId,$secKillBox); }); }else{ //秒殺開始 seckill.handleSecKillKill(secKillId,$secKillBox); } }, //驗(yàn)證手機(jī)號(hào) validatePhone: function(phone){ if(phone && phone.length == 11 && !isNaN(phone)){ return true; }else{ return false; } }, //詳情頁秒殺邏輯 detail: { //詳情頁初始化 init: function(params){ //用戶手機(jī)驗(yàn)證和登錄,計(jì)時(shí)交互 //規(guī)劃交互流程 //在cookie中查找手機(jī)號(hào) var killPhone = $.cookie('killPhone'), startTime = params.startTime, endTime = params.endTime, secKillId = params.secKillId; //驗(yàn)證手機(jī)號(hào) if(!seckill.validatePhone(killPhone)){ var killPhoneModal = $('#killPhoneModal'); killPhoneModal.modal({ show: true, backdrop: 'static',//禁止位置關(guān)閉 keyboard: false//關(guān)閉鍵盤事件 }); $('#killPhoneBtn').click(function(){ var inputPhone = $('#killPhoneKey').val(); if(seckill.validatePhone(inputPhone)){ //電話寫入cookie $.cookie('killPhone',inputPhone,{expires:7,path: '/seckill'}) window.location.reload(); }else{ //正常下會(huì)有一個(gè)前端字典 $('#killPhoneMessage').hide().html('<label class="label label-danger">手機(jī)號(hào)碼錯(cuò)誤</label>').show(300); } }); } //用戶已經(jīng)登錄 //計(jì)時(shí)交互 $.get(seckill.URL.now(),function(result){ if(result && result.success){ var nowTime = result.data; seckill.countdown(secKillId,nowTime,startTime,endTime); }else{ consolw.error('result:',result); } }); } } }
- 在detail.jsp頁面中引入seckill.js文件,并進(jìn)行初始化
<script type="text/javascript"> $(function(){ //使用EL表達(dá)式傳入?yún)?shù) seckill.detail.init({ secKillId: ${secKill.secKillId}, startTime: ${secKill.startTime.time}, endTime: ${secKill.endTime.time} }); }); </script>
-