秒杀系统项目学习

1.学习目标

2.如何设计

秒杀,对我们来说,都不是一个陌生的东西。每年的双11,618以及时下流行的直播等等。秒杀然而,这 对于我们系统而言是一个巨大的考验。

那么,如何才能更好地理解秒杀系统呢?我觉得作为一个程序员,你首先需要从高维度出发,从整体上 思考问题。在我看来,秒杀其实主要解决两个问题,一个是并发读,一个是并发写。并发读的核心优化 理念是尽量减少用户到服务端来“读”数据,或者让他们读更少的数据;并发写的处理原则也一样,它要 求我们在数据库层面独立出来一个库,做特殊的处理。另外,我们还要针对秒杀系统做一些保护,针对 意料之外的情况设计兜底方案,以防止最坏的情况发生。

其实,秒杀的整体架构可以概括为“稳、准、快”几个关键字。

首先,就是整个系统架构要满足高可用,流量符合预期时肯定要稳定,就是超出预期时也同样不能掉链子,你要保证秒杀活动顺利完成,即秒杀商品顺利地卖出去,这个是最基本的前提。

然后,就是“准”,就是秒杀 10 台 iPhone,那就只能成交 10 台,多一台少一台都不行。一旦库存不对, 那平台就要承担损失,所以“准”就是要求保证数据的一致性。

最后,再看“快”,“快”其实很好理解,它就是说系统的性能要足够高,否则你怎么支撑这么大的流量呢? 不光是服务端要做极致的性能优化,而且在整个请求链路上都要做协同的优化,每个地方快一点,整个 系统就完美了。

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

  • 高性能。 秒杀涉及大量的并发读和并发写,因此支持高并发访问这点非常关键。对应的方案比如动静分离方案、热点的发现与隔离、请求的削峰与分层过滤、服务端的极致优化

  • 一致性。 秒杀中商品减库存的实现方式同样关键。可想而知,有限数量的商品在同一时刻被很多倍的请求同时来减库存,减库存又分为“拍下减库存”“付款减库存”以及预扣等几种,在大并发更新的过程中都要保证数据的准确性,其难度可想而知

  • 高可用。 现实中总难免出现一些我们考虑不到的情况,所以要保证系统的高可用和正确性,我们 还要设计一个 PlanB 来兜底,以便在最坏情况发生时仍然能够从容应对。

3.项目搭建

1.创建项目

Spring项目,JDK1.8,勾选Spring Web,Thymeleaf,Lombok

2.添加依赖

<!--mybatisplus依赖-->
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>3.4.0</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
    <version>5.1.49</version>
</dependency>

3.创建基本目录

4.修改配置文件

spring:
    # thymelaef配置
    thymeleaf:
        # 关闭缓存
        cache: false
    # 数据源配置
    datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/seckill?useUnicode=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
        username: root
        password: 123456
        hikari:
            #连接池名
            pool-name: DateHikariCP
            # 最小空闲连接出
            minimum-idle: 5
            # 空闲连接存活最大时间,默认600000(10分钟)
            idle-timeout: 600000
            #最大连接数,默认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.xxxx.seckill.pojo

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

5.测试

创建DemoController.java

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

创建hello.html

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

4.分布式会话

4.1.实现登录功能

4.1.1.两次MD5加密

客户端:PASS=MD5(明文+固定Salt)

服务端:PASS=MD5(用户输入+随机Salt)

用户端MD5加密是为了防止用户密码在网络中明文传输,服务端MD5加密是为了提高密码安全性,双重保险。

导入依赖

<!-- md5 依赖 -->
<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
    <version>3.6</version>
</dependency>

编写MD5工具类

创建utils文件夹,创建MD5Util.java

@Component
public class MD5Util {

    public static String md5(String src){
        return DigestUtils.md5Hex(src);
    }

    private static final String salt="1a2b3c4d";
    
    public static String inputPassToFromPass(String inputPass){
        String str = "" +salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
        return md5(str);
    }

    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(str);
    }

    public static String inputPassToDBPass(String inputPass,String salt){
        String fromPass = inputPassToFromPass(inputPass);
        String dbPass = formPassToDBPass(fromPass, salt);
        return dbPass;
    }


    public static void main(String[] args) {
        // d3b1294a61a07da9b49b6e22b2cbd7f9
        System.out.println(inputPassToFromPass("123456"));
        System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
        System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
    }
}

4.1.2.登录功能实现

创建数据库

数据库:seckill,创建t_user表

CREATE TABLE t_user(
	`id` BIGINT(20) NOT NULL COMMENT '用户ID shoujihaoma',
	`nickname` VARCHAR(255) not NULL,
	`pasword`  VARCHAR(32) DEFAULT NULL COMMENT 'MD5二次加密',
	`slat` VARCHAR(10) DEFAULT NULL,
	`head` VARCHAR(128) DEFAULT NULL COMMENT '头像',
	`register_date` datetime DEFAULT NULL COMMENT '注册时间',
	`last_login_date` datetime DEFAULT NULL COMMENT '最后一次登录时间',
	`login_count` int(11) DEFAULT '0' COMMENT '登录次数',
	PRIMARY KEY(`id`)
);

