Java   发布时间:2022-04-02  发布网站:大佬教程  code.js-code.com
大佬教程收集整理的这篇文章主要介绍了Java秒杀系统二:Service层大佬教程大佬觉得挺不错的,现在分享给大家,也给大家做个参考。

编辑器:IDEA

java版本:java8

前文:秒杀系统环境搭建与DAO层设计

秒杀业务接口与实现

DAO层:接口设计、SQL编写

service:业务,DAO拼接等逻辑

代码和SQL分离,方便review。

service接口设计

目录如下:

image-20211005132704868

首先是SecKillservice接口的设计:

/**
 * 业务接口:站在使用者角度设计接口
 * 三个方面:
 * 方法定义粒度 - 方便调用
 * 参数 - 简练直接
 * 返回类型 - return(类型、异常)
 */
public interface SecKillservice {
    /**
     * 查询所有秒杀记录
     * @return
     */
    List<seckill> getSecKillList();

    SecKill getById(long seckillId);

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

    // 执行秒杀操作验证MD5秒杀地址,抛三个异常(有继承关系)
    // 是为了更精确的抛出异常
    SeckillExecution executeSeckill(long seckillId, long userPhone, String md5)
        throws SeckillException, RepeatKillException,SeckillCloseException;

}

exportSecKillUrl函数用来暴露出接口的地址,用一个专门的dto类Exposer来实现:

/**
 * 暴露秒杀地址DTO
 */
public class Exposer {
    // 是否开启秒杀
    private Boolean exposed;
    // 加密措施
    private String md5;

    private long seckillId;
    // 系统当前时间
    private long now;

    // 秒杀开启结束时间
    private long start;
    private long end;
    // constructor getter setter
}

这里有一个md5,是为了构造秒杀地址,防止提前猜出秒杀地址,执行作弊手段。

executeSeckill函数表示执行秒杀,应该返回执行的结果相关信息:

/**
 * 封装秒杀执行后结果
 */
public class SeckillExecution {
    private long seckillId;
    // 秒杀结果状态
    privatE int state;

    // 状态信息
    private String stateInfo;

    // 秒杀成功对象
    private successKilled successKilled;
    // constructor getter setter
}

执行过程中可能会抛出异常。这里面用到了几个异常:SeckillException, RepeatKillException, SeckillCloseException

SeckillException.java,这个是其他两个的父类,除了那两个精确的异常,都可以返回这个异常。

// 秒杀相关业务异常
public class SeckillException extends RuntimeException {
    public SeckillException(String messagE) {
        super(messagE);
    }

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

RepeatKillException是重复秒杀异常,一个用户一件商品只能秒杀一次:

// 重复秒杀异常,运行期异常
public class RepeatKillException extends SeckillException {
    public RepeatKillException(String messagE) {
        super(messagE);
    }

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

同理,SeckillCloseException是秒杀关闭异常,秒杀结束了还在抢,返回异常。

// 秒杀关闭异常,如时间到了,库存没了
public class SeckillCloseException extends SeckillException {
    public SeckillCloseException(String messagE) {
        super(messagE);
    }

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

service接口实现

首先开启扫描。

spring-service.xml

<!--?xml version="1.0" encoding="UTF-8"?-->
<beans xmlns="@R_489_10107@://www.springframework.org/scheR_723_11845@a/beans" xmlns:xsi="@R_489_10107@://www.w3.org/2001/XMLscheR_723_11845@a-instance" xmlns:context="@R_489_10107@://www.springframework.org/scheR_723_11845@a/context" xsi:scheR_723_11845@aLOCATIOn="@R_489_10107@://www.springframework.org/scheR_723_11845@a/beans
        @R_489_10107@://www.springframework.org/scheR_723_11845@a/beans/spring-beans.xsd @R_489_10107@://www.springframework.org/scheR_723_11845@a/context @R_489_10107@s://www.springframework.org/scheR_723_11845@a/context/spring-context.xsd">
    <!--扫描service包下使用注解的类型-->
    <context:component-scan base-package="cn.orzlinux.service">

</context:component-scan></beans>

SecKillserviceImpl实现类实现了SecKillservice接口的方法:指定日志对象,DAO对象。

@service
public class SecKillserviceImpl implements SecKillservice {
    // 使用指定类初始化日志对象,在日志输出的时候,可以打印出日志信息所在类
    private Logger logger = LoggerFactory.getLogger(this.getClass());

