SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设

SSM 框架搭建 sckill 秒杀系统
—— step.3 业务逻辑层建设

一、Service 层简介

       Service 层是服务层,也被称为业务逻辑层,是整个系统实现系统所需的业务逻辑功能的核心部分,承担着通过调用 DAO 接口实现数据的操纵与存储、利用 JavaBean 实现对系统所需数据处理并存储页面渲染功能的关键层级。
小贴士: 此处 Service 层的构建方式是使用接口 + 实现类的形式进行构建的。这样做是出于 Java 架构思想中开闭原则(即对拓展开放,对修改关闭)。(就通常而言,这是一种编程习惯,更适用项目的开发。)通过接口 + 实现类的组合,在需要向系统中添加新功能时,无需对原有功能的源码进行修改,只需添加新的抽象方法,并在实现类中进行实现就可以达到目的,并且,由于定义接口的存在,使得代码的重用率大大提高,在移植项目框架时,只需要重新定义方法的实现,而不需要整体替换。从而达到松耦合的效果,使得系统的维护更加便捷。

二、Service 层搭建

       如图所示,Service 层的接口及实现类存放与 service 文件夹下,通常,接口抽象方法的实现类放于 service/impl 目录下,以 “对应接口名 + Impl” 的方式命名。
SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设

(1) Service 接口定义

       由于在该 Seckill 秒杀系统较为简洁。主要的业务逻辑只需要:“1.对所有的秒杀记录进行查询;2.对单个特定秒杀记录进行查询;3.通过时间,控制秒杀的开启;4.执行秒杀操作” 四个核心功能。因此接口中,我们只需要对这四个方法进行定义。定义方式为,在接口 SeckillService 中定义:

public interface SeckillService {

    /**
     * 查询所有秒杀记录
     * @return
     */
    List<SeckillTab> getSeckillList();

    /**
     * 查询单个秒杀记录
     * @param seckillId
     * @return
     */
    SeckillTab getSeckillById(@Param("seckillId") long seckillId);

    /**
     * 目的:秒杀开启时输出秒杀地址,否则输出系统时间和秒杀时间
     * DTO :封装数据传输,
     * @param seckillId
     */
    Exposer exportSeckillUrl(@Param("seckill") long seckillId);

    /**
     * 执行秒杀操作
     * 内部 md5 比对方式如果不匹配,则用户ID被篡改,拒绝执行秒杀
     * 当抛出异常时,应该告诉接口使用方,可能会输出什么样的异常
     * SeckillException : 告诉用户秒杀错误
     * RepeatKillException : 告诉用户已经秒杀成功
     * SeckillCloseException : 告诉用户秒杀已经关闭
     * @param seckillId
     * @param userPhone
     * @param md5
     */
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
            throws SeckillException, RepeatKillException, SeckillCloseException;
}

(2)系统异常的定义

       考虑到系统的稳定性。在系统运行时,会由于一些非正常操作,或者缓存、信息传输延时等原因导致的运行期异常。为与 Java 自带的运行期异常进行区分。视频中定义了三种异常的抛出类,分别是: RepeatKillException (重复秒杀异常)、SeckillCloseException (通道关闭后秒杀异常)和 SeckillException (系统运行异常)。在 根目录下定义 exception 目录作为异常类的储存目录,三种异常分别配置如下:
SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设

  1. SeckillException (系统运行异常):
package org.seckill.exception;

/**
 * 秒杀相关业务异常
 * 所有相关异常的父类,从理论上来讲,所有属于该系统的报错都由该类进行处理,
 * 但是这样就失去了进行异常类定义的意义,无法对报错的原因进行精确的定位,
 * 因此该类只是作为整个系统运行时的总体异常类。
 */

public class SeckillException extends RuntimeException {
    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}
  1. SeckillCloseException (秒杀接口关闭异常):
public class SeckillCloseException extends SeckillException {
    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}
  1. RepeatKillException (重复秒杀异常):
package org.seckill.exception;

/**
 * 重复秒杀异常(本质上:运行期异常)  —— 属于秒杀相关业务异常
 * 用户通过重复执行秒杀程序提高秒杀成功率时,当秒杀成功时,产生重复秒杀异常
 * 系统能够识别重复秒杀异常并阻止重复秒杀行为
 */

public class RepeatKillException extends SeckillException {
    public RepeatKillException(String message) {
        super(message);
    }

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

(3)常量判定符 - 数据字典 建设

       一个良好的系统在实现 if - else 逻辑分辨时,通常使用 enum (枚举)来定义具有特定意义的 key 键,而非单纯的数字或文字。这样的好处是:

