SSM项目秒杀系统---(二)Service层

一、秒杀业务接口设计与实现

1、秒杀Service接口设计

main—java下新建service、dto、exception文件夹
dto层:类似于entity,但是这些类字段与业务其实不是很相关,只是为了方便service返回数据而进行了封装。
(1)业务接口
站在“使用者”角度设计接口
三个方面:方法定义粒度明确,参数简练直接,返回类型友好(return 类型/异常)
service—SeckillService.java

package org.luyangsiyi.seckill.service;

import org.luyangsiyi.seckill.dto.Exposer;
import org.luyangsiyi.seckill.dto.SeckillExecution;
import org.luyangsiyi.seckill.entity.Seckill;
import org.luyangsiyi.seckill.exception.RepeatKillException;
import org.luyangsiyi.seckill.exception.SeckillClosedException;
import org.luyangsiyi.seckill.exception.SeckillException;

import java.util.List;

/**
 * 业务接口:站在"使用者"角度设计接口
 * 三个方面:方法定义粒度明确,参数简练直接,返回类型(return 友好的类型/异常)
 * Created by luyangsiyi on 2020/2/22
 */
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
     * @throws SeckillException 是其余两类异常的父类,但是因为抛出的具体原因不同,所以都需要抛出让使用者明确异常的原因
     * @throws RepeatKillException
     * @throws SeckillClosedException
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
        throws SeckillException, RepeatKillException, SeckillClosedException;
}

(2)新建eums文件下,定义枚举类
SeckillStatEnums.java

package org.seckill.enums;

/**
 * 使用枚举类表述常量数据字段
 * Created by luyangsiyi on 2020/1/29
 */
public enum SeckillStatEnums {

    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATA_REWRITE(-3,"数据篡改");

    private int state;

    private String stateInfo;

    SeckillStatEnums(int state, String stateInfo) {
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public int getState() {
        return state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public static SeckillStatEnums stateOf(int index){
        for(SeckillStatEnums state : values()){
            if(state.getState() == index){
                return state;
            }
        }
        return null;
    }
}

(3)dto中定义service中需要用到的实体
Exposer.java

package org.luyangsiyi.seckill.dto;

/**
 * 暴露秒杀地址DTO
 * 这些字段与业务其实不相关,只是为了方便service返回数据的封装
 * Created by luyangsiyi on 2020/2/22
 */
public class Exposer {

    //是否开启秒杀
    private boolean exposed;

    //一种加密措施
    private String md5;

    //id
    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 now, long start, long end) {
        this.exposed = exposed;
        this.now = now;
        this.start = start;
        this.end = end;
    }

    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;
    }

    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;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", seckillId=" + seckillId +
                ", now=" + now +
                ", start=" + start +
                ", end=" + end +
                '}';
    }
}

SeckillException.java

package org.luyangsiyi.seckill.dto;

import org.luyangsiyi.seckill.entity.SuccessKilled;
import org.luyangsiyi.seckill.enums.SeckillStatEnum;

/**
 * 封装秒杀执行后的结果
 * Created by luyangsiyi on 2020/2/22
 */
public class SeckillExecution {

    private long seckillId;

    //秒杀执行结果
    private int state;

    //状态标识
    private String stateInfo;

    //秒杀成功对象
    private SuccessKilled successKilled;

    //秒杀成功返回的结果
    public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = seckillStatEnum.getState();
        this.stateInfo = seckillStatEnum.getStateInfo();
        this.successKilled = successKilled;
    }

    //秒杀失败返回的结果
    public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum) {
        this.seckillId = seckillId;
        this.state = seckillStatEnum.getState();
        this.stateInfo = seckillStatEnum.getStateInfo();
    }

    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;
    }

    @Override
    public String toString() {
        return "SeckillExecution{" +
                "seckillId=" + seckillId +
                ", state=" + state +
                ", stateInfo='" + stateInfo + '\'' +
                ", successKilled=" + successKilled +
                '}';
    }
}

(3)Exception中定义可能的异常
SeckillException.java

package org.seckill.exception;

/**
 * 秒杀相关业务异常
 * Created by luyangsiyi on 2020/1/29
 */
public class SeckillException extends RuntimeException{

    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}

RepeatKillException.java

package org.seckill.exception;

/**
 * 重复秒杀异常(运行期异常)
 * Created by luyangsiyi on 2020/2/22
 */
public class RepeatKillException extends SeckillException{

    public RepeatKillException(String message) {
        super(message);
    }

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
}

SeckillCloseException.java

package org.seckill.exception;