逆向工程

首先需要通过逆向工程基于 t_user 表生产对应的POJO、Mapper、Service、ServiceImpl、Controller 等类,项目中使用了MybatisPlus,所以逆向工程也是用了MybatisPlus提供的AutoGenerator,代码如 下。具体可去官网查看

创建新项目generator

添加依赖

<!--mybatisplus依赖-->
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-boot-starter</artifactId>
   <version>3.4.0</version>
</dependency>
<dependency>
   <groupId>com.baomidou</groupId>
   <artifactId>mybatis-plus-generator</artifactId>
   <version>3.4.0</version>
</dependency>
<dependency>
   <groupId>org.freemarker</groupId>
   <artifactId>freemarker</artifactId>
   <version>2.3.30</version>
</dependency>
<dependency>
   <groupId>mysql</groupId>
   <artifactId>mysql-connector-java</artifactId>
   <version>5.1.49</version>
   <scope>runtime</scope>
</dependency>

创建文件夹generator

创建CodeGenerator.java

/**
* 执行 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("sxs");
        //打开输出目录
        gc.setOpen(false);
        //xml开启 BaseResultMap
        gc.setBaseResultMap(true);
        //xml 开启BaseColumnList
        gc.setBaseColumnList(true);
        //日期格式,采用Date
        gc.setDateType(DateType.ONLY_DATE);
        mpg.setGlobalConfig(gc);
        // 数据源配置
        DataSourceConfig dsc = new DataSourceConfig();
        dsc.setUrl("jdbc:mysql://localhost:3306/seckill?useUnicode=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
        dsc.setDriverName("com.mysql.jdbc.Driver");
        dsc.setUsername("root");
        dsc.setPassword("123456");
        mpg.setDataSource(dsc);
        // 包配置
        PackageConfig pc = new PackageConfig();
        pc.setParent("com.xxxx.seckill")
                .setEntity("pojo")
                .setMapper("mapper")
                .setService("service")
                .setServiceImpl("service.impl")
                .setController("controller");
        mpg.setPackageInfo(pc);
        // 自定义配置
        InjectionConfig cfg = new InjectionConfig() {
            @Override
            public void initMap() {
                // to do nothing
                Map<String, Object> map = new HashMap<>();
                map.put("date1", "1.0.0");
                this.setMap(map);
            }
        };
        // 如果模板引擎是 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/" +
                        tableInfo.getEntityName() + "Mapper"
                        + StringPool.DOT_XML;
            }
        });
        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("templates/controller2.java");
        templateConfig.setXml(null);
        mpg.setTemplate(templateConfig);
        // 策略配置
        StrategyConfig strategy = new StrategyConfig();
        //数据库表映射到实体的命名策略
        strategy.setNaming(NamingStrategy.underline_to_camel);
        //数据库表字段映射到实体的命名策略
        strategy.setColumnNaming(NamingStrategy.underline_to_camel);
        //lombok模型
        strategy.setEntityLombokModel(true);
        //生成 @RestController 控制器
        // strategy.setRestControllerStyle(true);
        strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
        strategy.setControllerMappingHyphenStyle(true);
        //表前缀
        strategy.setTablePrefix("t_");
        mpg.setStrategy(strategy);
        mpg.setTemplateEngine(new FreemarkerTemplateEngine());
        mpg.execute();
    }
}

导入模板

复制到

运行后,将得到的文件夹复制回我们的秒杀项目中

登录功能编写

LoginController.java

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

    @Autowired
    private IUserService userService;
    /*
    * 跳转登录页面
    * */
    @RequestMapping("/toLogin")
    public String toLogin(){
        return "login";
    }
    /**
    * 登录
    * @return
    */
   @RequestMapping("/doLogin")
   @ResponseBody
   public RespBean doLogin(LoginVo loginVo) {
      log.info(loginVo.toString());
      return userService.login(loginVo);
   }
 }

导入编写好的静态资源

login.html

<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <!-- jquery -->
    <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
    <!-- bootstrap -->
    <link rel="stylesheet" type="text/css"
          th:href="@{/bootstrap/css/bootstrap.min.css}"/>
    <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}">
    </script>
    <!-- jquery-validator -->
    <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>
    <!-- layer -->
    <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
    <!-- md5.js -->
    <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
    <!-- common.js -->
    <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("成功");
                    window.location.href="/goods/toList"
                } else {
                    layer.msg(data.message);
                }
            },
            error: function () {
                layer.closeAll();
            }
        });
    }
</script>
</html>

此时可以正常进入登录界面

添加公共返回对象

创建vo文件夹

创建RespBeanEnum.java

@Getter
@ToString
@AllArgsConstructor
public enum RespBeanEnum {
   //通用
   SUCCESS(200, "SUCCESS"),
   ERROR(500, "服务端异常"),
   //登录模块5002xx
   LOGIN_ERROR(500210, "用户名或密码不正确"),
   MOBILE_ERROR(500211, "手机号码格式不正确"),
   BIND_ERROR(500212, "参数校验异常"),
   MOBILE_NOT_EXIST(500213, "手机号码不存在"),
   PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),
   SESSION_ERROR(500215, "用户不存在");

   private final Integer code;
   private final String message;
}

