seckill项目创建流程---Service层

首先先赢明白service层的作用:Service层主要负责业务模块的逻辑应用设计,同样是先设计接口,再设计其实现的类,接着再是spring的配置其实现的关联,这样就可以在应用中调用service接口来进行业务处理

1、service接口设计

~、前台页面需要显示参与秒杀商品的信息:getSeckillList(showPage):根据现实的页码进行列表查询,查询秒杀记录
~、点击相应连接后前台显示具体商品信息:getById(sekillId):查询单个秒杀记录

package com.seckill.service;
public interface SeckillService {
    /**
     * 查询所有秒杀记录
     * @return
     */
    List<Seckill> getSeckillList(int showPage);

    /**
     * 在每页显示5条数据情况下
     * 获得总页数
     *
     * @return
     */
    int getPageCount();
    /**
     * 查询单个秒杀记录
     * @param seckillId
     * @return
     */
    Seckill getById(long seckillId);

    /**
     * 秒杀开启时输出秒杀接口地址
     * 否则输出系统时间和秒杀时间
     * @param seckillId
     */
    Exposer exportSeckillUrl(long seckillId);

    /**
     * 执行秒杀操作,有可能是失败的,失败的时候就抛出异常
     * @param seckillId 秒杀的商品Id
     * @param userPhone 手机号码
     * @param md5 md5加密值
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException,SeckillCloseException,RepeaKillException;

    /**
     * 执行存储过程操作
     * @param seckillId
     * @param userPhone
     * @param md5
     * @return
     */
    SeckillExecution executeSeckillProcedure(long seckillId, long userPhone,String md5);
}

当开始编写service接口时,需要思考一个问题,那就是后台反馈给前台的数据是一个怎样的json数据?数字状态码,或者是文字?
这里先解释几种对象:
~、PO: 也就是我们在为每一张数据库表写一个实体的类
~、VO, 对某个页面或者展现层所需要的数据,封装成一个实体类
~、BO, 就是业务对象,我也不是很了解
~、DTO, 跟VO的概念有点混淆,也是相当于页面需要的数据封装成一个实体类
~、POJO, 简单的无规则java对象
了解了这几种对象,便可以明白,我们需要向前台返回的可以是DTO对象,后台对所需要返回的数据进行封装。
因此对于service接口中的Esposer和SeckillExecution 对象便可以解释,以及相应的异常对象

com.seckill.dto.Exposer
com.seckill.dto.SeckillExecution
    /**
     * 秒杀开启时输出秒杀接口地址
     * 否则输出系统时间和秒杀时间
     * @param seckillId
     */
    Exposer exportSeckillUrl(long seckillId);

    /**
     * 执行秒杀操作,有可能是失败的,失败的时候就抛出异常
     * @param seckillId 秒杀的商品Id
     * @param userPhone 手机号码
     * @param md5 md5加密值
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException,SeckillCloseException,RepeaKillException;

Exposer如下:(编写时也要想好Exposer包含的变量以及不同情况下对应的构造函数)

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 + "]";
    }
    //无参构造函数
    public Exposer() {
        // Auto-generated constructor stub
    }
    //是否开启秒杀
    private boolean exposed;
    // 一种加密措施
    private String md5;
    //秒杀商品id
    private long seckillId;
    // 系统当前时间
    private long now;
    //开启时间
    private long start;
    //结束时间
    private long end;

    /**
     * 当秒杀时间不符合时,返回的数据
     * @param exposed   false状态
     * @param seckillId 商品ID
     * @param now   当前系统时间(服务器时间)
     * @param start 秒杀开启时间
     * @param end   秒杀结束时间
     */
    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;
    }

    /**
     * 该商品符合秒杀时返回的数据
     * @param exposed   状态
     * @param md5   一种盐值加密数据
     * @param seckillId 秒杀商品id
     */
    public Exposer(boolean exposed, String md5, long seckillId) {
        super();
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

    /**
     * 秒杀商品不存在时返回的数据
     * @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:执行秒杀后后台返回给前台的数据封装对象

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;

    // 秒杀结果的状态
    private int state;

    /* 状态的明文标示 */
    private String stateInfo;

    /* 当秒杀成功时,需要传递秒杀结果的对象回去 */
    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;
    }

    /* 秒杀失败时返回的实体,没有秒杀结果的对象 */
    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;
    }
}