  1. 通过特定意义的 key 键的定义,使得每个 key 键都有自己的注释,最直观的表现是代码的可读性得到极大的提升;
  2. 由于系统的功能具有部分重叠与逻辑关联的特性,key 键的定义通常可以得到广泛的使用,当一个位置上的 key 值需要改变时,只需要改动 key 键的定义,就可以完成判定规则的修改。从而大大提高了系统的可维护性;(如果使用的是单纯的数字或文字,当一处 key 被修改后,为保证系统的正常运行,必须对整个系统进行检查,防止同样的定义在系统的相关功能区应用。)
           数据字典的定义是建立在系统根目录下的 enums 目录下,如图所示:
    SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
            枚举型数据字典 SeckillStatEnum 定义如下:
public enum SeckillStatEnum {
    SUCCESS(1, "秒杀成功"),
    END(0, "秒杀结束"),
    REPEAT_KILL(-1, "重复秒杀"),
    INNER_ERROR(-2, "系统异常"),
    DATA_REWTIRE(-3, "数据篡改");

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

	// 重定义枚举索引方法,通过 state 索引对应的枚举值 key
    public static SeckillStatEnum stateOf(int index){
        for (SeckillStatEnum state: values()){
            if(state.getState() == index){
                return state;
            }
        }
        return null;
    }
}

(4)DTO 数据传输对象的构建

       DTO 是数据传输对象(Data Transfer Object)的缩写,用于 MVC 设计模式下,充当 Model 的角色(即 JavaBean)。该类定义在根目录下的 dto 文件夹中,如图所示:
SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设

       在刚开始学习的时候,也是有这么一个疑问,我们从数据库中获得的数据已经被封装在了 entity 类中,在页面渲染时,直接使用 entity 对象数据不是简洁明了么,为什么还要专门开一个空间封装数据传输对象?
       但事实上, DTO 的存在并非多此一举。这是由 entity 类自身定义决定的。我们为了操作的方便,通常会在 entity 类中添加一定的数据冗余,而这些冗余在很多时候只是为了处理业务逻辑时能够更加快捷,更多的时候,前端并不需要这些数据,甚至于前端要展示的只是整个数据表的很小一部分数据,如果不加区分的一股脑全部传输给前端,会对数据传输造成很大的负担,而且有数据泄露的风险。
       DTO 则是一种针对的是前端的 UI 需求而进行封装的特定封装类型。通过继承和包含 entity 类,添加上一些特定的 key 键,来实现数据对应读取的效果。在减轻数据传输负担的同时,有效防止了后台数据的泄露。
       在本 Seckill 秒杀系统中,总共定义了三个封装类型: Exposer (秒杀地址暴露接口 DTO)、SeckillExecution (秒杀操作执行后,反馈数据封装 DTO)、SeckillResult (封装 json 格式数据的反馈类型数据)。

  1. Exposer 类的定义:
public class Exposer {
    private  boolean exposed;    // 布尔值 : 是否给予用户秒杀地址
    private String md5;          // 一种加密措施(算法), md5是一种在 Java 中封装好的算法,返回一个唯一的字符串,用以充当验证码的作用
    private long seckillId;
    private long now;            // 系统当前时间(毫秒)
    private long start;          // 秒杀开启时间
    private long end;            // 秒杀结束时间

    public Exposer() {}
    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;
    }

    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 +
                '}';
    }
}
  1. SeckillExecution 类的定义:
public class SeckillExecution {
    private long seckillId;
    private int state;            // 秒杀状态
    private String stateInfo;     // 秒杀状态描述
    private SucKilled sucKilled;  // 秒杀成功对象

    public SeckillExecution() {}
    public SeckillExecution(long seckillId, SeckillStatEnum seckillStatEnum, SucKilled sucKilled) {
        this.seckillId = seckillId;
        this.state = seckillStatEnum.getState();
        this.stateInfo = seckillStatEnum.getStateInfo();
        this.sucKilled = sucKilled;
    }
    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 SucKilled getSucKilled() {
        return sucKilled;
    }

    public void setSucKilled(SucKilled sucKilled) {
        this.sucKilled = sucKilled;
    }

    @Override
    public String toString() {
        return "SeckillExecution{" +
                "seckillId=" + seckillId +
                ", state=" + state +
                ", stateInfo='" + stateInfo + '\'' +
                ", sucKilled=" + sucKilled +
                '}';
    }
}
  1. SeckillResult 类的定义:
// 所有ajax请求返回类型: 封装 json 结果
public class SeckillResult<T> {
    private boolean success;
    private T data;
    private String error;

    public SeckillResult(){}
    public SeckillResult(boolean success, T data) {
        this.success = success;
        this.data = data;
    }

    public SeckillResult(boolean success, String error) {
        this.success = success;
        this.error = error;
    }

    public boolean isSuccess() {
        return success;
    }

    public T getData() {
        return data;
    }

    public String getError() {
        return error;
    }
}