    // 需要两个 dao的配合
    // 注入service依赖
    @resource
    private SecKillDao secKillDao;
    @resource
    private successKilledDao successKilledDao;
    
    //...
}

查询比较简单,直接调用DAO方法:

@Override
public List<seckill> getSecKillList() {
    // 这里因为只有四条秒杀商品
    return secKillDao.queryAll(0,4);
}

@Override
public SecKill getById(long seckillId) {
    return secKillDao.queryById(seckillId);
}

暴露秒杀接口函数:

/**
 * 秒杀开启时,输出秒杀接口地址
 * 否则输出系统时间和秒杀时间
 *
 * @param seckillId
 */
@Override
public Exposer exportSecKillUrl(long seckillId) {
    SecKill secKill = secKillDao.queryById(seckillId);
    if(secKill == null) {
        // 查不到id,false
        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());
    }
    // 不可逆
    String md5 = getMD5(seckillId);
    return new Exposer(true,md5,seckillId);
}

getMD5是一个自定义函数:

// 加入盐、混淆效果,如瞎打一下:
private final String slat="lf,ad.ga.dfgm;adrktpqerml[fasedfa]";
private String getMD5(long seckillId) {
    String base = seckillId+"/orzlinux.cn/"+slat;
    // spring已有实现
    String md5 = Digestutils.md5DigestAsHex(base.getBytes(StandardCharsetS.UTF_8));
    return md5;
}

执行秒杀函数,这里面牵扯到编译异常和运行时异常。

异常

编译时异常:编译成字节码过程中可能出现的异常。

运行时异常:将字节码加载到内存、运行类时出现的异常。

异常体系结构:

 * java.lang.Throwable
 * 		|-----java.lang.Error:一般不编写针对性的代码进行处理。
 * 		|-----java.lang.Exception:可以进行异常的处理
 * 			|------编译时异常(@R_673_10943@ked)
 * 					|-----IOException
 * 						|-----FileNotFoundException
 * 					|-----ClassnotFoundException
 * 			|------运行时异常(un@R_673_10943@ked,RuntimeException)
 * 					|-----NullPointerException
 * 					|-----ArrayIndexOutOfBoundsException
 * 					|-----ClassCastException
 * 					|-----numberFormatException
 * 					|-----InputMismatchException
 * 					|-----ArithmeticException

使用try-catch-finally处理编译时异常,是得程序在编译时就不再报错,但是运行时仍可能报错。相当于我们使用try-catch-finally将一个编译时可能出现的异常,延迟到运行时出现。开发中,由于运行时异常比较常见,所以我们通常就不针对运行时异常编写try-catch-finally了。针对于编译时异常,我们说一定要虑异常的处理。

executeSeckill.java

@Override
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) throws
        SeckillException, RepeatKillException, SeckillException {
    if(md5==null || !md5.equals(getMD5(seckillId))) {
        // 秒杀的数据被重写修改了
        throw new SeckillException("seckill data rewrite");
    }
    // 执行秒杀逻辑:减库存、加记录购买行为
    Date nowTime = new Date();
    // 减库存
    int updateCount = secKillDao.reducenumber(seckillId,nowTimE);
    try {
        if(updateCount<=0) {
            // 没有更新记录,秒杀结束
            throw new SeckillCloseException("seckill is closed");
        } else {
            // 记录购买行为
            int insertCount = successKilledDao.insertsuccessKilled(seckillId,userPhonE);
            if(insertCount<=0) {
                // 重复秒杀
                throw new RepeatKillException("seckill repeated");
            } else {
                //秒杀成功
                successKilled successKilled = successKilledDao.queryByIdWithSeckill(seckillId,userPhonE);
                //return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
                return new SeckillExecution(seckillId, SecKillStatEnum.succesS,successKilled);
            }
        }
    } catch (SeckillCloseException | RepeatKillException e1){
        throw e1;
    } catch (Exception E) {
        logger.error(e.getmessage(),E);
        // 所有编译期异常转化为运行期异常,这样spring才能回滚
        throw new SeckillException("seckill inner error"+e.getmessage());
    }
}

SeckillCloseExceptionRepeatKillException都继承了运行时异常,所以这些操作把异常都转化为了运行时异常。这样spring才能回滚。数据库的修改才不会紊乱。

这里有一个操作就是枚举的使用。

//return new SeckillExecution(seckillId,1,"秒杀成功",successKilled);
return new SeckillExecution(seckillId, SecKillStatEnum.succesS,successKilled);

用第一行的方式割裂了状态和状态信息,很不优雅,而且后续要更改的话,这些代码分散在各个代码中,不易修改,所以用枚举代替。

package cn.orzlinux.enums;

/**
 * 使用枚举表述常量数据字段
 */
public enum SecKillStatEnum {