定义秒杀过程中可能会出现的异常
~、定义一个基础异常,所有的异常都继承这个异常
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);
    }
}

~、定义重复秒杀异常:

package com.seckill.exception;

/**
 * 重复秒杀异常,不需要我们手动去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);
    }

}

~、定义秒杀关闭异常

package com.seckill.exception;

/**
 * 秒杀已经关闭异常,当秒杀结束就会出现这个异常
 * @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接口实现

@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;//单页显示的数量

    //注入service依赖
    @Autowired
    private SeckillDao seckillDao;

    @Autowired
    private SuccessKilledDao successKilledDao;

    @Autowired
    private RedisDao redisDao;

    public SeckillServiceImpl() {
        //  Auto-generated constructor stub
    }

    /**
     * 查询全部的秒杀记录
     *
     * @return 数据库中所有的秒杀记录
     */
    /*
        pageSize(每个页面所显示的记录数)、
        pageCount(页面的总数)、
        showPage(目前显示第几页)、
        recordCount(总的记录数)
     */
    @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();//总数
        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
    /**
     * 使用注解控制事务方法的优点:
     * 1:开发团队打成一致约定,明确标注事务方法的编程风格
     * 2:保证事务方法的执行时间尽可能短,不穿插其他网络操作,RPC/HTTP请求或者剥离到事务方法外部
     * 3:不是所有的方法都需要事务,比如只有一条修改操作,或者只读操作不需要事务控制
     */
    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) {
            //访问数据库读取数据
            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();
        //当前系统时间
        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 {

        //在这一系列操作中,对应事务,return是commit,抛异常是rollback
        try {
            if (md5 == null || !md5.equals(getMD5(seckillId))) {
                logger.error("秒杀数据被篡改");
                throw new SeckillException("seckill data rewrite");
            }

            //执行秒杀逻辑: 减库存+记录购买行为
            Date nowTime = new Date();
            //记录购买行为
            int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
            //唯一:seckillId,userPhone
            if (insertCount <= 0) {
                logger.warn("不允许重复秒杀");
                throw new RepeaKillException("repeaKill !!");
            } else {
                //减库存,热点商品竞争
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if (updateCount <= 0) {
                    //没有更新记录,秒杀结束,可能时间结束也可能库存为零,rollback
                    logger.warn("没有更新数据库记录,秒杀已经结束了");
                    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);
        //执行存储过程,result被复制

        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的事物默认就是发生了RuntimeException才会回滚,可以检测出来的异常是不会导致事物的回滚的,这样的目的就是你明知道这里会发生异常,所以你一定要进行处理.如果只是为了让编译通过的话,那捕获异常也没多意思,所以这里要注意事物的回滚.
然后我们还发现这里存在硬编码的现象,就是返回各种字符常量,例如秒杀成功,秒杀失败等等,这些字符串时可以被重复使用的,而且这样维护起来也不方便,要到处去类里面寻找这样的字符串,所有我们使用枚举类来管理这样状态,在com.seckill包下建立enum包,专门放置枚举类,然后再建立SeckillStateEnum枚举类:
此处关于秒杀结果的集中状态,采用枚举类进行封装

com.seckill.enums.SeckillStateEnum
package com.seckill.enums;

/**
 * 使用枚举表述常量数据字段
 * @author hyh47
 *
 */
public enum SeckillStateEnum {
    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATE_PEWRITER(-3,"数据篡改");
    
    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包下的注解
~、配置事务,在事务中注入数据库连接池
~、开启基于注解的声明式事务

<?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>

    <!-- 配置事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库连接池 -->
        <property name="dataSource" ref="dataSource"></property>
    </bean>

    <!-- 开启基于注解的声明式事务 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

在配置过程中应该注意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());
        }
    }

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 172,117评论 25 707
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 31,631评论 18 399
  • 国家电网公司企业标准(Q/GDW)- 面向对象的用电信息数据交换协议 - 报批稿:20170802 前言: 排版 ...
    庭说阅读 10,967评论 6 13
  • 我们每个人,都或多或少的在修行,修行学业,身材,社交,友情,爱情,亲情,婚姻,工作业绩…… 而修行,不一定会有陪伴...
    冰冰月月在何方阅读 354评论 0 0
  • 组织文化 上一篇文章主要说了自己对敏捷测试的理解。其实很多事情都是说得容易做得难,从传统过渡到敏捷,我们会面临到很...
    天秤座的橘子阅读 457评论 0 7