当前位置:   article > 正文

秒杀系统项目学习_jetcache 预加载

jetcache 预加载

1.学习目标

2.如何设计

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

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

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

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

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

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

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

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

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

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

3.项目搭建

1.创建项目

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

2.添加依赖

  1. <!--mybatisplus依赖-->
  2. <dependency>
  3. <groupId>com.baomidou</groupId>
  4. <artifactId>mybatis-plus-boot-starter</artifactId>
  5. <version>3.4.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>mysql</groupId>
  9. <artifactId>mysql-connector-java</artifactId>
  10. <scope>runtime</scope>
  11. <version>5.1.49</version>
  12. </dependency>

3.创建基本目录

4.修改配置文件

  1. spring:
  2. # thymelaef配置
  3. thymeleaf:
  4. # 关闭缓存
  5. cache: false
  6. # 数据源配置
  7. datasource:
  8. driver-class-name: com.mysql.jdbc.Driver
  9. url: jdbc:mysql://localhost:3306/seckill?useUnicode=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
  10. username: root
  11. password: 123456
  12. hikari:
  13. #连接池名
  14. pool-name: DateHikariCP
  15. # 最小空闲连接出
  16. minimum-idle: 5
  17. # 空闲连接存活最大时间,默认600000(10分钟)
  18. idle-timeout: 600000
  19. #最大连接数,默认10
  20. maximum-pool-size: 10
  21. # 从连接池返回的连接自动提交
  22. auto-commit: true
  23. # 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
  24. max-lifetime: 1800000
  25. # 连接超时时间,默认30000(30秒)
  26. connection-timeout: 30000
  27. # 测试连接是否可用的查询语句
  28. connection-test-query: SELECT 1
  29. #Mybatis-plus配置
  30. mybatis-plus:
  31. # 配置Mapper.xml映射文件
  32. mapper-locations: classpath*:/mapper/*Mapper.xml
  33. # 配置MyBatis数据返回类型别名(默认别名是类名)
  34. type-aliases-package: com.xxxx.seckill.pojo
  35. # MyBatis SQL打印(方法接口所在的包,不是Mapper.xml所在的包)
  36. logging:
  37. level:
  38. com.xxxx.seckill.mapper: debug

5.测试

创建DemoController.java

  1. @Controller
  2. @RequestMapping("/demo")
  3. public class DemoController {
  4. /**
  5. * 功能描述: 测试页面跳转
  6. */
  7. @RequestMapping("/hello")
  8. public String hello(Model model){
  9. model.addAttribute("name","xxxx");
  10. return "hello";
  11. }
  12. }

创建hello.html

  1. <!DOCTYPE html>
  2. <html lang="en"
  3. xmlns:th="http://www.thymeleaf.org">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>测试</title>
  7. </head>
  8. <body>
  9. <p th:text="'hello:'+${name}"></p>
  10. </body>
  11. </html>

4.分布式会话

4.1.实现登录功能

4.1.1.两次MD5加密

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

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

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

导入依赖

  1. <!-- md5 依赖 -->
  2. <dependency>
  3. <groupId>commons-codec</groupId>
  4. <artifactId>commons-codec</artifactId>
  5. </dependency>
  6. <dependency>
  7. <groupId>org.apache.commons</groupId>
  8. <artifactId>commons-lang3</artifactId>
  9. <version>3.6</version>
  10. </dependency>

编写MD5工具类

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

  1. @Component
  2. public class MD5Util {
  3. public static String md5(String src){
  4. return DigestUtils.md5Hex(src);
  5. }
  6. private static final String salt="1a2b3c4d";
  7. public static String inputPassToFromPass(String inputPass){
  8. String str = "" +salt.charAt(0)+salt.charAt(2)+inputPass+salt.charAt(5)+salt.charAt(4);
  9. return md5(str);
  10. }
  11. public static String formPassToDBPass(String formPass,String salt){
  12. String str = "" +salt.charAt(0)+salt.charAt(2)+formPass+salt.charAt(5)+salt.charAt(4);
  13. return md5(str);
  14. }
  15. public static String inputPassToDBPass(String inputPass,String salt){
  16. String fromPass = inputPassToFromPass(inputPass);
  17. String dbPass = formPassToDBPass(fromPass, salt);
  18. return dbPass;
  19. }
  20. public static void main(String[] args) {
  21. // d3b1294a61a07da9b49b6e22b2cbd7f9
  22. System.out.println(inputPassToFromPass("123456"));
  23. System.out.println(formPassToDBPass("d3b1294a61a07da9b49b6e22b2cbd7f9","1a2b3c4d"));
  24. System.out.println(inputPassToDBPass("123456","1a2b3c4d"));
  25. }
  26. }

