Java秒杀方案(1)

1.课程介绍

1.1技术点介绍

image.png

1.2 课程介绍

image.png

2.学习目标

3.如何设计一个秒杀系统

秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化理念是尽量减少用户到服务端”读“数据,或者让他们读更少的数据;并发写的处理原则也一样,他要求我们在数据库层面独立出来一个库,做特殊的处理,另外,我们还要针对系统做一些保护,针对意料之外的情况设计兜底方案,以防止最坏的情况发生。

秒杀的整体架构可以概括为”稳,准,快“几个关键字。
所谓”稳“就是整个系统架构要满足高可用,流量符合预期时肯定要稳,就是超出预期也同样不能掉链子,保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这是最基本的前提。
”准“就是秒杀10台iPhone,那就只能成交10台,多一台少一台都不行。一旦库存不对,那平台就要承担损失,所以”准“就是要求保证数据的一致性。
”快“是说系统的性能足够高,否则你怎么支撑这么大的流量?不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个系统就完美了。

所以从技术角度”稳,准,快“,就对应了架构上的高可用,一致性和高性能的要求

  • 高性能 秒杀设计大量的并发读和并发写,因此支持高并发访问这点非常关键,对应的方案比如动静分离方案,热点的发现与隔离,请求的肖峰与分层过滤,服务端的极致优化。
  • 一致性 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为”拍下减库存“”付款减库存“以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知
  • 高可用 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,还要设计PlanB来兜底,以便在最坏情况发生时仍然能够从容应对。

4.项目搭建

4.1 pom文件

<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.0</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.xxx</groupId>
    <artifactId>seckill-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>seckill-demo</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
<!--        thymeleaf模板-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
<!--        web框架-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
<!--        mysql驱动-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
<!--        mybatis-plus插件-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.3</version>
        </dependency>
<!--        lombok插件-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
<!--        测试-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

4.2 配置文件

spring:
  #thymeleaf配置
  thymeleaf:
    #关闭缓存
    cache: false
  #数据源配置
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql:///seckill?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
    username: 
    password: 
    hikari:
      #连接池名
      pool-name: DataHikariCP
      #最小空闲连接数
      minimum-idle: 5
      # 空闲连接存活最大时间,默认600000(10分钟)
      idle-timeout: 1800000
      #最大连接数,默认10
      maximum-pool-size: 10
      #从连接池返回的连接自动提交
      auto-commit: true
      #连接最大存活时间,0表示永久存活,默认1800000(30分钟)
      max-lifetime: 1800000
      #连接超时时间,默认30000(30秒)
      connection-timeout: 30000
      #测试连接是否可用的查询语句
      connection-test-query: SELECT 1

#Mybatis-plus配置
mybatis-plus:
  #配置Mapper.xml映射文件
  mapper-locations: classpath*:/mapper/*Mapper.xml
  #配置MyBatis数据返回类型别名(默认别名是类名)
  type-aliases-package: com.xxx.seckill.pojo


#MyBatis SQL打印(方法接口所在的包,不是Mapper.xml所在的包)
logging:
  level:
    com.xxx.seckill.mapper: debug

4.3 启动类

package com.xxx.seckill;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.xxx.seckill.pojo")
public class SeckillDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SeckillDemoApplication.class, args);
    }

}

4.4 测试接口

package com.xxx.seckill.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

/**
 * 测试
 */
@Controller
@RequestMapping("/demo")
public class DemoController {

    /**
     * 功能描述:测试页面跳转
     * @return
     */
    @RequestMapping("/hello")
    public String hello(Model model){
        model.addAttribute("name", "xxxx");
        return "hello";
    }

}

4.5 测试页面

<!DOCTYPE html>
<html lang="en"
    xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>测试</title>
</head>
<body>

<p th:text="'hello'+${name}"></p>
</body>
</html>

5 分布式会话

5.1 实现登录功能

5.1.1 两次MD5加密

5.1.1.1 为什么要两次MD5加密?

第一次MD5加密因为客户端传输给服务器,为了使明文不爆露在网络传输过程中。
服务器接收到客户端传过来的MD5加密的密码第二次MD5加密,将二次加密过后的密码和盐存入到数据库中,即便数据库中的数据泄露,能拿到数据库中的二次加密后的密码,也无法获取到用户的明文密码。