/**
 * 秒杀关闭异常
 * Created by luyangsiyi on 2020/2/22
 */
public class SeckillCloseException extends SeckillException{

    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}

(4)接口实现
service下新建impl文件夹,新建SeckillServiceImpl.java实现接口
SeckillServiceImpl.java

package org.seckill.service.impl;

import org.seckill.dao.SeckillDao;
import org.seckill.dao.SuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.Seckill;
import org.seckill.entity.SuccessKilled;
import org.seckill.enums.SeckillStatEnums;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.SecikillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.List;

/**
 * Created by luyangsiyi on 2020/2/22
 */
public class SeckillServiceImpl implements SeckillService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

    private SeckillDao seckillDao;

    private SuccessKilledDao successKilledDao;

    //md5盐值字符串,用于混淆
    private final String slat = "sawfhooauwencklohegd!@@34456ASGR?>'";

    @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 (seckill == null) {
            return new Exposer(false, seckillId);
        }
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();
        //系统当前时间
        Date nowTime = new Date();
        //getTime()返回毫秒级表示
        if (nowTime.getTime() < startTime.getTime()
                || nowTime.getTime() > endTime.getTime()) {
            return new Exposer(false, seckillId, nowTime.getTime(), startTime.getTime(),
                    endTime.getTime());
        }
        //转换特定字符串的过程,不可逆
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }

    private String getMD5(long seckillId) {
        String base = seckillId + "/" + slat;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());//spring工具类
        return md5;
    }

    @Override
    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 updateCount = seckillDao.reduceNumber(seckillId, nowTime);
            if (updateCount <= 0) {
                //没有更新到记录,秒杀结束
                throw new SeckillCloseException("seckill is closed");
            } else {
                //记录购买行为
                int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
                //唯一:seckillId,userPhone
                if (insertCount <= 0) {
                    //重复秒杀
                    throw new RepeatKillException("seckill repeated");
                } else {
                    //秒杀成功
                    SuccessKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId, userPhone);
                    return new SeckillExecution(seckillId, SeckillStatEnums.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());
        }
    }
}

二、基于spring托管Service实现类

(1)Spring IOC功能理解

对象工厂、依赖管理–>一致的访问接口

  • 业务对象依赖:SeckillService依赖SeckillDao、SuccessKilledDao依赖SqlSessionFactory依赖DataSource…
  • 为什么要用IOC:对象创建统一托管、规范的生命周期管理、灵活的依赖注入、一致的获取对象
  • Spring-IOC注入方式和场景:
XML注解Java配置类
1、Bean实现类来自第三方类库,如DataSource等;2:需要命名空间配置,如context、aop、mvc等。项目中自身开发使用的类,可直接在代码中使用注解,如@Service,@Controller需要通过代码控制对象创建逻辑的场景,如自定义修改依赖类库
  • 本项目IOC使用
    XML配置、package-scan、Annotation注解

(2)使用spring托管service依赖配置

resources—spring—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"
       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.0.xsd">
    <!--扫描service包下所有使用注解的类型-->
    <context:component-scan base-package="org.seckill.service"/>
</beans>

在SeckillServiceImpl.java中注解整个class为@Service,将两个Dao注解为@Autowired

@Service
public class SeckillServiceImpl implements SecikillService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());

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

    @Autowired
    private SuccessKilledDao successKilledDao;

三、配置并使用spring声明式事务

1、Spring声明式事务

  • 什么是声明式事务
    开启事务—修改SQL-1、修改SQL-2、…、修改SQL-n—提交/回滚事务 ==>全自动的方式即声明式事务
  • 声明式事务使用方式
    ProxyFactoryBean+XML–>早起使用方式
    tx:advice_aop命名空间–>一次配置永久生效
    注解@Transactional–>注解控制(建议使用这种方式,只对事务相关的业务进行注解)
  • 事务方法嵌套
    声明式事务独有的概念
    传播行为–>propagation_required(当有新事务时,加入到原有的事务中)
  • 什么时候回滚事务
    抛出运行期异常(RuntimeException)、小心不当的try-catch

2、使用spring声明式事务配置

(1)继续配置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-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">

    <!--扫描service包下所有使用注解的类型-->
    <context:component-scan base-package="org.luyangsiyi.seckill.service"/>

    <!--配置事务管理器-->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!--注入数据库的连接池-->
        <property name="dataSource" ref="dataSource"/><!--这边dataSource标红是因为在另一个spring配置文件中,但是运行时都给到程序即可-->
    </bean>

    <!--配置基于注解的声明式事务,默认使用注解来管理事务行为-->
    <!--有很多annotation-driven,选择http://www.springframework.org/schema/tx这个-->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