4.1.2.登录功能实现

创建数据库

数据库:seckill,创建t_user表

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

逆向工程

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

创建新项目generator

添加依赖

  1. <!--mybatisplus依赖-->
  2. <dependency>
  3. <groupId>com.baomidou</groupId>
  4. <artifactId>mybatis-plus-boot-starter</artifactId>
  5. <version>3.4.0</version>
  6. </dependency>
  7. <dependency>
  8. <groupId>com.baomidou</groupId>
  9. <artifactId>mybatis-plus-generator</artifactId>
  10. <version>3.4.0</version>
  11. </dependency>
  12. <dependency>
  13. <groupId>org.freemarker</groupId>
  14. <artifactId>freemarker</artifactId>
  15. <version>2.3.30</version>
  16. </dependency>
  17. <dependency>
  18. <groupId>mysql</groupId>
  19. <artifactId>mysql-connector-java</artifactId>
  20. <version>5.1.49</version>
  21. <scope>runtime</scope>
  22. </dependency>

创建文件夹generator

创建CodeGenerator.java

  1. /**
  2. * 执行 main 方法控制台输入模块表名回车自动生成对应项目目录中
  3. */
  4. public class CodeGenerator {
  5. /**
  6. * <p>
  7. * 读取控制台内容
  8. * </p>
  9. */
  10. public static String scanner(String tip) {
  11. Scanner scanner = new Scanner(System.in);
  12. StringBuilder help = new StringBuilder();
  13. help.append("请输入" + tip + ":");
  14. System.out.println(help.toString());
  15. if (scanner.hasNext()) {
  16. String ipt = scanner.next();
  17. if (StringUtils.isNotBlank(ipt)) {
  18. return ipt;
  19. }
  20. }
  21. throw new MybatisPlusException("请输入正确的" + tip + "!");
  22. }
  23. public static void main(String[] args) {
  24. // 代码生成器
  25. AutoGenerator mpg = new AutoGenerator();
  26. // 全局配置
  27. GlobalConfig gc = new GlobalConfig();
  28. String projectPath = System.getProperty("user.dir");
  29. gc.setOutputDir(projectPath + "/src/main/java");
  30. //作者
  31. gc.setAuthor("sxs");
  32. //打开输出目录
  33. gc.setOpen(false);
  34. //xml开启 BaseResultMap
  35. gc.setBaseResultMap(true);
  36. //xml 开启BaseColumnList
  37. gc.setBaseColumnList(true);
  38. //日期格式,采用Date
  39. gc.setDateType(DateType.ONLY_DATE);
  40. mpg.setGlobalConfig(gc);
  41. // 数据源配置
  42. DataSourceConfig dsc = new DataSourceConfig();
  43. dsc.setUrl("jdbc:mysql://localhost:3306/seckill?useUnicode=false&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
  44. dsc.setDriverName("com.mysql.jdbc.Driver");
  45. dsc.setUsername("root");
  46. dsc.setPassword("123456");
  47. mpg.setDataSource(dsc);
  48. // 包配置
  49. PackageConfig pc = new PackageConfig();
  50. pc.setParent("com.xxxx.seckill")
  51. .setEntity("pojo")
  52. .setMapper("mapper")
  53. .setService("service")
  54. .setServiceImpl("service.impl")
  55. .setController("controller");
  56. mpg.setPackageInfo(pc);
  57. // 自定义配置
  58. InjectionConfig cfg = new InjectionConfig() {
  59. @Override
  60. public void initMap() {
  61. // to do nothing
  62. Map<String, Object> map = new HashMap<>();
  63. map.put("date1", "1.0.0");
  64. this.setMap(map);
  65. }
  66. };
  67. // 如果模板引擎是 freemarker
  68. String templatePath = "/templates/mapper.xml.ftl";
  69. // 如果模板引擎是 velocity
  70. // String templatePath = "/templates/mapper.xml.vm";
  71. // 自定义输出配置
  72. List<FileOutConfig> focList = new ArrayList<>();
  73. // 自定义配置会被优先输出
  74. focList.add(new FileOutConfig(templatePath) {
  75. @Override
  76. public String outputFile(TableInfo tableInfo) {
  77. // 自定义输出文件名 , 如果你 Entity 设置了前后缀、此处注意 xml 的名称会跟着发生变化!!
  78. return projectPath + "/src/main/resources/mapper/" +
  79. tableInfo.getEntityName() + "Mapper"
  80. + StringPool.DOT_XML;
  81. }
  82. });
  83. cfg.setFileOutConfigList(focList);
  84. mpg.setCfg(cfg);
  85. // 配置模板
  86. TemplateConfig templateConfig = new TemplateConfig()
  87. .setEntity("templates/entity2.java")
  88. .setMapper("templates/mapper2.java")
  89. .setService("templates/service2.java")
  90. .setServiceImpl("templates/serviceImpl2.java")
  91. .setController("templates/controller2.java");
  92. templateConfig.setXml(null);
  93. mpg.setTemplate(templateConfig);
  94. // 策略配置
  95. StrategyConfig strategy = new StrategyConfig();
  96. //数据库表映射到实体的命名策略
  97. strategy.setNaming(NamingStrategy.underline_to_camel);
  98. //数据库表字段映射到实体的命名策略
  99. strategy.setColumnNaming(NamingStrategy.underline_to_camel);
  100. //lombok模型
  101. strategy.setEntityLombokModel(true);
  102. //生成 @RestController 控制器
  103. // strategy.setRestControllerStyle(true);
  104. strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
  105. strategy.setControllerMappingHyphenStyle(true);
  106. //表前缀
  107. strategy.setTablePrefix("t_");
  108. mpg.setStrategy(strategy);
  109. mpg.setTemplateEngine(new FreemarkerTemplateEngine());
  110. mpg.execute();
  111. }
  112. }

导入模板

复制到

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

登录功能编写

LoginController.java

  1. @Controller
  2. @RequestMapping("/login")
  3. @Slf4j
  4. public class LoginController {
  5. @Autowired
  6. private IUserService userService;
  7. /*
  8. * 跳转登录页面
  9. * */
  10. @RequestMapping("/toLogin")
  11. public String toLogin(){
  12. return "login";
  13. }
  14. /**
  15. * 登录
  16. * @return
  17. */
  18. @RequestMapping("/doLogin")
  19. @ResponseBody
  20. public RespBean doLogin(LoginVo loginVo) {
  21. log.info(loginVo.toString());
  22. return userService.login(loginVo);
  23. }
  24. }

导入编写好的静态资源

login.html

  1. <!DOCTYPE html>
  2. <html lang="en"
  3. xmlns:th="http://www.thymeleaf.org">
  4. <head>
  5. <meta charset="UTF-8">
  6. <title>登录</title>
  7. <!-- jquery -->
  8. <script type="text/javascript" th:src="@{/js/jquery.min.js}"></script>
  9. <!-- bootstrap -->
  10. <link rel="stylesheet" type="text/css"
  11. th:href="@{/bootstrap/css/bootstrap.min.css}"/>
  12. <script type="text/javascript" th:src="@{/bootstrap/js/bootstrap.min.js}">
  13. </script>
  14. <!-- jquery-validator -->
  15. <script type="text/javascript" th:src="@{/jquery-validation/jquery.validate.min.js}"></script>
  16. <script type="text/javascript" th:src="@{/jquery-validation/localization/messages_zh.min.js}"></script>
  17. <!-- layer -->
  18. <script type="text/javascript" th:src="@{/layer/layer.js}"></script>
  19. <!-- md5.js -->
  20. <script type="text/javascript" th:src="@{/js/md5.min.js}"></script>
  21. <!-- common.js -->
  22. <script type="text/javascript" th:src="@{/js/common.js}"></script>
  23. </head>
  24. <body>
  25. <form name="loginForm" id="loginForm" method="post" style="width:50%; margin:0
  26. auto">
  27. <h2 style="text-align:center; margin-bottom: 20px">用户登录</h2>
  28. <div class="form-group">
  29. <div class="row">
  30. <label class="form-label col-md-4">请输入手机号码</label>
  31. <div class="col-md-5">
  32. <input id="mobile" name="mobile" class="form-control"
  33. type="text" placeholder="手机号码" required="true"
  34. minlength="11" maxlength="11"/>
  35. </div>
  36. <div class="col-md-1">
  37. </div>
  38. </div>
  39. </div>
  40. <div class="form-group">
  41. <div class="row">
  42. <label class="form-label col-md-4">请输入密码</label>
  43. <div class="col-md-5">
  44. <input id="password" name="password" class="form-control"
  45. type="password" placeholder="密码"
  46. required="true" minlength="6" maxlength="16"/>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="row">
  51. <div class="col-md-5">
  52. <button class="btn btn-primary btn-block" type="reset"
  53. onclick="reset()">重置</button>
  54. </div>
  55. <div class="col-md-5">
  56. <button class="btn btn-primary btn-block" type="submit"
  57. onclick="login()">登录</button>
  58. </div>
  59. </div>
  60. </form>
  61. </body>
  62. <script>
  63. function login() {
  64. $("#loginForm").validate({
  65. submitHandler: function (form) {
  66. doLogin();
  67. }
  68. });
  69. }
  70. function doLogin() {
  71. g_showLoading();
  72. var inputPass = $("#password").val();
  73. var salt = g_passsword_salt;
  74. var str = "" + salt.charAt(0) + salt.charAt(2) + inputPass +
  75. salt.charAt(5) + salt.charAt(4);
  76. var password = md5(str);
  77. $.ajax({
  78. url: "/login/doLogin",
  79. type: "POST",
  80. data: {
  81. mobile: $("#mobile").val(),
  82. password: password
  83. },
  84. success: function (data) {
  85. layer.closeAll();
  86. if (data.code == 200) {
  87. layer.msg("成功");
  88. window.location.href="/goods/toList"
  89. } else {
  90. layer.msg(data.message);
  91. }
  92. },
  93. error: function () {
  94. layer.closeAll();
  95. }
  96. });
  97. }
  98. </script>
  99. </html>

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

添加公共返回对象

创建vo文件夹

创建RespBeanEnum.java

  1. @Getter
  2. @ToString
  3. @AllArgsConstructor
  4. public enum RespBeanEnum {
  5. //通用
  6. SUCCESS(200, "SUCCESS"),
  7. ERROR(500, "服务端异常"),
  8. //登录模块5002xx
  9. LOGIN_ERROR(500210, "用户名或密码不正确"),
  10. MOBILE_ERROR(500211, "手机号码格式不正确"),
  11. BIND_ERROR(500212, "参数校验异常"),
  12. MOBILE_NOT_EXIST(500213, "手机号码不存在"),
  13. PASSWORD_UPDATE_FAIL(500214, "密码更新失败"),
  14. SESSION_ERROR(500215, "用户不存在");
  15. private final Integer code;
  16. private final String message;
  17. }

创建RespBean.java

  1. @Data
  2. @NoArgsConstructor
  3. @AllArgsConstructor
  4. public class RespBean {
  5. private long code;
  6. private String message;
  7. private Object obj;
  8. /**
  9. * 功能描述: 成功返回结果
  10. */
  11. public static RespBean success(){
  12. return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBeanEnum.SUCCESS.getMessage(),null);
  13. }
  14. /**
  15. * 功能描述: 成功返回结果
  16. */
  17. public static RespBean success(Object obj){
  18. return new RespBean(RespBeanEnum.SUCCESS.getCode(),RespBean.success().getMessage(),obj);
  19. }
  20. /**
  21. * 功能描述: 失败返回结果
  22. */
  23. public static RespBean error(RespBeanEnum respBeanEnum){
  24. return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),null);
  25. }
  26. /**
  27. * 功能描述: 失败返回结果
  28. */
  29. public static RespBean error(RespBeanEnum respBeanEnum,Object obj){
  30. return new RespBean(respBeanEnum.getCode(),respBeanEnum.getMessage(),obj);
  31. }
  32. }