5.1.1.2 MD5加密的依赖

        <dependency>
            <groupId>commons-codec</groupId>
            <artifactId>commons-codec</artifactId>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.7</version>
        </dependency>

5.1.1.3 MD5Util实现

import org.apache.commons.codec.digest.DigestUtils;
import org.springframework.stereotype.Component;

/**
 * MD5工具类
 */
@Component
public class MD5Util {

    public static String md5(String src){
        //DigestUtils生成MD5密码
        return DigestUtils.md5Hex(src);
    }

    private static final String salt = "1a2b3c4d";

    /**
     * 对输入的密码进行第一次加密
     * @param inputPass
     * @return
     */
    public static String inputPassToFromPass(String inputPass){
        //对密码进行混淆
        String str = salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
        return md5(str);
    }

    /**
     * 二次加密:对客户端传入到服务器的密码进行二次加密。
     * @param formPass 第一次加密过后的数据
     * @param salt
     * @return
     */
    public static String formPassToDBPass(String formPass, String salt){
        String str = salt.charAt(0) + salt.charAt(2) + formPass + salt.charAt(5) + salt.charAt(4);
        return md5(salt);
    }

    /**
     * 对明文密码进行两次加密,第二次加密指定盐,返回加密后的密文。
     * @param inputPass
     * @param salt
     * @return
     */
    public static String inputPassToDBPass(String inputPass, String salt){
        String fromPass = inputPassToFromPass(inputPass);
        String dbPass = formPassToDBPass(fromPass, salt);
        return dbPass;
    }
}

5.1.2 逆向工程

使用MyBatis-Plus插件协助生成
MyBatis-Plus官方网站:MyBatis-Plus

5.1.2.1 新建项目添加依赖

<!--        mybatis-plus依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.2</version>
        </dependency>
<!--        mybatis-plus-generator代码生成器依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.4.1</version>
        </dependency>
<!--        freemarker模板引擎依赖-->
        <dependency>
            <groupId>org.freemarker</groupId>
            <artifactId>freemarker</artifactId>
            <version>2.3.31</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.28</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.22</version>
            <scope>provided</scope>
        </dependency>

5.1.2.2 官网测试代码

package com.xxx.generator;


import com.baomidou.mybatisplus.core.exceptions.MybatisPlusException;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.InjectionConfig;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;

import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;

/**
 * 演示例子,执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
 */
public class CodeGenerator {

    /**
     * <p>
     * 读取控制台内容
     * </p>
     */
    public static String scanner(String tip) {
        Scanner scanner = new Scanner(System.in);
        StringBuilder help = new StringBuilder();
        help.append("请输入" + tip + ":");
        System.out.println(help.toString());
        if (scanner.hasNext()) {
            String ipt = scanner.next();
            if (StringUtils.isNotBlank(ipt)) {
                return ipt;
            }
        }
        throw new MybatisPlusException("请输入正确的" + tip + "!");
    }

    public static void main(String[] args) {
        // 代码生成器
        AutoGenerator mpg = new AutoGenerator();

        // 全局配置
        GlobalConfig gc = new GlobalConfig();
        String projectPath = System.getProperty("user.dir");
        gc.setOutputDir(projectPath + "/src/main/java");
        //作者
        gc.setAuthor("initialize liu");
        //是否要打开输出目录
        gc.setOpen(false);
        // gc.setSwagger2(true); 实体属性 Swagger2 注解
        mpg.setGlobalConfig(gc);

        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/seckill?useUnicode=true&useSSL=false&characterEncoding=utf8");
        // dsc.setSchemaName("public");
        dsc.setDriverName("com.mysql.cj.jdbc.Driver");
        dsc.setUsername("");
        dsc.setPassword("");
        mpg.setDataSource(dsc);

        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setModuleName(scanner("模块名"));
        pc.setParent("com.xxx.generator")
            .setEntity("pojo");
        mpg.setPackageInfo(pc);

        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
            }
        };

        // 如果模板引擎是 freemarker
        String templatePath = "/templates/mapper.xml.ftl";
        // 如果模板引擎是 velocity
        // String templatePath = "/templates/mapper.xml.vm";

        // 自定义输出配置
        List<FileOutConfig> focList = new ArrayList<>();
        // 自定义配置会被优先输出
        focList.add(new FileOutConfig(templatePath) {
            @Override
            public String outputFile(TableInfo tableInfo) {
                // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
                return projectPath + "/src/main/resources/mapper/" + pc.getModuleName()
                        + "/" + tableInfo.getEntityName() + "Mapper" + StringPool.DOT_XML;
            }
        });
        /*
        cfg.setFileCreate(new IFileCreate() {
            @Override
            public boolean isCreate(ConfigBuilder configBuilder, FileType fileType, String filePath) {
                // 判断自定义文件夹是否需要创建
                checkDir("调用默认方法创建的目录,自定义目录用");
                if (fileType == FileType.MAPPER) {
                    // 已经生成 mapper 文件判断存在,不想重新生成返回 false
                    return !new File(filePath).exists();
                }
                // 允许生成模板文件
                return true;
            }
        });
        */
        cfg.setFileOutConfigList(focList);
        mpg.setCfg(cfg);

        // 配置模板
        TemplateConfig templateConfig = new TemplateConfig();
        //配置自定义输出模板
