1.学习目标
![](https://i-blog.csdnimg.cn/blog_migrate/57dbddcb0cdd325f37119b47d2945fa8.png)
![](https://i-blog.csdnimg.cn/blog_migrate/47037599432d5df33555fcb70cd8ba0e.png)
![](https://i-blog.csdnimg.cn/blog_migrate/dc8bd13f7bd8b5562b796a77d20b390d.png)
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.创建基本目录
![](https://i-blog.csdnimg.cn/blog_migrate/e7e73271a11ca830b7f227e8eafe0976.png)
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();
}
}
导入模板
![](https://i-blog.csdnimg.cn/blog_migrate/698e9d82f423be4500fca082a51d38c4.png)
复制到
![](https://i-blog.csdnimg.cn/blog_migrate/d5fc394287533e6954a583b9db487b07.png)
运行后,将得到的文件夹复制回我们的秒杀项目中
登录功能编写
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);
}
}
导入编写好的静态资源
![](https://i-blog.csdnimg.cn/blog_migrate/269dea512568a5b741d56bd60e8f4e10.png)
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);
}
}
测试登录功能
手机号码格式不正确
![](https://i-blog.csdnimg.cn/blog_migrate/f2f27e12dd79ddcca5238a52e0f04c20.png)
密码不正确
![](https://i-blog.csdnimg.cn/blog_migrate/07548e79b87cdeeff4f85e7b1674e201.png)
正确登录
![](https://i-blog.csdnimg.cn/blog_migrate/6bc356886c3f565db0b5468a8daa91ee.png)
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);
}
测试:
![](https://i-blog.csdnimg.cn/blog_migrate/e1e08ff95bf3632fd6d4c0a62dd88a39.png)
4.3.异常处理
我们知道,系统中异常包括:编译时异常和运行时异常 RuntimeException ,前者通过捕获异常从而获 取异常信息,后者主要通过规范代码开发、测试通过手段减少运行时异常的发生。在开发中,不管是 dao层、service层还是controller层,都有可能抛出异常,在Springmvc中,能将所有类型的异常处理 从各处理过程解耦出来,既保证了相关处理过程的功能较单一,也实现了异常信息的统一处理和维护。 SpringBoot全局异常处理方式主要两种:
使用 @ControllerAdvice 和 @ExceptionHandler 注解。
使用 ErrorController类 来实现
区别:
@ControllerAdvice 方式只能处理控制器抛出的异常。此时请求已经进入控制器中。
ErrorController类 方式可以处理所有的异常,包括未进入控制器的错误,比如404,401等错误
如果应用中两者共同存在,则 @ControllerAdvice 方式处理控制器抛出的异常, ErrorController类 方式处理未进入控制器的异常
@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();
}
测试
![](https://i-blog.csdnimg.cn/blog_migrate/a4bd984890f0e39ffe3638b6396357e3.png)
![](https://i-blog.csdnimg.cn/blog_migrate/4eee1341861087260c160467d138ac7a.png)
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) {