编写登录参数

LoginVo.java

  1. /**
  2. * 登录参数
  3. */
  4. @Data
  5. public class LoginVo {
  6. private String mobile;
  7. private String password;
  8. }

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

添加数据库信息:18012345678 123456

继续编写登录服务类

IUserService.java

  1. /**
  2. * 服务类
  3. */
  4. public interface IUserService extends IService<User> {
  5. /**
  6. * 登录
  7. * @param loginVo
  8. * @return
  9. */
  10. RespBean login(LoginVo loginVo);
  11. }

服务实现类

UserServiceImpl.java

  1. /**
  2. * 服务实现类
  3. */
  4. @Service
  5. public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements
  6. IUserService {
  7. @Autowired
  8. private UserMapper userMapper;
  9. /**
  10. * 登录
  11. * @param loginVo
  12. * @return
  13. */
  14. @Override
  15. public RespBean login(LoginVo loginVo) {
  16. String mobile = loginVo.getMobile();
  17. String password = loginVo.getPassword();
  18. if (StringUtils.isEmpty(mobile)||StringUtils.isEmpty(password)){
  19. return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
  20. }
  21. if (!ValidatorUtil.isMobile(mobile)){
  22. return RespBean.error(RespBeanEnum.MOBILE_ERROR);
  23. }
  24. //根据手机号获取用户
  25. User user = userMapper.selectById(mobile);
  26. if (null==user){
  27. return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
  28. }
  29. //校验密码
  30. if
  31. (!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
  32. return RespBean.error(RespBeanEnum.LOGINVO_ERROR);
  33. }
  34. return RespBean.success();
  35. }
  36. }