//                .setEntity("templates/entity2.java")
//                .setMapper("templates/mapper2.java")
//                .setService("templates/service2.java")
//                .setServiceImpl("templates/serviceImpl2.java")
//                .setController("template/controller2.java");

        // 配置自定义输出模板
        //指定自定义模板路径,注意不要带上.ftl/.vm, 会根据使用的模板引擎自动识别
        // templateConfig.setEntity("templates/entity2.java");
        // templateConfig.setService();
        // templateConfig.setController();

        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);

        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        //数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        //数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
//        strategy.setSuperEntityClass("你自己的父类实体,没有就不用设置!");
        //lombok模型
        strategy.setEntityLombokModel(true);
        //生成@RestController控制器
//        strategy.setRestControllerStyle(true);
        // 公共父类
//        strategy.setSuperControllerClass("你自己的父类控制器,没有就不用设置!");
        // 写于父类中的公共字段
//        strategy.setSuperEntityColumns("id");
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        //表前缀
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }

}

5.1.2.3 使用步骤

  • 运行main程序


    image.png
  • 查看生成目录


    image.png
  • 将生成的目录结构拷贝到seckill项目

5.1.3 登录功能

5.1.3.1 HTML

<!DOCTYPE html>
<html lang="en"
    xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <link rel="stylesheet" type="text/css" th:href="@{/bootstrap/css/bootstrap.min.css}">
    <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
    <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
    <script type="text/javascript" th:src="@{/js/common.js}"></script>
</head>
<body>
    <form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0 auto">
        <h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
        <div class="form-group">
            <div class="row">
                <label class="form-label col-md-4">请输入手机号码</label>
                <div class="col-md-5">
                    <input id="mobile" name="mobile" class="form-control" type="text" placeholder="手机号码" required="true"/>
<!--                           minlength="11" maxlength="11"-->
                </div>
                <div class="col-md-1">
                </div>
            </div>
        </div>
        <div class="form-group">
            <div class="row">
                <label class="form-label col-md-4">请输入密码</label>
                <div class="col-md-5">
                    <input id="password" name="password" class="form-control" type="password" placeholder="密码" required="true"/>
<!--                           minlength="6" maxlength="16"-->
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-md-5">
                <button class="btn btn-primary btn-block" type="reset" onclick="reset()">重置</button>
            </div>
            <div class="col-md-5">
                <button class="btn btn-primary btn-block" type="submit" onclick="login()">登录</button>
            </div>
        </div>
    </form>
</body>
<script>
    function login(){
        $("#loginForm").validate({
           submitHandler: function(form){
               doLogin();
           }
        });
    }
    function doLogin(){
        g_showLoading();

        var inputPass = $("#password").val();
        var salt = g_passsword_salt;
        var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
        var password = md5(str);

        $.ajax({
            url: "/login/doLogin",
            type: "POST",
            data: {
                mobile: $("#mobile").val(),
                password: password
            },
            success: function (data){
                layer.closeAll();
                if(data.code == 200) {
                    layer.msg("成功");
                }else{
                    layer.msg(data.message);
                }
            },
            error: function (){
                layer.closeAll();
            }
        });
    }