创建RespBean.java

@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 success(Object obj){
      return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
   }

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

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

}

编写登录参数

LoginVo.java

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

}

此时登录输入参数已经可以得到我们输入的手机号以及加密的密码

添加数据库信息:18012345678 123456

继续编写登录服务类

IUserService.java

/**
* 服务类
*/
public interface IUserService extends IService<User> {
 /**
 * 登录
 * @param loginVo
 * @return
 */
 RespBean login(LoginVo loginVo);
}

服务实现类

UserServiceImpl.java

/**
* 服务实现类
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements
IUserService {
   @Autowired
   private UserMapper userMapper;

   /**
    * 登录
    * @param loginVo
    * @return
    */
   @Override
   public RespBean login(LoginVo loginVo) {
      String mobile = loginVo.getMobile();
      String password = loginVo.getPassword();
      if (StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      if (!ValidatorUtil.isMobile(mobile)){
         return RespBean.error(RespBeanEnum.MOBILE_ERROR);
     }
      //根据手机号获取用户
      User user = userMapper.selectById(mobile);
      if (null==user){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      //校验密码
      if
(!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
         return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
     }
      return RespBean.success();
   }
}

添加校验手机工具类

/**
 * 手机号码校验
 */
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();
   }

}

运行类:

@SpringBootApplication
@MapperScan("com.xxxx.seckill.mapper")
public class SeckillApplication {

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

}

测试登录功能

手机号码格式不正确

密码不正确

正确登录

4.2.参数校验

每个类都写大量的健壮性判断过于麻烦,我们可以使用 validation简化我们的代码

注入依赖

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

修改loginVo

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

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

}

自定义注解 IsMobile

创建新文件夹validation,

IsMobile.java

/**
 * 验证手机号
 */
@Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
@Retention(RUNTIME)
@Documented
@Constraint(validatedBy = {IsMobileValidator.class})
public @interface IsMobile {

   boolean required() default true;

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

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

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

自定义手机号码验证规则

添加在vo中

IsMobileValidator.java

/**
 * 手机号码校验规则
 */
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 context) {
      if (required){
         return ValidatorUtil.isMobile(value);
      }else {
         if (StringUtils.isEmpty(value)){
            return true;
         }else {
            return ValidatorUtil.isMobile(value);
         }
      }
   }
}

将userServiceImp的这参数校验注释,

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

重新运行,发现界面没有展示,只在控制输出台显示拦截和异常。

解决:

在LoginController中加上注解 入参添加 @Valid

@RequestMapping("/doLogin")
@ResponseBody
public RespBean doLogin(@Valid LoginVo loginVo) {
   log.info(loginVo.toString());
   return userService.login(loginVo);
}

测试:

4.3.异常处理

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

  • 使用 @ControllerAdvice 和 @ExceptionHandler 注解。

  • 使用 ErrorController类 来实现

区别:

  1. @ControllerAdvice 方式只能处理控制器抛出的异常。此时请求已经进入控制器中。

  1. ErrorController类 方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误

  1. 如果应用中两者共同存在,则 @ControllerAdvice 方式处理控制器抛出的异常, ErrorController类 方式处理未进入控制器的异常

  1. @ControllerAdvice 方式可以定义多个拦截方法,拦截不同的异常类,并且可以获取抛出的异常 信息,自由度更大。

使用组合注解:@ControllerAdvice 和 @ExceptionHandler

创建exception文件夹

GlobalException.java

/**
 * 全局异常
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class GlobalException extends RuntimeException {
   private RespBeanEnum respBeanEnum;
}

GlobalExceptionHandler.java

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

   @ExceptionHandler(Exception.class)
   public RespBean ExceptionHandler(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);
   }

}

在RespBeanNum添加异常信息

修改之前代码 直接返回RespBean改为直接抛 GlobalException 异常

/**
* 登录
* @param loginVo
* @return
*/
@Override
public RespBean login(LoginVo loginVo) {
   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.getSalt()).equals(user.getPassword())){
      throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
   }
   return RespBean.success();
}

测试

4.4.分布式session

将session信息存放到第三方,客户端和服务端在通信时,去第三方存取session信息。

4.4.1.完善登录功能

使用cookie+session记录用户信息

准备工具类

CookieUtil.java

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

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

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

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

    /**
     * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
     */
    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域名
     */
    public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
                                    String cookieName) {
        doSetCookie(request, response, cookieName, "", -1, false);
    }

    /**
     * 设置Cookie的值,并使其在指定时间内生效
     *
     * @param cookieMaxage cookie生效的最大秒数
     */
    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 (!"localhost".equals(domainName)) {
                    cookie.setDomain(domainName);
                }
            }
            cookie.setPath("/");
            response.addCookie(cookie);
        } catch (Exception e) {
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值