(5)Service 接口的实现

       准备工作做完,就是进行 Service 接口方法的实现。Service 接口实现类存放在 service/impl 路径下。以 “对应接口 + Impl” 的形式进行命名。
       以下是 SeckillServiceImpl 类的定义和实现:

/**
 * Service层实现类,实现Service接口
 * 命名习惯: 接口名 + Impl
 * spring 提供注解类型:
 * --@Component : 代表所有组件,当不知道类属于dao, service,conroller等时使用。统称spring组件实例
 * --@Service : 特指服务层代码
 * --@Controller : 特指控制区代码
 */

// 在 Service 层,的实现类前 @Service 注释必须写,
// 这是告诉 Spring 将该类中的方法导入到 Spring 管理池中,如果不写,Service 接口将找不到实现类,故而报错
@Service
public class SeckillServiceImpl implements SeckillService {
    // 日志的应用
    private Logger logger = LoggerFactory.getLogger(this.getClass());
    // 注入Service依赖
    @Autowired  // 自动注入
    private SeckillDao seckillDao;
    @Autowired
    private SuccessKilledDao successKilledDao;
    private final String slat = "the@SLAT&confusion_md5#slat";  // 用于混淆 md5

    @Override  // 注释,表 实现/重写 父类方法
    public List<SeckillTab> getSeckillList() {
        return seckillDao.queryAll(0, 100);
    }

    @Override
    public SeckillTab getSeckillById(long seckillId) {
        return seckillDao.queryById(seckillId);
    }

    @Override
    public Exposer exportSeckillUrl(long seckillId) {
        SeckillTab seckillTab = seckillDao.queryById(seckillId);
        if(seckillTab == null){
            return new Exposer(false, seckillId);
        }
        Date startTime = seckillTab.getStartTime();
        Date endTime = seckillTab.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());
        }
        // md5 本质: 对任意字符串转换为特定长度的编码 —— 不可逆!!
        String md5 = getMD5(seckillId);
        return new Exposer(true, md5, seckillId);
    }

    // 自定义生成 md5 函数
    private String getMD5(long seckillId){
        String base = seckillId + '@' +slat + '#';
        // DigestUtils.md5DigestAsHex() 用于生成 md5 , 参数为二进制(byte),用 .getBytes()得到字符串二进制
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }

    @Override
    public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {
        try {
            if(md5 == null || !md5.equals(getMD5(seckillId))){
                throw new SeckillException("seckill info rewrite");
            }else{
                // 执行秒杀操作
                // 操作逻辑: 减库存 + 记录购买行为
                Date nowTime = new Date();
                // 减库存
                int updateCount = seckillDao.reduceNumber(seckillId, nowTime);
                if(updateCount <= 0){
                    // 没有更新记录,秒杀已经结束
                    throw new SeckillCloseException("seckill was closed");
                }else{
                    // 记录购买行为
                    int insertCount = successKilledDao.insertSuccessKilled(seckillId, userPhone);
                    if(insertCount <= 0){  // 重复秒杀,无效,抛出重复秒杀异常
                        throw new RepeatKillException("seckill repeated");
                    }else{  // 秒杀成功,打印秒杀记录
                        SucKilled sucKilled = successKilledDao.queryByIdwithSeckill(seckillId, userPhone);
                        return new SeckillExecution(seckillId, SeckillStatEnum.SUCCESS, sucKilled);
                    }
                }
            }
        }catch(SeckillCloseException es){
            throw es;  // 抛出“秒杀窗口关闭异常”
        }catch(RepeatKillException er){
            throw er;  // 抛出 “重复秒杀异常”
        }catch (Exception e){
            logger.error(e.getMessage(), e);
            // 将所有编译期异常转化为运行期异常, RuntimeException -> rowback(回滚机制)
            throw new SeckillException("seckill inner error:" + e.getMessage());
        }
    }
}

三、整合 - Spring 框架管理 Service 层

       从配置代码数量的角度看,Service 层的 Spring 管理配置是几个层中配置最简单的一个。同样的,在 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"
       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/tx
						   http://www.springframework.org/schema/tx/spring-tx.xsd
						   http://www.springframework.org/schema/context
						   http://www.springframework.org/schema/context/spring-context.xsd">
    <!-- 配置并使用spring声明式事务 -->
    <!-- 扫描 service 包下所有使用注解的类型,初始化该类型并放入spring容器中 -->
    <context:component-scan base-package="org.seckill.service" />

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

    <!-- 配置基于注解的声明式事务,默认使用注解的方式来管理事务行为 -->
    <tx:annotation-driven transaction-manager="transactionManager" />
</beans>

       注意:此处需回头检查 Service 实现类前,是否带了 Spring 的@Service注释,只有带了该注释,Spring 才会将该实现类中的方法纳入 Spring 管理池进行管理。否则,将会在调用时,发生报错,提示方法未纳入管理。(接口不需要写@Service注释 )
       至此,Service 层的创建和配置工作就算告一段落了。