</script>
</html>

5.1.3.2 Controller

package com.xxx.seckill.controller;


import com.xxx.seckill.service.IUserService;
import com.xxx.seckill.vo.LoginVo;
import com.xxx.seckill.vo.RespBean;
import com.xxx.seckill.vo.RespBeanEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * 登录
 *
 *
 */
@Controller
@RequestMapping("/login")
@Slf4j
public class LoginController {

    @Autowired
    private IUserService userService;


    /**
     * 功能描述: 跳转到登录页面
     * @return
     */
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }


    /**
     * 功能描述:成功返回结果
     */
    @RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin(LoginVo loginVo){

        //打日志
        log.info("()" + loginVo);
        return userService.doLogin(loginVo);
    }


    public static RespBean success(Object obj){
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), obj);
    }

    /**
     * 功能描述:失败返回结果
     */
    public static RespBean error(RespBeanEnum respBeanEnum){
        return new RespBean(respBeanEnum.getCode(), respBeanEnum.getMessage(), null);
    }

    /**
     * 功能描述:失败的返回结果
     * @param respBeanEnum
     * @param obj
     * @return
     */
    public static RespBean error(RespBeanEnum respBeanEnum, Object obj){
        return new RespBean(respBeanEnum.getCode(), respBeanEnum.getMessage(), obj);
    }
}

5.1.3.3 Service

package com.xxx.seckill.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.xxx.seckill.mapper.UserMapper;
import com.xxx.seckill.pojo.User;
import com.xxx.seckill.service.IUserService;
import com.xxx.seckill.util.MD5Util;
import com.xxx.seckill.util.ValidatorUtil;
import com.xxx.seckill.vo.LoginVo;
import com.xxx.seckill.vo.RespBean;
import com.xxx.seckill.vo.RespBeanEnum;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.thymeleaf.util.StringUtils;

/**
 * <p>
 *  服务实现类
 * </p>
 *
 * @author initialize liu
 * @since 2022-07-05
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {

    @Autowired
    private UserMapper userMapper;
    /**
     * 登录验证
     * @param loginVo
     * @return
     */
    @Override
    public RespBean doLogin(LoginVo loginVo) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();

        //参数校验
        if(StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        if(!ValidatorUtil.isMobile(mobile)){
            return RespBean.error(RespBeanEnum.MOBILE_ERROR);
        }
        //根据手机号获取用户
        User user = userMapper.selectById(mobile);
        if(null == user){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        if(!MD5Util.formPassToDBPass(password, user.getSlat()).equals(user.getPassword())){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        return RespBean.success();
    }
}

5.1.3.4 Mapper


import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.xxx.seckill.pojo.User;


/**
 * <p>
 *  Mapper 接口
 * </p>
 *
 * @author initialize liu
 * @since 2022-07-05
 */
public interface UserMapper extends BaseMapper<User> {

}

5.1.3.5 手机号验证类,响应实体

/**
 * 手机号码效验
 */
public class ValidatorUtil {

    private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");

    public static boolean isMobile(String mobile){
        if(StringUtils.isEmpty(mobile)){
            return false;
        }
        Matcher matcher = mobile_pattern.matcher(mobile);
        return matcher.matches();
    }
}

import lombok.Data;

/**
 * 登录参数
 */
@Data
public class LoginVo {
    private String mobile;
    private String password;
}


@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {

    //通用
    SUCCESS(200, "SUCCESS"),
    ERROR(500, "服务端异常"),

    //登录模块
    LOGIN_ERROR(500210, "用户名或密码不正确"),
    MOBILE_ERROR(500211, "手机号码格式不正确");




    private final Integer code;
    private final String message;
}


@Data
@NoArgsConstructor
@AllArgsConstructor
public class RespBean {

    private long code;
    private String message;
    private Object obj;


    public static RespBean success(){
        return new RespBean(RespBeanEnum.SUCCESS.getCode(), RespBeanEnum.SUCCESS.getMessage(), null);
    }

    public static RespBean error(RespBeanEnum errorEnum){
        return new RespBean(errorEnum.getCode(), errorEnum.getMessage(), null);
    }
}

5.2 参数效验优化

5.2.1 优化原因

代码冗余

    @Override
    public RespBean doLogin(LoginVo loginVo) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();

        //参数校验
//        if(StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
//            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
//        }
//
//        if(!ValidatorUtil.isMobile(mobile)){
//            return RespBean.error(RespBeanEnum.MOBILE_ERROR);
//        }
        //根据手机号获取用户
        User user = userMapper.selectById(mobile);
        if(null == user){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        if(!MD5Util.formPassToDBPass(password, user.getSlat()).equals(user.getPassword())){
            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        return RespBean.success();
    }

5.2.2 引入validator进行参数优化

<!--        validation组件-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>

5.2.2.1 Controller借助@Valid完成参数优化

关于@Valid的使用在学习springBoot的时候,学习过。

    /**
     * 功能描述:成功返回结果
     */
    @RequestMapping("/doLogin")
    @ResponseBody
    public RespBean doLogin(@Valid LoginVo loginVo){

        //打日志
        log.info("()" + loginVo);
        return userService.doLogin(loginVo);
    }

5.2.2.2 LoginVo视图实体,添加自定义注解

/**
 * 登录参数
 */
@Data
public class LoginVo {

    @NotNull
    @IsMobile
    private String mobile;

    @NotNull
    @Length(min = 32)
    private String password;
}

5.2.2.3 IsMobile注解效验mobile

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class}) //约束条件
public @interface IsMobile {