添加校验手机工具类

  1. /**
  2. * 手机号码校验
  3. */
  4. public class ValidatorUtil {
  5. private static final Pattern mobile_pattern = Pattern.compile("[1]([3-9])[0-9]{9}$");
  6. public static boolean isMobile(String mobile){
  7. if (StringUtils.isEmpty(mobile)){
  8. return false;
  9. }
  10. Matcher matcher = mobile_pattern.matcher(mobile);
  11. return matcher.matches();
  12. }
  13. }

运行类:

  1. @SpringBootApplication
  2. @MapperScan("com.xxxx.seckill.mapper")
  3. public class SeckillApplication {
  4. public static void main(String[] args) {
  5. SpringApplication.run(SeckillApplication.class, args);
  6. }
  7. }

测试登录功能

手机号码格式不正确

密码不正确

正确登录

4.2.参数校验

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

注入依赖

  1. <!-- validation组件 -->
  2. <dependency>
  3. <groupId>org.springframework.boot</groupId>
  4. <artifactId>spring-boot-starter-validation</artifactId>
  5. </dependency>

修改loginVo

  1. /**
  2. * 登录参数
  3. */
  4. @Data
  5. public class LoginVo {
  6. @NotNull
  7. @IsMobile
  8. private String mobile;
  9. @NotNull
  10. @Length(min = 32)
  11. private String password;
  12. }