**小贴士:**Service 层所用特殊注释标签及含义

注解名 用途解释
@Component 通用组件代表所有组件,当不知道该 Java 类具体属于 dao, service, conroller 等时使用。统称 spring 组件实例
@Controller web 页面控制组件,作用于表现层(spring-mvc 的注解)
@Service 服务层控制,作用于业务逻辑层
@Autowired 对类成员变量、方法及构造函数进行标注,完成自动装配工作

四、测试 Service 层接口功能

       和 DAO 层实现完毕一样, Service 层的接口方法也要进行测试操作。测试类生成方法同 DAO 接口层。将光标置于接口名上, alt + enter(回车) ,点击 Create Test ,便可在弹出的窗口中快捷建立 test 文件夹内对应的 JUnit 4 测试类。如图所示:
SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
       选中所有要测试的方法,点击 OK ,自动生成 Test 测试类。
SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
测试类代码如下:

package org.seckill.service;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExecution;
import org.seckill.entity.SeckillTab;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
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;

// @RunWith(SpringJUnit4ClassRunner.class)  junit 依赖,在junit启动时,加载springIOC容器
@RunWith(SpringJUnit4ClassRunner.class)
// 告诉junit spring 的配置文件,
// 注意:大括号中的字符串配置索引地址要用逗号(",")隔开
// 在 DAO 接口测试时,只用导入 spring-dao.xml 配置,
// 在 Service 接口测试时,不仅要导入 spring-service 配置,还要导入 spring-dao.xml 配置
@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<SeckillTab> list = seckillService.getSeckillList();
        logger.info("list={}", list);
        // Closing non transactional SqlSession ——> 只读操作,不是在事务控制下
    }

    @Test
    public void getSeckillById() {
        long id = 1000L;
        SeckillTab seckillTab = seckillService.getSeckillById(id);
        logger.info("seckill={}", seckillTab);
    }

    @Test
    public void exportSeckillUrl() {
//        long id = 1000L;
        long id = 1004L;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        logger.info("exposer={}", exposer);
        // 秒杀未开启或已结束(报错): exposer=Exposer{exposed=false, md5='null', seckillId=1000, now=1612421304186, start=1610380800000, end=1610467200000}
        // 秒杀开启: exposer=Exposer{exposed=true, md5='da80b6ac68b1c8e104bf3964c6d6b7e3', seckillId=1004, now=0, start=0, end=0}
    }

    @Test
    public void executeSeckill() {
//        long id = 1000L;
        long id = 1004L;
        long phone = 13568957721L;
        String md5 = "da80b6ac68b1c8e104bf3964c6d6b7e3";
        try {
            SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
            logger.info("result={}", execution);
            // result=SeckillExecution{seckillId=1004, state=1, stateInfo='秒杀成功', sucKilled=SucKilled [seckillId=1004, userPhone=13568957721, state=0, killTime=Thu Feb 04 15:29:57 CST 2021]}
        } catch (RepeatKillException er) {
            logger.error(er.getMessage());
        } catch (SeckillCloseException es) {
            logger.error(es.getMessage());
        }
    }

    // 测试代码完整逻辑,注意代码的可重复执行性
    @Test  // 整合 exportSeckillUrl() 与 executeSeckill() 测试
    public void testSeckillLogic() {
        long id = 1004L;
        Exposer exposer = seckillService.exportSeckillUrl(id);
        long phone = 13568957769L;
        if(exposer.isExposed()){
            String md5 = exposer.getMd5();
            try {
                SeckillExecution execution = seckillService.executeSeckill(id, phone, md5);
                logger.info("result={}", execution);
                // result=SeckillExecution{seckillId=1004, state=1, stateInfo='秒杀成功', sucKilled=SucKilled [seckillId=1004, userPhone=13568957721, state=0, killTime=Thu Feb 04 15:29:57 CST 2021]}
            } catch (RepeatKillException er) {
                logger.error(er.getMessage());
            } catch (SeckillCloseException es) {
                logger.error(es.getMessage());
            }
        }else{
            // 秒杀未开启
            logger.warn("exposer={}", exposer);
        }
    }
}

       如果测试没有问题,那么恭喜,Service 层的搭建宣告完毕。

报错及解决方法

(1)Service 实现类未纳入 Spring 管理池管理。解决方法就是在 Service 接口的实现类前加@Service注释。报错共有两处,如图所示:

  1. Service 接口导入报错(提示不存在要导入的 bean)
    SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
  2. 功能报错,(同样是提示 autowired 依赖导入失败)
    SSM 框架搭建 sckill 秒杀系统 —— step.3 业务逻辑层建设
上一篇:加密


下一篇:[BJDCTF2020]Easy MD5