    boolean required() default true;

    String message() default "手机号格式错误";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};


}

5.2.2.4 IsMobileValidator约束条件

mport com.xxx.seckill.util.ValidatorUtil;
import org.thymeleaf.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

public class IsMobileValidator implements ConstraintValidator<IsMobile, String> {

    private boolean required = false;

    @Override
    public void initialize(IsMobile constraintAnnotation) {
        required = constraintAnnotation.required();
    }

    @Override
    public boolean isValid(String value, ConstraintValidatorContext constraintValidatorContext) {
        if(required){
            //调用自定义的验证工具
            return ValidatorUtil.isMobile(value);
        }else{
            if(StringUtils.isEmpty(value)){
                return true;
            }else {
                return ValidatorUtil.isMobile(value);
            }
        }
    }

}

5.2.3 手机号验证

client传输错误的手机号,服务器的日志中会有提醒。

2022-07-06 14:59:11.985 WARN 10420 --- [nio-8080-exec-4] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved [org.springframework.validation.BindException: org.springframework.validation.BeanPropertyBindingResult: 1 errors<EOL>Field error in object 'loginVo' on field 'mobile': rejected value [11111111111]; codes [IsMobile.loginVo.mobile,IsMobile.mobile,IsMobile.java.lang.String,IsMobile]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [loginVo.mobile,mobile]; arguments []; default message [mobile],true]; default message [手机号格式错误]]

注意:这时候提示的错误只是在服务器端的日志中,错误没有被捕获并发送给client.

5.3 异常处理

系统中异常包括:编译时异常和运行时异常RuntimeException,前者通过捕获异常从而获取异常信息,后者主要通过规范代码开发,测试通过手段减少运行时异常的发生,在开发中,不管是dao层,service层,controller层都可能抛出异常,在Springmvc中,能将所有的类型异常处理从个各处理过程解耦出来,既保证相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。
SpringBoot全局异常处理方式主要有两种:

  • 使用@ControllerAdvice和@ExceptionHandler注解
  • 使用ErrorController类来实现

区别:

    1. @ControllerAdvice方式只能处理控制器抛出的异常,此时请求已经进入控制器中。
    1. ErrorController类方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误
    1. 如果应用中两者共同存在,则@ControllerAdvice方式处理控制器抛出的异常,ErrorController类方式处理未进入控制器的异常。
    1. @ControllerAdvice方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常信息,自由度更大。

这里提供的只是异常处理的一种方式。

5.3.1 定义全局异常


import com.xxx.seckill.vo.RespBeanEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * 全局异常处理类
 */
@Data
@AllArgsConstructor
@NoArgsConstructor
public class GlobalException extends RuntimeException{

    private RespBeanEnum respBeanEnum;
}