(2)在SeckillSericeImpl.java中增加@Transactional注解

@Override
@Transactional
/**
 * 使用注解控制事务方法的优点:
 * 1:开发团队达成一致的预定,明确标注事务方法的编程风格
 * 2:保证事务方法的执行时间尽可能短,不要禅茶其他风格操作RPC/HTTP请求或者剥离到事务方法外部
 * 3:不是所有的方法都需要事务,如只有一条修改操作,只读操作不需要事务控制,所以不需要使用tx:advice_aop方式
 */
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillClosedException {

四、完成Service集成测试

1、配置logback

在resources下新建logback.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

    <root level="debug">
        <appender-ref ref="STDOUT" />
    </root>
</configuration>

2、测试代码

同dao层测试一样选中service层接口名字生成test测试模块:
注意测试的时候商品的时间可以修改为已开启/未开启秒杀的时间。

package org.luyangsiyi.seckill.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.luyangsiyi.seckill.dto.Exposer;
import org.luyangsiyi.seckill.dto.SeckillExecution;
import org.luyangsiyi.seckill.entity.Seckill;
import org.luyangsiyi.seckill.exception.RepeatKillException;
import org.luyangsiyi.seckill.exception.SeckillClosedException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

import static org.junit.Assert.*;

/**
 * Created by luyangsiyi on 2020/2/22
 */
@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 getSeckillList() {
        List<Seckill> seckills = seckillService.getSeckillList();
        logger.info("list={}",seckills);
    }

    @Test
    public void getById() {
        long id = 1000;
        Seckill seckill = seckillService.getById(id);
        logger.info("seckill={}",seckill);
    }

    @Test
    public void exportSeckillUrl() {
        long id = 1000;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        logger.info("exposer={}",exposer);
        //exposer=Exposer{exposed=true, md5='	55FDBFEA6C5D3BBB4352DAFAFAED9053',
        //seckillId=1000, now=0, start=0, end=0}
    }

    @Test
    public void executeSeckill() {
        long id = 1000;
        long phone = 13313344560L;
        String md5 = "55FDBFEA6C5D3BBB4352DAFAFAED9053";
        //try-catch是为了让我们允许的异常不被junit当做是需要抛出的异常
        try{
            SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
            logger.info("seckillExecution={}",seckillExecution);
        } catch (RepeatKillException e){
            logger.error(e.getMessage(),e);
        } catch (SeckillClosedException e){
            logger.error(e.getMessage(),e);
        }
        //seckillExecution=SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功',
        //successKilled=SuccessKilled{seckillId=1000, userPhone=13313344560,state=0, createTime=Sun Feb 23 11:59:14 CST 2020,
        //seckill=Seckill{seckId=0, name='2000元秒杀iphone8p', number=99, startTime=Thu Feb 20 14:00:00 CST 2020, endTime=Tue Feb 25 14:00:00 CST 2020, createTime=Sun Feb 23 05:23:04 CST 2020}}}
    }

    //联合测试exportSeckillUrl()和executeSeckill()
    @Test
    public void seckillLogicTest(){
        long id = 1000;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        if(exposer.isExposed()){
            logger.info("exposer={}",exposer);
            long phone = 13313344568L;
            String md5 = exposer.getMd5();
            try{
                SeckillExecution seckillExecution = seckillService.executeSeckill(id,phone,md5);
                logger.info("seckillExecution={}",seckillExecution);
            } catch (RepeatKillException e){
                logger.error(e.getMessage(),e);
            } catch (SeckillClosedException e){
                logger.error(e.getMessage(),e);
            }
        } else {
            //秒杀未开启
            logger.warn("exposer={}",exposer);
        }
        //注意测试的多种情况:
        //(1)重复秒杀[main] ERROR o.l.s.service.SeckillServiceTest - seckill repeated
        // (2) 秒杀未开启 exposer=Exposer{exposed=false, md5='null', seckillId=1002, now=1582380451359, start=1582437600000, end=1582610400000}
        // (3) 正常秒杀 seckillExecution=SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功', successKilled=SuccessKilled{seckillId=1000, userPhone=13313344568, state=0, createTime=Sun Feb 23 12:08:53 CST 2020, seckill=Seckill{seckId=0, name='2000元秒杀iphone8p', number=98, startTime=Thu Feb 20 14:00:00 CST 2020, endTime=Tue Feb 25 14:00:00 CST 2020, createTime=Sun Feb 23 05:23:04 CST 2020}}}
    }
}

版权声明:本文为qq_40709702原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。