自定义注解 IsMobile

创建新文件夹validation,

IsMobile.java

  1. /**
  2. * 验证手机号
  3. */
  4. @Target({ METHOD, FIELD, ANNOTATION_TYPE, CONSTRUCTOR, PARAMETER, TYPE_USE })
  5. @Retention(RUNTIME)
  6. @Documented
  7. @Constraint(validatedBy = {IsMobileValidator.class})
  8. public @interface IsMobile {
  9. boolean required() default true;
  10. String message() default "手机号码格式错误";
  11. Class<?>[] groups() default { };
  12. Class<? extends Payload>[] payload() default { };
  13. }

自定义手机号码验证规则

添加在vo中

IsMobileValidator.java

  1. /**
  2. * 手机号码校验规则
  3. */
  4. public class IsMobileValidator implements ConstraintValidator<IsMobile,String> {
  5. private boolean required = false;
  6. @Override
  7. public void initialize(IsMobile constraintAnnotation) {
  8. required = constraintAnnotation.required();
  9. }
  10. @Override
  11. public boolean isValid(String value, ConstraintValidatorContext context) {
  12. if (required){
  13. return ValidatorUtil.isMobile(value);
  14. }else {
  15. if (StringUtils.isEmpty(value)){
  16. return true;
  17. }else {
  18. return ValidatorUtil.isMobile(value);
  19. }
  20. }
  21. }
  22. }