5.3.2 定义全局异常处理方式

@RestControllerAdvice: 处理Controller过程中抛出的异常,切面的方式实现。

import com.xxx.seckill.vo.RespBean;
import com.xxx.seckill.vo.RespBeanEnum;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;


/**
 * 全局异常处理类
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(Exception.class)
    public RespBean ExcpeitonHandler(Exception e){

        if(e instanceof GlobalException){
            GlobalException ex = (GlobalException) e;
            return RespBean.error(ex.getRespBeanEnum());
        }else if(e instanceof BindException){
            BindException ex = (BindException) e;
            RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
            respBean.setMessage("参数效验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
            return respBean;
        }
        return RespBean.error(RespBeanEnum.ERROR);
    }
}

5.3.3 UserService调整

    @Override
    public RespBean doLogin(LoginVo loginVo) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();

        //根据手机号获取用户
        User user = userMapper.selectById(mobile);
        if(null == user){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
//            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }

        if(!MD5Util.formPassToDBPass(password, user.getSlat()).equals(user.getPassword())){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
//            return RespBean.error(RespBeanEnum.LOGIN_ERROR);
        }
        return RespBean.success();
    }

5.4 Cookie完善登录功能

5.4.1 登录成功后,保存Cookie到Session

    @Override
    public RespBean doLogin(LoginVo loginVo, HttpServletRequest request, HttpServletResponse response) {
        String mobile = loginVo.getMobile();
        String password = loginVo.getPassword();

        //根据手机号获取用户
        User user = userMapper.selectById(mobile);
        if(null == user){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }

        if(!MD5Util.formPassToDBPass(password, user.getSlat()).equals(user.getPassword())){
            throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
        }

        //生成cookie
        String ticket = UUIDUtil.uuid();
        request.getSession().setAttribute(ticket, user);
        CookieUtil.setCookie(request, response, "userTicket", ticket);

        return RespBean.success();
    }

5.4.2 HTML跳转的商品列表

function doLogin(){
        g_showLoading();

        var inputPass = $("#password").val();
        var salt = g_passsword_salt;
        var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass + salt.charAt(5) + salt.charAt(4);
        var password = md5(str);

        $.ajax({
            url: "/login/doLogin",
            type: "POST",
            data: {
                mobile: $("#mobile").val(),
                password: password
            },
            success: function (data){
                layer.closeAll();
                if(data.code == 200) {
                    layer.msg("成功");
                    window.location.href = "/goods/toList";
                }else{
                    layer.msg(data.message);
                }
            },
            error: function (){
                layer.closeAll();
            }
        });
    }

5.4.3 GoodsController处理


/**
 * 商品
 */

@Controller
@RequestMapping("/goods")
public class GoodsController {

    /**
     * 跳转到商品列表页面
     * @param session
     * @param model
     * @param ticket
     * @return
     */
    @RequestMapping("/toList")
    public String toList(HttpSession session, Model model, @CookieValue("userTicket") String ticket){
        if(StringUtils.isEmpty(ticket)){
            return "login";
        }
        User user = (User)session.getAttribute(ticket);
        if(null == user){
            return "login";
        }
        model.addAttribute("user", user);
        return "goodsList";
    }


}

5.4.4 CookieUtil工具

/**
 * 生成UUID
 */
public class UUIDUtil {

    public static String uuid(){
        return UUID.randomUUID().toString().replace("-", "");
    }
}



import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.net.URLEncoder;

/**
 * Cookie工具类
 */
public final class CookieUtil {


