统一处理异常:

思考:如何解决传统的事务处理的重复代码问题?

小知识点: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分钟

登录流程图

springboot自动注册到工厂_spring boot