将userServiceImp的这参数校验注释,

  1. // // //参数校验
  2. // if (StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
  3. // return RespBean.error(RespBeanEnum.LOGIN_ERROR);
  4. // }
  5. // if (!ValidatorUtil.isMobile(mobile)) {
  6. // return RespBean.error(RespBeanEnum.MOBILE_ERROR);
  7. // }

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

解决:

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

  1. @RequestMapping("/doLogin")
  2. @ResponseBody
  3. public RespBean doLogin(@Valid LoginVo loginVo) {
  4. log.info(loginVo.toString());
  5. return userService.login(loginVo);
  6. }

测试:

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

  1. /**
  2. * 全局异常
  3. */
  4. @Data
  5. @NoArgsConstructor
  6. @AllArgsConstructor
  7. public class GlobalException extends RuntimeException {
  8. private RespBeanEnum respBeanEnum;
  9. }

GlobalExceptionHandler.java

  1. /**
  2. * 全局异常处理类
  3. */
  4. @RestControllerAdvice
  5. public class GlobalExceptionHandler {
  6. @ExceptionHandler(Exception.class)
  7. public RespBean ExceptionHandler(Exception e) {
  8. if (e instanceof GlobalException) {
  9. GlobalException ex = (GlobalException) e;
  10. return RespBean.error(ex.getRespBeanEnum());
  11. } else if (e instanceof BindException) {
  12. BindException ex = (BindException) e;
  13. RespBean respBean = RespBean.error(RespBeanEnum.BIND_ERROR);
  14. respBean.setMessage("参数校验异常:" + ex.getBindingResult().getAllErrors().get(0).getDefaultMessage());
  15. return respBean;
  16. }
  17. return RespBean.error(RespBeanEnum.ERROR);
  18. }
  19. }

在RespBeanNum添加异常信息

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

  1. /**
  2. * 登录
  3. * @param loginVo
  4. * @return
  5. */
  6. @Override
  7. public RespBean login(LoginVo loginVo) {
  8. String mobile = loginVo.getMobile();
  9. String password = loginVo.getPassword();
  10. //根据手机号获取用户
  11. User user = userMapper.selectById(mobile);
  12. if (null==user){
  13. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  14. }
  15. //校验密码
  16. if
  17. (!MD5Util.formPassToDBPass(password,user.getSalt()).equals(user.getPassword())){
  18. throw new GlobalException(RespBeanEnum.LOGIN_ERROR);
  19. }
  20. return RespBean.success();
  21. }

测试

4.4.分布式session

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

4.4.1.完善登录功能

使用cookie+session记录用户信息

准备工具类