    /**
     * 得到Cookie的值,不编码
     * @param request
     * @param cookieName
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName){
        return getCookieValue(request, cookieName, false);
    }

    /**
     * 得到Cookie的值
     * @param request
     * @param cookieName
     * @param isDecoder
     * @return
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder){
        Cookie[] cookieList = request.getCookies();
        if(cookieList == null || cookieName == null){
            return null;
        }
        String retValue = null;
        try{
            for(int i = 0; i < cookieList.length; i++){
                if(cookieList[i].getName().equals(cookieName)){
                    if(isDecoder){
                        retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
                    }else{
                        retValue = cookieList[i].getValue();
                    }
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 得到Cookie的值
     */
    public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString){
        Cookie[] cookieList = request.getCookies();
        if(cookieList == null || cookieName == null){
            return null;
        }
        String retValue = null;
        try{
            for(int i = 0; i < cookieList.length; i++){
                if(cookieList[i].getName().equals(cookieName)){
                    retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
                    break;
                }
            }
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        return retValue;
    }

    /**
     * 设置Cookie的值,不设置生效时间,默认浏览器关闭即失效,也不编码
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue){
        setCookie(request, response, cookieName, cookieValue, -1);
    }

    /**
     * 设置Cookie的值,在指定时间内生效,但不编码
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage){
        setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
    }


    /**
     * 设置Cookie的值,不设置生效时间,但编码
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param isEncode
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, boolean isEncode){
        setCookie(request, response, cookieName, cookieValue, -1, isEncode);
    }


    /**
     * 设置Cookie的值,,在指定时间内生效,编码参数
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage
     * @param isEncode
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode){
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
    }


    /**
     * 设置Cookie的值,在指定时间内生效,编码参数(指定编码)
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage
     * @param encodeString
     */
    public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString){
        doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
    }

    /**
     * 删除Cookie带cookie域名
     * @param request
     * @param response
     * @param cookieName
     */
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response, String cookieName){
        doSetCookie(request, response, cookieName, "", -1, false);
    }

    /**
     * 设置Cookie的值,并使其在指定时间内生效
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage
     * @param isEncode
     */
    private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, boolean isEncode){
        try{
            if(cookieValue == null){
                cookieValue = "";
            } else if(isEncode){
                cookieValue = URLEncoder.encode(cookieValue, "utf-8");
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if(cookieMaxage > 0)
                cookie.setMaxAge(cookieMaxage);
            if(null != request){
                //设置域名的cookie
                String domainName = getDomainName(request);
                System.out.println(domainName);
                if(!"locallhost".equals(domainName)){
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        }catch (Exception e){
            e.printStackTrace();
        }
    }

    /**
     * 设置Cookie的值,并使其在指定时间内生效,指定编码格式
     * @param request
     * @param response
     * @param cookieName
     * @param cookieValue
     * @param cookieMaxage
     * @param encodeString
     */
    private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response, String cookieName, String cookieValue, int cookieMaxage, String encodeString){
        try{
            if(cookieValue == null){
                cookieValue = "";
            }else{
                cookieValue = URLEncoder.encode(cookieValue, encodeString);
            }
            Cookie cookie = new Cookie(cookieName, cookieValue);
            if(cookieMaxage > 0){
                cookie.setMaxAge(cookieMaxage);
            }
            if(null != request){    //设置域名的cookie
                String domainName = getDomainName(request);
                System.out.println(domainName);
                if(!"localhost".equals(domainName)){
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }


    /**
     * 得到cookie的域名
     * @param request
     * @return
     */
    private static final String getDomainName(HttpServletRequest request){
        String domainName = null;
        //通过reqeust对象获取访问的url地址
        String serverName = request.getRequestURL().toString();
        if(serverName == null || serverName.equals("")){
            domainName = "";
        }else{
            //将url地址转换为小写
            serverName = serverName.toLowerCase();
            //如果url地址是以http://开头,将http://截取
            if(serverName.startsWith("http://")){
                serverName = serverName.substring(7);
            }
            int end = serverName.length();
            //判断url地址是否包含"/"
            if (serverName.contains("/")){
                //得到第一个"/"出现的位置
                end = serverName.indexOf("/");
            }
            //截取
            serverName = serverName.substring(0, end);
            //根据"."进行分割
            final String[] domains = serverName.split("\\.");
            int len = domains.length;
            if(len > 3){
                //www.xxx.com.cn
                domainName = domains[len - 3] + "." + domains[len - 2] + "." + domains[len - 1];
            }else if(len <= 3 && len > 1){
                //xxx.com or xxx.cn
                domainName = domains[len - 2] + "." + domains[len - 1];
            }else {
                domainName = serverName;
            }
        }
        if(domainName != null && domainName.indexOf(":") > 0){
            String[] ary = domainName.split("\\:");
            domainName = ary[0];
        }
        return domainName;
    }

}

5.4.5 查看浏览器中的cookie

image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容