文章所用例子参考慕课网相关课程。
使用IDEA创建一个maven的webapp项目,建好相应的项目结构。
-
main
-
java:java源码
-
com.dct.seckillDemo:包名,一般为groupId+artifactId
- dao:Data Access Object,数据访问对象,主要数据库操作相关方法。
- dto:主要是自定义的pojo,用于service层与web层之间的数据传输
- entity:实体包,是简单的java对象(pojo),与数据库中的表对应。
- service:主要的业务逻辑层
- web:即控制器层,接受请求并调用服务,返回数据和视图。
-
-
resources
- mapper:与mybatis数据库操作相关的xml文件
- spring:spring相关配置文件
-
webapp
- resources:项目的静态资源,如js css images等。
- WEB-INF:外部浏览器无法访问,只有内部才能访问
-
-
test
- java:java测试相关代码
- resources:测试所需资源文件
然后导入项目依赖的jar包
pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.dct</groupId>
<artifactId>seckillDemo</artifactId>
<packaging>war</packaging>
<version>1.0-SNAPSHOT</version>
<name>seckillDemo Maven Webapp</name>
<url>http://maven.apache.org</url>
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.11</version>
<scope>test</scope>
</dependency>
<!--补全项目依赖-->
<!--1.日志 java日志有:slf4j,log4j,logback,common-logging
slf4j:是规范/接口
日志实现:log4j,logback,common-logging
使用:slf4j+logback
-->
<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>
<!--实现slf4j接口并整合-->
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.1.1</version>
</dependency>
<!--1.数据库相关依赖-->
<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>
<!--2.dao框架:MyBatis依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.3.0</version>
</dependency>
<!--mybatis自身实现的spring整合依赖-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.2.3</version>
</dependency>
<!--3.Servlet web相关依赖-->
<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>
<!--4:spring依赖-->
<!--1)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>
<!--2)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>
<!--3)springweb相关依赖-->
<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>
<!--4)spring test相关依赖-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>4.1.7.RELEASE</version>
</dependency>
</dependencies>
<build>
<finalName>seckillDemo</finalName>
</build>
</project>
下面开始进行整合SSM,首进行mybatis和spring的整合。
在resources下新建jdbc.properties和mybatis-config.xml文件。
jdbc.properties(数据库连接相关参数)
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/seckill?useUnicode=true&characterEncoding=utf-8
jdbc.username=root
jdbc.password=root
mybatis-config.xml(mybatis全局属性的配置)
<?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的getGeneratekeys获取自增主键值-->
<setting name="useGeneratedKeys" value="true"/>
<!--使用列别名替换列名 默认值为true
select name as title(实体中的属性名是title) form table;
开启后mybatis会自动帮我们把表中name的值赋到对应实体的title属性中
-->
<setting name="useColumnLabel" value="true"/>
<!--开启驼峰命名转换Table:create_time到 Entity(createTime)-->
<setting name="mapUnderscoreToCamelCase" value="true"/>
</settings>
</configuration>
然后在spring包下新建spring-dao.xml文件,进行spring和mybatis的整合配置。
spring-dao.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.xsd">
<!--配置整合mybatis过程
1.配置数据库相关参数-->
<context:property-placeholder location="classpath:jdbc.properties"/>
<!--2.数据库连接池-->
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
<!--配置连接池属性-->
<property name="driverClass" value="${jdbc.driver}" />
<!-- 基本属性 url、user、password -->
<property name="jdbcUrl" value="${jdbc.url}" />
<property name="user" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<!--c3p0私有属性-->
<property name="maxPoolSize" value="30"/>
<property name="minPoolSize" value="10"/>
<!--关闭连接后不自动commit-->
<property name="autoCommitOnClose" value="false"/>
<!--获取连接超时时间-->
<property name="checkoutTimeout" value="1000"/>
<!--当获取连接失败重试次数-->
<property name="acquireRetryAttempts" value="2"/>
</bean>
<!--约定大于配置-->
<!--3.配置SqlSessionFactory对象-->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<!--往下才是mybatis和spring真正整合的配置-->
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
<!--配置mybatis全局配置文件:mybatis-config.xml-->
<property name="configLocation" value="classpath:mybatis-config.xml"/>
<!--扫描entity包,使用别名,多个用;隔开-->
<property name="typeAliasesPackage" value="com.dct.seckillDemo.entity"/>
<!--扫描sql配置文件:mapper需要的xml文件-->
<property name="mapperLocations" value="classpath:mapper/*.xml"/>
</bean>
<!--4:配置扫描Dao接口包,动态实现DAO接口,注入到spring容器-->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<!--注入SqlSessionFactory-->
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/>
<!-- 给出需要扫描的Dao接口-->
<!--<property name="basePackage" value="com.dct.seckillDemo.dao"/>-->
</bean>
<!--扫描dao包下所有使用注解的类型-->
<context:component-scan base-package="com.dct.seckillDemo.dao"/>
</beans>
在spring包下新建spring-service.xml文件,配置service层。
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.dct.seckillDemo.service"/>
<!--配置事务管理器-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!--注入数据库连接池-->
<property name="dataSource" ref="dataSource"/>
</bean>
<!--配置基于注解的声明式事务
默认使用注解来管理事务行为-->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
在spring包下新建spring-web.xml文件,配置控制器层。
spring-web.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:mvc="http://www.springframework.org/schema/mvc"
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/mvc
http://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!--配置spring mvc-->
<!--1,开启springmvc注解模式
a.自动注册DefaultAnnotationHandlerMapping,AnnotationMethodHandlerAdapter
b.默认提供一系列的功能:数据绑定,数字和日期的format@NumberFormat,@DateTimeFormat
c:xml,json的默认读写支持-->
<mvc:annotation-driven/>
<!--2.静态资源默认servlet配置-->
<!--
1).加入对静态资源处理:js,gif,png
2).允许使用 "/" 做整体映射
-->
<mvc:default-servlet-handler/>
<!--3:配置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>
<!--4:扫描web相关的controller-->
<context:component-scan base-package="com.dct.seckillDemo.web"/>
</beans>
配置web.xml文件
web.xml
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<!--用maven创建的web-app需要修改servlet的版本为3.1-->
<!--配置DispatcherServlet-->
<servlet>
<servlet-name>seckill-dispatcher</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<!--
配置SpringMVC 需要配置的文件
spring-dao.xml,spring-service.xml,spring-web.xml
Mybites -> spring -> springMvc
-->
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring/spring-*.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>seckill-dispatcher</servlet-name>
<!--默认匹配所有请求-->
<url-pattern>/</url-pattern>
</servlet-mapping>
<!--过滤器,解决字符编码问题-->
<filter>
<description>字符集过滤器</description>
<filter-name>encodingFilter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<description>字符集编码</description>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
目前为止,SSM整合工作就已经完成,项目目录结构如下。
SSM框架应用示例--秒杀系统
首先新建数据库及相应的数据表,在main下新建一个sql包,然后新建一个schema.sql文件
schema.sql
-- 数据库初始化脚本
-- 创建数据库
CREATE DATABASE seckill;
-- 使用数据库,只有INNODb支持事务
use seckill;
CREATE TABLE seckill(
`seckill_id` BIGINT NOT NUll AUTO_INCREMENT COMMENT '商品库存ID',
`name` VARCHAR(120) NOT NULL COMMENT '商品名称',
`number` int NOT NULL COMMENT '库存数量',
`start_time` TIMESTAMP NOT NULL COMMENT '秒杀开始时间',
`end_time` TIMESTAMP NOT NULL COMMENT '秒杀结束时间',
`create_time` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
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=10000 DEFAULT CHARSET=utf8 COMMENT='秒杀库存表';
-- 初始化数据
INSERT into seckill(name,number,start_time,end_time)
VALUES
('1000元秒杀iphone6',100,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('800元秒杀ipad',200,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('6600元秒杀mac book pro',300,'2016-01-01 00:00:00','2016-01-02 00:00:00'),
('7000元秒杀iMac',400,'2016-01-01 00:00:00','2016-01-02 00:00:00');
-- 秒杀成功明细表
-- 用户登录认证相关信息(简化为手机号)
CREATE TABLE success_killed(
`seckill_id` BIGINT NOT NULL COMMENT '秒杀商品ID',
`user_phone` BIGINT NOT NULL COMMENT '用户手机号',
`state` TINYINT NOT NULL DEFAULT -1 COMMENT '状态标识:-1:无效 0:成功 1:已付款 2:已发货',
`create_time` TIMESTAMP NOT NULL COMMENT '创建时间',
PRIMARY KEY(seckill_id,user_phone),/*联合主键*/
KEY idx_create_time(create_time)
)ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='秒杀成功明细表';
-- SHOW CREATE TABLE seckill;#显示表的创建信息
dao层开发
在entity包中新建两个实体类,属性分别与数据表一一对应
Seckill.java
public class Seckill
{
private long seckillId;
private String name;
private int number;
//该注解处理日期的格式化
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date startTime;
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date endTime;
@DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
private Date createTime;
public Seckill(){
}
public Seckill(String name, int number, Date startTime, Date endTime) {
this.name = name;
this.number = number;
this.startTime = startTime;
this.endTime = endTime;
}
public long getSeckillId() {
return seckillId;
}
public void setSeckillId(long seckillId) {
this.seckillId = seckillId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public Date getStartTime() {
return startTime;
}
public void setStartTime(Date startTime) {
this.startTime = startTime;
}
public Date getEndTime() {
return endTime;
}
public void setEndTime(Date endTime) {
this.endTime = endTime;
}
public Date getCreateTime() {
return createTime;
}
public void setCreateTime(Date createTime) {
this.createTime = createTime;
}
@Override
public String toString() {
return "Seckill{" +
"seckillId=" + seckillId +
", name='" + name + '\'' +
", number=" + number +
", startTime=" + startTime +
", endTime=" + endTime +
", createTime=" + createTime +
'}';
}
}
SuccessKilled.java
public class SuccessKilled
{
private long seckillId;
private long userPhone;
private short state;
private Date createTime;
//省略getter,setter和toString方法
}
在dao包下新建两个接口,定义数据库操作相关方法
seckillDao.java
@Repository
public interface SeckillDao
{
/**
* 减库存
* @param seckillId
* @param killTime
* @return 如果影响行数>1,表示更新库存的记录行数
*/
int reduceNumber(@Param("seckillId") long seckillId, @Param("killTime") Date killTime);
/**
* 根据id查询秒杀的商品信息
* @param seckillId
* @return
*/
Seckill queryById(long seckillId);
/**
* 根据偏移量查询秒杀商品列表
* @param offset
* @param limit
* @return
*/
List<Seckill> queryAll(@Param("offset") int offset, @Param("limit") int limit);
// 添加秒杀物品
boolean addItem(Seckill seckill);
// 删除秒杀物品
boolean deleteById(long seckillId);
// 更新秒杀物品
boolean updateItem(Seckill seckill);
}
SuccessKilledDao.java
@Repository
public interface SuccessKilledDao {
/**
* 插入购买明细,可过滤重复
* @param seckillId
* @param userPhone
* @return插入的行数
*/
int insertSuccessKilled(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
/**
* 根据秒杀商品的id查询明细SuccessKilled对象(该对象携带了Seckill秒杀产品对象)
* @param seckillId
* @return
*/
SuccessKilled queryByIdWithSeckill(@Param("seckillId") long seckillId, @Param("userPhone") long userPhone);
}
在resources包下的mapper保重创建xml文件,实现dao包接口中的方法。
SeckillDao.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--包名与dao包中的接口名对应-->
<mapper namespace="com.dct.seckillDemo.dao.SeckillDao">
<!--目的:为dao接口方法提供sql语句配置
即针对dao接口中的方法编写我们的sql语句-->
<!--
id与接口中的方法名一致
parameterType:参数类型,与接口方法中的参数类型一致;对于多个参数,可不写参数类型,但在dao接口中要用@Param注解
resultType:返回值类型,
-->
<update id="reduceNumber">
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 *
FROM seckill
WHERE seckill_id=#{seckillId}
</select>
<select id="queryAll" resultType="Seckill">
SELECT *
FROM seckill
ORDER BY create_time DESC
limit #{offset},#{limit}
</select>
<insert id="addItem" parameterType="Seckill">
INSERT INTO seckill(name,number,start_time,end_time)
VALUES (#{name},#{number},#{startTime},#{endTime})
</insert>
<delete id="deleteById" parameterType="long">
DELETE FROM seckill
WHERE seckill_id=#{seckillId}
</delete>
<update id="updateItem" parameterType="Seckill">
UPDATE seckill
set name=#{name},number=#{number},start_time=#{startTime},end_time=#{endTime}
WHERE seckill_id=#{seckillId}
</update>
</mapper>
SuccessKilledDao.xml
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.dct.seckillDemo.dao.SuccessKilledDao">
<insert id="insertSuccessKilled">
<!--当出现主键冲突时(即重复秒杀时),会报错;不想让程序报错,加入ignore-->
INSERT ignore INTO success_killed(seckill_id,user_phone,state)
VALUES (#{seckillId},#{userPhone},0)
</insert>
<select id="queryByIdWithSeckill" resultType="SuccessKilled">
<!--根据seckillId查询SuccessKilled对象,并携带Seckill对象-->
<!--如何告诉mybatis把结果映射到SuccessKill属性同时映射到Seckill属性-->
<!--可以自由控制SQL语句-->
SELECT
sk.seckill_id,
sk.user_phone,
sk.create_time,
sk.state,
s.seckill_id as "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.create_time"
FROM success_killed sk
INNER JOIN seckill s ON sk.seckill_id=s.seckill_id
WHERE sk.seckill_id=#{seckillId} and sk.user_phone=#{userPhone}
</select>
</mapper>
完成了dao层开发,接下来对到dao方法进行测试。点击我们要测试的类,快捷键ctrl+shift+t,创建新的测试,选择测试的路径及所要测试的方法。
SeckillDaoTest.java
/**
* Created by codingBoy on 16/11/27.
* 配置spring和junit整合,这样junit在启动时就会加载springIOC容器
*/
@RunWith(SpringJUnit4ClassRunner.class)
//告诉junit spring的配置文件
@ContextConfiguration({"classpath:spring/spring-dao.xml"})
public class SeckillDaoTest {
//注入Dao实现类依赖
@Resource
private SeckillDao seckillDao;
@Test
public void reduceNumber() throws Exception {
long seckillId=10000;
Date date=new Date();
int updateCount=seckillDao.reduceNumber(seckillId,date);
System.out.println(updateCount);
}
@Test
public void queryById() throws Exception {
long seckillId=10000;
Seckill seckill=seckillDao.queryById(seckillId);
System.out.println(seckill.getName());
System.out.println(seckill);
}
@Test
public void queryAll() throws Exception {
List<Seckill> seckills=seckillDao.queryAll(0,100);
for (Seckill seckill : seckills)
{
System.out.println(seckill);
}
}
@Test
public void addItem() throws Exception {
Seckill seckill = new Seckill("222",300,new Date(),new Date());
seckillDao.addItem(seckill);
}
@Test
public void deleteById() throws Exception {
seckillDao.deleteById(10011);
}
@Test
public void updateItem() throws Exception {
Seckill seckill = seckillDao.queryById(10018);
seckill.setNumber(500);
seckillDao.updateItem(seckill);
}
}
dao层开发过程中的注意点
- 主要xml文件与dao接口中的方法一一对应
- 对于与数据表对应的实体类,mybatis提供了自动生成的工具,详见官网
对所有的dao包中的方法进行测试后,就可进入service层方法的开发。
service层为主要的业务逻辑层,也是最复杂的部分,一般在service下新建一个接口,然后在impl包下放接口的实现类。
SeckillService.java
/**业务接口:站在使用者(程序员)的角度设计接口
* 三个方面:1.方法定义粒度,方法定义的要非常清楚2.参数,要越简练越好
* 3.返回类型(return 类型一定要友好/或者return异常,我们允许的异常)
* Created by codingBoy on 16/11/27.
*/
public interface SeckillService {
/**
* 查询全部的秒杀记录
* @return
*/
List<Seckill> getSeckillList();
/**
*查询单个秒杀记录
* @param seckillId
* @return
*/
Seckill getById(long seckillId);
//再往下,是我们最重要的行为的一些接口
/**
* 在秒杀开启时输出秒杀接口的地址,否则输出系统时间和秒杀时间
* @param seckillId
*/
Exposer exportSeckillUrl(long seckillId);
/**
* 执行秒杀操作,有可能失败,有可能成功,所以要抛出我们允许的异常
* @param seckillId
* @param userPhone
* @param md5
* @return
*/
SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
throws SeckillException,RepeatKillException,SeckillCloseException;
void addItem(Seckill seckill);
void deleteItemById(long seckillId);
void updateItemById(Seckill seckill);
}
SeckillServiceImpl.java
@Service
public class SeckillServiceImpl implements SeckillService
{
//日志对象
private Logger logger= LoggerFactory.getLogger(this.getClass());
//加入一个混淆字符串(秒杀接口)的salt,为了我避免用户猜出我们的md5值,值任意给,越复杂越好
private final String salt="shsdssljdd'l.";
//注入Service依赖
@Autowired //@Resource
private SeckillDao seckillDao;
@Autowired //@Resource
private SuccessKilledDao successKilledDao;
public List<Seckill> getSeckillList() {
return seckillDao.queryAll(0,100);
}
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 (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;
}
//秒杀是否成功,成功:减库存,增加明细;失败:抛出异常,事务回滚
@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);
//看是否该明细被重复插入,即用户是否重复秒杀
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);
//所以编译期异常转化为运行期异常
throw new SeckillException("seckill inner error :"+e.getMessage());
}
}
public void addItem(Seckill seckill) {
seckillDao.addItem(seckill);
}
public void deleteItemById(long seckillId) {
seckillDao.deleteById(seckillId);
}
public void updateItemById(Seckill seckill) {
seckillDao.updateItem(seckill);
}
}
以上代码中用到了dto、enums、exception包中自定义的类。
dto包下三个类
//省略getter,setter和toString方法
public class Exposer {
//是否开启秒杀
private boolean exposed;
//加密措施
private String md5;
private long seckillId;
//系统当前时间(毫秒)
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 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;
}
}
//省略getter,setter和toString方法
public class SeckillExecution {
private long seckillId;
//秒杀执行结果的状态
private int state;
//状态的明文标识
private String stateInfo;
//当秒杀成功时,需要传递秒杀成功的对象回去
private SuccessKilled successKilled;
//秒杀成功返回所有信息
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();
}
}
//将所有的ajax请求返回类型,全部封装成json数据
public class SeckillResult<T> {
//请求是否成功
private boolean success;
private T data;
private String error;
public SeckillResult(boolean success, T data) {
this.success = success;
this.data = data;
}
public SeckillResult(boolean success, String error) {
this.success = success;
this.error = error;
}
}
新建enums存放枚举类型
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;
}
}
新建exception存放自定义的异常类
public class RepeatKillException extends SeckillException {
public RepeatKillException(String message) {
super(message);
}
public RepeatKillException(String message, Throwable cause) {
super(message, cause);
}
}
public class SeckillException extends RuntimeException {
public SeckillException(String message) {
super(message);
}
public SeckillException(String message, Throwable cause) {
super(message, cause);
}
}
public class SeckillCloseException extends SeckillException{
public SeckillCloseException(String message) {
super(message);
}
public SeckillCloseException(String message, Throwable cause) {
super(message, cause);
}
}
完成service层,进行测试
控制层开发
在web包下新建控制类
SeckillController.java
/*
* /seckill/list 获取列表
* /seckill/{seckillId}/detail 物品详情
* /seckill/{seckillId}/exposer 获取物品秒杀地址
* /seckill/{seckillId}/{md5}/execution 执行物品秒杀
*
* /seckill/add get获取添加页面
* /seckill/add post向数据库中添加记录
* /seckill/{seckillId}/update get获取更新页面
* /seckill/update post更新数据
* /seckill//{seckillId}delete 删除数据
* */
@Controller
@RequestMapping("/seckill")//url:模块/资源/{}/细分
public class SeckillController
{
@Autowired
private SeckillService seckillService;
@RequestMapping(value = "/list",method = RequestMethod.GET)
public String list(Model model)
{
//list.jsp+mode=ModelAndView
//获取列表页
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 "forward:/seckill/list";
}
model.addAttribute("seckill",seckill);
return "detail";
}
@RequestMapping(value = "/add",method = {RequestMethod.GET})
public String addItemUI(){
return "add";
}
@RequestMapping(value = "/add",method = {RequestMethod.POST})
public String addItemInfo(Seckill seckill){
seckillService.addItem(seckill);
return "redirect:/seckill/list";
}
@RequestMapping(value = "/{sk.seckillId}/delete",method = {RequestMethod.GET})
public String deleteItem(@PathVariable("sk.seckillId") Long seckillId){
seckillService.deleteItemById(seckillId);
return "redirect:/seckill/list";
}
@RequestMapping(value = "/{sk.seckillId}/update",method = {RequestMethod.GET})
public String updateItemUI(@PathVariable("sk.seckillId") Long seckillId,Model model){
Seckill seckill = seckillService.getById(seckillId);
model.addAttribute("item",seckill);
return "update";
}
@RequestMapping(value = "/update",method = {RequestMethod.POST})
public String updateItem(Seckill seckill){
seckillService.updateItemById(seckill);
return "redirect:/seckill/list";
}
//ajax ,json暴露秒杀接口的方法
@RequestMapping(value = "/{seckillId}/exposer",
method = RequestMethod.GET,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<Exposer> exposer(@PathVariable("seckillId") Long seckillId)
{
SeckillResult<Exposer> result;
try{
Exposer exposer=seckillService.exportSeckillUrl(seckillId);
result=new SeckillResult<Exposer>(true,exposer);
}catch (Exception e)
{
e.printStackTrace();
result=new SeckillResult<Exposer>(false,e.getMessage());
}
return result;
}
@RequestMapping(value = "/{seckillId}/{md5}/execution",
method = RequestMethod.POST,
produces = {"application/json;charset=UTF-8"})
@ResponseBody
public SeckillResult<SeckillExecution> execute(@PathVariable("seckillId") Long seckillId,
@PathVariable("md5") String md5,
@CookieValue(value = "userPhone",required = false) Long userPhone)
{
if (userPhone==null)
{
return new SeckillResult<SeckillExecution>(false,"未注册");
}
SeckillResult<SeckillExecution> result;
try {
SeckillExecution execution = seckillService.executeSeckill(seckillId, userPhone, md5);
return new SeckillResult<SeckillExecution>(true, execution);
}catch (RepeatKillException e1)
{
SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.REPEAT_KILL);
return new SeckillResult<SeckillExecution>(true,execution);
}catch (SeckillCloseException e2)
{
SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.END);
return new SeckillResult<SeckillExecution>(true,execution);
}
catch (Exception e)
{
SeckillExecution execution=new SeckillExecution(seckillId, SeckillStatEnum.INNER_ERROR);
return new SeckillResult<SeckillExecution>(true,execution);
}
}
//获取系统时间
@RequestMapping(value = "/time/now",method = RequestMethod.GET)
@ResponseBody
public SeckillResult<Long> time()
{
Date now=new Date();
return new SeckillResult<Long>(true,now.getTime());
}
}
web层开发过程中的注意点
- 注意类级别与方法级别路径映射的区别
- 返回页面和返回json类型数据不同的注解方式
- 方法中参数的获取
目前为止,秒杀系统后端服务全部完成,前端相关代码就不在此贴出来,全部代码见github
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。