CookieUtil.java

  1. /**
  2. * Cookie工具类
  3. */
  4. public final class CookieUtil {
  5. /**
  6. * 得到Cookie的值, 不编码
  7. *
  8. * @param request
  9. * @param cookieName
  10. * @return
  11. */
  12. public static String getCookieValue(HttpServletRequest request, String cookieName) {
  13. return getCookieValue(request, cookieName, false);
  14. }
  15. /**
  16. * 得到Cookie的值,
  17. *
  18. * @param request
  19. * @param cookieName
  20. * @return
  21. */
  22. public static String getCookieValue(HttpServletRequest request, String cookieName, boolean isDecoder) {
  23. Cookie[] cookieList = request.getCookies();
  24. if (cookieList == null || cookieName == null) {
  25. return null;
  26. }
  27. String retValue = null;
  28. try {
  29. for (int i = 0; i < cookieList.length; i++) {
  30. if (cookieList[i].getName().equals(cookieName)) {
  31. if (isDecoder) {
  32. retValue = URLDecoder.decode(cookieList[i].getValue(), "UTF-8");
  33. } else {
  34. retValue = cookieList[i].getValue();
  35. }
  36. break;
  37. }
  38. }
  39. } catch (UnsupportedEncodingException e) {
  40. e.printStackTrace();
  41. }
  42. return retValue;
  43. }
  44. /**
  45. * 得到Cookie的值,
  46. *
  47. * @param request
  48. * @param cookieName
  49. * @return
  50. */
  51. public static String getCookieValue(HttpServletRequest request, String cookieName, String encodeString) {
  52. Cookie[] cookieList = request.getCookies();
  53. if (cookieList == null || cookieName == null) {
  54. return null;
  55. }
  56. String retValue = null;
  57. try {
  58. for (int i = 0; i < cookieList.length; i++) {
  59. if (cookieList[i].getName().equals(cookieName)) {
  60. retValue = URLDecoder.decode(cookieList[i].getValue(), encodeString);
  61. break;
  62. }
  63. }
  64. } catch (UnsupportedEncodingException e) {
  65. e.printStackTrace();
  66. }
  67. return retValue;
  68. }
  69. /**
  70. * 设置Cookie的值 不设置生效时间默认浏览器关闭即失效,也不编码
  71. */
  72. public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
  73. String cookieValue) {
  74. setCookie(request, response, cookieName, cookieValue, -1);
  75. }
  76. /**
  77. * 设置Cookie的值 在指定时间内生效,但不编码
  78. */
  79. public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
  80. String cookieValue, int cookieMaxage) {
  81. setCookie(request, response, cookieName, cookieValue, cookieMaxage, false);
  82. }
  83. /**
  84. * 设置Cookie的值 不设置生效时间,但编码
  85. */
  86. public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
  87. String cookieValue, boolean isEncode) {
  88. setCookie(request, response, cookieName, cookieValue, -1, isEncode);
  89. }
  90. /**
  91. * 设置Cookie的值 在指定时间内生效, 编码参数
  92. */
  93. public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
  94. String cookieValue, int cookieMaxage, boolean isEncode) {
  95. doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, isEncode);
  96. }
  97. /**
  98. * 设置Cookie的值 在指定时间内生效, 编码参数(指定编码)
  99. */
  100. public static void setCookie(HttpServletRequest request, HttpServletResponse response, String cookieName,
  101. String cookieValue, int cookieMaxage, String encodeString) {
  102. doSetCookie(request, response, cookieName, cookieValue, cookieMaxage, encodeString);
  103. }
  104. /**
  105. * 删除Cookie带cookie域名
  106. */
  107. public static void deleteCookie(HttpServletRequest request, HttpServletResponse response,
  108. String cookieName) {
  109. doSetCookie(request, response, cookieName, "", -1, false);
  110. }
  111. /**
  112. * 设置Cookie的值,并使其在指定时间内生效
  113. *
  114. * @param cookieMaxage cookie生效的最大秒数
  115. */
  116. private static final void doSetCookie(HttpServletRequest request, HttpServletResponse response,
  117. String cookieName, String cookieValue, int cookieMaxage, boolean isEncode) {
  118. try {
  119. if (cookieValue == null) {
  120. cookieValue = "";
  121. } else if (isEncode) {
  122. cookieValue = URLEncoder.encode(cookieValue, "utf-8");
  123. }
  124. Cookie cookie = new Cookie(cookieName, cookieValue);
  125. if (cookieMaxage > 0)
  126. cookie.setMaxAge(cookieMaxage);
  127. if (null != request) {// 设置域名的cookie
  128. String domainName = getDomainName(request);
  129. System.out.println(domainName);
  130. if (!"localhost".equals(domainName)) {
  131. cookie.setDomain(domainName);
  132. }
  133. }
  134. cookie.setPath("/");
  135. response.addCookie(cookie);
  136. } catch (Exception e) {
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/人工智能uu/article/detail/807305
推荐阅读
相关标签
  

闽ICP备14008679号