    succesS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATA_REWRITE(-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;
    }

    public static SecKillStatEnum stateOf(int indeX) {
        for(SecKillStatEnum statEnum:values()) {
            if(statEnum.getState()==indeX) {
                return statEnum;
            }
        }
        return null;
    }
}

声明式事务

image-20211005154110783

spring早期使用方式(2.0):ProxyFactoryBean + XML

后来:tx:advice+aop命名空间,一次配置永久生效。

注解@transactional,注解控制。(推荐)

支持事务方法嵌套。

何时回滚事务?抛出运行期异常,小心try/catch

具体配置:

在spring-service.xml添加:

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

<!--配置基于注解的声明式事务-->
<!--默认使用注解管理事务行为-->
<tx:Annotation-driven transaction-manager="transationManager">

在SecKillserviceImpl.java文件添加注解

@Override
@transactional
public SeckillExecution executeSeckill(long seckillId, long userPhone, String md5) 		throws SeckillException, RepeatKillException, SeckillException
...

使用注解控制事务方法的优点

  • 开发团队达成一致约定,明确标注事务方法的编程风格
  • 保证事务方法的执行时间尽可能短,不要穿插其它网络操作,要剥离到事务外部
  • 不是所有的方法都需要事务

集成测试

resource文件夹下新建logBACk.xml,日志的配置文件:

<!--?xml version="1.0" encoding="UTF-8"?-->
<configuration>
    <appender name="STDOUT" class="ch.qos.logBACk.core.ConsoleAppender">
        <layout class="ch.qos.logBACk.classic.PatternLayout">
            <pattern>[%d{yyyy-MM-dd' 'HH:mm:ss.sss}] [%C] [%t] [%L] [%-5p] %m%n</pattern>
        </layout>
    </appender>

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

SecKillserviCETest.java

@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> list = secKillservice.getSecKillList();;
        logger.info("list={}",list);
        // 输出信息: [main] [89] [DEBUG] JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@5443d039] will not be managed by Spring
        //[DEBUG] ==>  Preparing: SELEct seckill_id,name,number,start_time,end_time,create_time from seckill order by create_time DESC limit 0,?;
        //[DEBUG] ==> Parameters: 4(Integer)
        //[DEBUG] <==      @R_464_10586@l: 4
        //[DEBUG]
        // ------------------Closing non transactional SqlSession ---------------------
        // [org.apache.ibatis.session.defaults.DefaultSqlSession@66c61024]
        //[INFO ] list=[SecKill{seckillId=1000, name='1000秒杀iphone13', number=99, startTime=Tue Oct 05 02:00:00 CST 2021, endTime=Wed Oct 06 02:00:00 CST 2021, createTime=Tue Oct 05 10:01:47 CST 2021}, SecKill{seckillId=1001, name='500秒杀iphone12', number=200, startTime=Tue Oct 05 02:00:00 CST 2021, endTime=Wed Oct 06 02:00:00 CST 2021, createTime=Tue Oct 05 10:01:47 CST 2021}, SecKill{seckillId=1002, name='300秒杀iphone11', number=300, startTime=Tue Oct 05 02:00:00 CST 2021, endTime=Wed Oct 06 02:00:00 CST 2021, createTime=Tue Oct 05 10:01:47 CST 2021}, SecKill{seckillId=1003, name='100秒杀iphone6', number=400, startTime=Tue Oct 05 02:00:00 CST 2021, endTime=Wed Oct 06 02:00:00 CST 2021, createTime=Tue Oct 05 10:01:47 CST 2021}]
    }

    @Test
    public void getById() {
        long id = 1000;
        SecKill secKill = secKillservice.getById(id);
        logger.info("seckill={}",secKill);
        // seckill=SecKill{seckillId=1000, name='1000秒杀iphone13', n...}
    }

    @Test
    public void exportSecKillUrl() {
        long id = 1000;
        Exposer exposer = secKillservice.exportSecKillUrl(id);
        logger.info("exposer={}",exposer);
        //exposer=Exposer{exposed=true, md5='c78a6784f8e8012796c934dbb3f76c03',
        //          seckillId=1000, now=0, start=0, end=0}
        // 表示在秒杀时间范围内
    }

    @Test
    public void executeSeckill() {
        long id = 1000;
        long phone = 10134256781L;
        String md5 = "c78a6784f8e8012796c934dbb3f76c03";

        // 重复测试会抛出异常,junit会认为测试失败,要把异常捕获一下更好看
        try {
            SeckillExecution seckillExecution = secKillservice.executeSeckill(id,phone,md5);
            logger.info("result: {}",seckillExecution);
        } catch (RepeatKillException | SeckillCloseException E) {
            logger.error(e.getmessage());
            // 再运行一次: [ERROR] seckill repeated
        }


        // 有事务记录
        // CommitTing JDBC transaction on Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@4b40f651]
        //[2021-10-05 16:45:00.000] [org.springframework.jdbc.datasource.DatasourcetransactionManager] [main] [384] [DEBUG] Releasing JDBC Connection [com.mchange.v2.c3p0.impl.NewProxyConnection@4b40f651] after transaction
        //result: SeckillExecution{seckillId=1000, state=1, stateInfo='秒杀成功', successKilled=successKilled{seckillId=1000, userPhone=10134256781, state=0, createTime=Wed Oct 06 00:45:00 CST 2021}}
    }

    // 集成测试完整逻辑实现
    @Test
    public void testSeckillLogic() {
        long id = 1001;
        Exposer exposer = secKillservice.exportSecKillUrl(id);
        if(exposer.isExposed()) {
            logger.info("exposer={}",exposer);

            long phone = 10134256781L;
            String md5 = exposer.getMd5();

            // 重复测试会抛出异常,junit会认为测试失败,要把异常捕获一下更好看
            try {
                SeckillExecution seckillExecution = secKillservice.executeSeckill(id,phone,md5);
                logger.info("result: {}",seckillExecution);
            } catch (RepeatKillException | SeckillCloseException E) {
                logger.error(e.getmessage());
                // 再运行一次: [ERROR] seckill repeated
            }

        } else {
            // 秒杀未开启
            logger.warn("exposer={}",exposer);
        }
    }
}

大佬总结

以上是大佬教程为你收集整理的Java秒杀系统二:Service层全部内容,希望文章能够帮你解决Java秒杀系统二:Service层所遇到的程序开发问题。

如果觉得大佬教程网站内容还不错,欢迎将大佬教程推荐给程序员好友。

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。
标签: