统一处理异常:
思考:如何解决传统的事务处理的重复代码问题?
小知识点:SpringAOP是什么,SpringAOP的底层原理,@ControllerAdvice,@ExceptionHandler
SpringAop就是可以在不改动原有代码的基础上,对原有代码添加功能
SpringAOP的底层原理:其实就是动态代理,通过创建代理类帮我们调用真实类中具体的方法
@ControllerAdvice:贴在类头上,声明该类为Controller增强类
@ExceptionHandler:贴在方法上,声明只处理指定类型的异常
复习动态代理伪代码:
public class $proxy implements UserInfoServiceImpl{
public void save (...){
try{
begin();
super.save();//真实类对象的save方法
commit();
}catch(Exception e){
rollback();
}
}
}
但是这里的统一异常处理并不是使用动态代理完成的,只是通过Controller增强的方法来完成,可以理解为类似的思想
步骤:
1.自定义异常类LogicException
public class LogicException extends RuntimeException{
public LogicException(String message) {
super(message);
}
}
2.自定义断言工具类
用于断言时抛出自定义的异常类型
public class AssertUtils {
//私有化构造器
private AssertUtils(){}
/**
* 判断参数是否为空
* @param value
* @param message
*/
public static void isNull(String value, String message) {
if (!StringUtils.hasText(value)) {
throw new LogicException(message);
}
}
/**
* 判断两个参数值是否相等
* @param value
* @param value2
* @param message
*/
public static void isEqual(String value, String value2, String message) {
if (value == null || value2==null){
throw new RuntimeException("比较的参数不能为空");
}
if (!value.equals(value2)) {
throw new LogicException(message);
}
}
}
3.创建一个Controller的增强类
/**
* 通用异常处理类
* ControllerAdvice controller类功能增强注解, 动态代理controller类实现一些额外功能
*
* 请求进入controller映射方法之前做功能增强: 经典用法:日期格式化
* 请求进入controller映射方法之后做功能增强: 经典用法:统一异常处理
*/
@ControllerAdvice
public class CommonExceptionHandler {
//这个方法定义的跟映射方法操作一样
@ExceptionHandler(LogicException.class)
@ResponseBody
public Object logicExp(Exception e, HttpServletResponse resp) {
e.printStackTrace();
resp.setContentType("application/json;charset=utf-8");
return JsonResult.error(JsonResult.CODE_ERROR_PARAM, e.getMessage(), null);
}
@ExceptionHandler(RuntimeException.class)
@ResponseBody
public Object runTimeExp(Exception e, HttpServletResponse resp) {
e.printStackTrace();
resp.setContentType("application/json;charset=utf-8");
return JsonResult.defaultError();
}
}
4.业务层登录逻辑
小知识点:Mybatis-plus的使用
queryWrapper.eq(“phone”,username) : 等价于SQL中的 where phone = #{username}
userInfoService.getOne() :查询数据库获取唯一的一条数据,如果超过一条就报错
使用自定义的断言工具类AssertUtils做一些参数的校验
@Override
public UserInfo login(String username, String password) {
//对请求参数做相应的校验
AssertUtils.isNull(username,"用户名不能为空");
AssertUtils.isNull(password,"密码不能为空");
//都不空,查询数据库获取UserInfo对象
QueryWrapper<UserInfo> wrapper = new QueryWrapper<>();
wrapper.eq("phone",username);
UserInfo userInfo = super.getOne(wrapper);
//判断用户是否已经注册,否则未注册
if (userInfo==null){
throw new LogicException("用户不存在");
}
//如果UserInfo不为空,校验密码是否正确,否则抛异常,提示密码错误
AssertUtils.isEqual(userInfo.getPassword(),password,"账号或密码错误");
return userInfo;
}
5.控制器中调用业务层的login方法
@RestController
@RequestMapping("users")
public class UserInfoController {
@Autowired
private IUserInfoRedisService userInfoRedisService;
@Autowired
private IUserInfoService userInfoService;
@PostMapping("/login")
public Object login(String username, String password){
//执行登录逻辑
UserInfo userInfo = userInfoService.login(username, password);
//登录成功,生成token,并以token为key,UserInfo对象为value存到redis中,返回token
String token = userInfoRedisService.createToken(userInfo);
//创建map存放token和UserInfo对象,用于返回给前端
Map<String,Object>map = new HashMap<>();
map.put("token",token);
map.put("user",userInfo);
return JsonResult.success(map);
}
}
使用枚举约定redis中的key 和 有效时长timeout
在使用redis缓存注册验证码/登录token的时候,每次拼接写死的key,写死的有效时间,为了提高代码的可读性和可维护性,我们可以使用枚举的方式来完成key和时间的定义,并且当枚举类定义完成之后,就约定好了key的拼接方式
复习枚举类的特点:
1.枚举类的构造器是私有的
2.枚举类定义完之后,枚举类的实例个数就固定了
3.剩下的所有操作跟普通类一样
枚举类代码
@Getter
public enum RedisUtil {
//用户登录令牌
USER_LOGIN_TOKEN(Consts.LOGIN_TOKEN,Consts.USER_INFO_TOKEN_VAI_TIME * 60L),
//注册验证码
REGIST_VERIFY_CODE(Consts.VERIFY_CODE,Consts.VERIFY_CODE_VAI_TIME * 60L);
@Setter
private String prefix;//前缀
@Setter
private Long timeout;//有效时长
private RedisUtil(String prefix,Long timeout) {
this.prefix = prefix;
this.timeout= timeout;
}
/**
*
* @param value 需要拼接的参数集
* @return 拼接后的字符串
*/
public String join(String... value){
StringBuilder sb = new StringBuilder();
System.out.println(prefix);
sb.append(this.prefix);
for (String s : value) {
sb.append(":").append(s);
}
return sb.toString();
}
}
注册细节
注册流程文字描述
1.客户端发送注册请求(phone)
2.后端查询数据库,获取用户信息对象UserInfo,判断UserInfo是否为空
3.不为空跳转到填写用户信息的页面,用户需要获取注册验证码(phone)
<调用第三方Api帮我们完成短信的发送
<发送成功后,把phone作为key,把验证码作为value缓存到redis中
<响应告诉前端短信发送成功
4.填写用户信息以及注册验证码后,发送完成注册请求(phone,nickname,password,rpassword)
5.对参数的做校验(判空,验证码校验),判断是否所有请求参数都合法了
6.验证码验证成功,执行用户注册逻辑save(UserInfo)
发送短信
如何使用java代码发送http请求:
直接使用Spring给我们提供了一个发送Http请求的工具RestTemplate就好了,它提供了RESTful请求方案的模版,如GET,POST,PUT,DELETE等
使用前提:引入依赖spring-boot-starter-web,该启动器中集成了RestTemplate
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
上代码:
@Override
public void sendVerifyCode(String phone) {
//生成验证码
String code = RandomUtil.randomNumbers(4);
//使用Spring给我们提供了一个发送Http请求的工具RestTemplate
RestTemplate template = new RestTemplate();
//这里的appkey使用自己的appkey就好了
String result = template.getForObject(url, String.class, phone, code, appkey);
System.out.println(url);
System.out.println(appkey);
System.out.println(result);
if (!result.contains("Success")){
throw new LogicException("短信发送失败");
}
System.out.println("短信发送成功");
//把验证码缓存到redis中,用于后期验证码校验
userInfoRedisService.set(phone,code);
}
这里有个小细节,如果url和appkey想要配置到properties文件中,那么如果有中文必须先把中文转换成Unicode才行,否则会报错
登录细节
复习session操作原理:
1.用户登录成功后,把用户信息保存到session对象中
2.后端创建cookie对象,把session对象Id存到cookie对象中
3.然后后端把cookie对象放到response中响应给客户端
4.当客户端再次向后端发送请求时,浏览器会取出cookie中的session对象Id并放到请求体中(request)
5.后端收到请求,获取请求体中的session对象Id,去Tomcat中查找该session对象Id对应的session对象
6.获取session中的用户信息对象,最后判断该用户对象是否为空
使用redis解决部分客户端不支持session的问题
小知识点:
生成token方式:UUID / Hutool
**json字符串转Java对象:**Hutool => JSONUtil.toBean(resultStr,UserInfo.class)
前后端分离项目,部分客户端有可能不支持session,所以采用了互联网的登录方式:令牌登录方式,把登录信息对象缓存到redis中,通过工具类生成一个32位的token作为key,UserInfo对象作为value缓存到redis中。
登录流程文字描述
1.客户端发送登录请求(username,password)
2.对请求参数做响应的校验(判空,用户是否存在 …)
3.调用login方法查询数据库,获得UserInfo对象,判断该对象是否为空
4.该对象不为空,判断密码是否正确,都满足则登录成功
5.登录成功后,以UUID生成的token作为key,把UserInfo对象最为value缓存到redis中(时效30分钟)
6.以Json格式返回token和UserInfo对象给前端
7.前端解析json数据,把接收到的token和UserInfo对象存到cookie中(时效30分钟)
8.客户端去访问其他接口的时候,把token带上,(需要经过登录认证后才能访问)
9.客户端发送请求,后端获取请求头中的token,token不为空时,再通过token获取userInfo对象
10.判断UserInfo对象是否为空,为空:证明还没登录过,不为空:证明登陆过,同时重置token的有效时间为30分钟
登录流程图
4.该对象不为空,判断密码是否正确,都满足则登录成功
5.登录成功后,以UUID生成的token作为key,把UserInfo对象最为value缓存到redis中(时效30分钟)
6.以Json格式返回token和UserInfo对象给前端
7.前端解析json数据,把接收到的token和UserInfo对象存到cookie中(时效30分钟)
8.客户端去访问其他接口的时候,把token带上,(需要经过登录认证后才能访问)
9.客户端发送请求,后端获取请求头中的token,token不为空时,再通过token获取userInfo对象
10.判断UserInfo对象是否为空,为空:证明还没登录过,不为空:证明登陆过,同时重置token的有效时间为30分钟
登录流程图