黑马点评(一)--短信登录

1.基于token的验证码登录功能

1.1发送验证码

1.1.1Controller
    /**
     * 发送手机验证码
     */
    @PostMapping("code")
    public Result sendCode(@RequestParam("phone") String phone, HttpSession session) {
        // 发送短信验证码并保存验证码
        return userService.sendCode(phone);
    }
1.1.2Service
 /**
     *  生成验证码
     *
     * @author  lichuancheng
     * @date 创建时间 2024-04-17
     * @since V1.0
     */
    @Override
    public Result sendCode(String phone) {
        // 1.验证手机号码格式
        if (RegexUtils.isPhoneInvalid(phone)) {
            // 如果不符合,返回错误信息
            return Result.fail("手机号格式错误!");
        }
        // 2.生成验证码
        String code = RandomUtil.randomNumbers(6);
        // 3.验证码存redis
        stringRedisTemplate.opsForValue().set(LOGIN_CODE_KEY+phone,code);

        // 4.设置验证码有效期
        stringRedisTemplate.expire(LOGIN_CODE_KEY+phone,LOGIN_CODE_TTL, TimeUnit.MINUTES);

        //  TODO 5.发送验证码

        return Result.ok("发送成功!");
    }

1.2登录

1.2.1Controller
    /**
     * 登录功能
     *
     * @param loginForm 登录参数,包含手机号、验证码;或者手机号、密码
     */
    @PostMapping("/login")
    public Result login(@RequestBody LoginFormDTO loginForm, HttpSession session) {
        // 登录功能
        return userService.login(loginForm);
    }
1.2.2Service
/**
     *  登录
     *
     * @author  lichuancheng
     * @date 创建时间 2024-04-17
     * @since V1.0
     */
    @Override
    public Result login(LoginFormDTO loginForm) {
        String phone = loginForm.getPhone();
        String code = loginForm.getCode();

        // 1、检验手机号码格式
        if (RegexUtils.isPhoneInvalid(phone)) {
            return Result.fail("手机号格式错误!");
        }

        if (StringUtil.isNullOrEmpty(code)) {
            return Result.fail("验证码为空");
        }

        // 2、检验验证码
        String redisCode = stringRedisTemplate.opsForValue().get(LOGIN_CODE_KEY + phone);

        if (!code.equals(redisCode)) {
            return Result.fail("验证码不正确");
        }

        // 3、查询数据库中用户
        User user = query().eq("phone", phone).one();

        // 3、1判断用户是否存在
        if (user == null) {
            // 不存在创建用户
            user = createUserWithPhone(phone);
        }

        // 4、颁发登录凭证存入redis
        String token = UUID.randomUUID().toString();
        UserDTO userDTO = BeanUtil.copyProperties(user, UserDTO.class);
        Map<String, Object> userMap = BeanUtil.beanToMap(userDTO, new HashMap<>(),
                CopyOptions.create()
                        .setIgnoreNullValue(true)
                        .setFieldValueEditor((fieldName, fieldValue) -> fieldValue.toString()));
         stringRedisTemplate.opsForHash().putAll(LOGIN_USER_KEY+token,userMap);

        // 5、设置登录凭证有效期
        stringRedisTemplate.expire(LOGIN_USER_KEY+token,30,TimeUnit.MINUTES);

        // 6、返回token
        return Result.ok(token);
    }

1.3有用知识点

TODO 具体的验证码发送功能未实现,目前只能通过redis客户查看发送的验证码

2.双层拦截器刷新登录凭证并拦截登录请求

2.1第一层刷新登录凭证拦截器

public class RefreshTokenInterceptor implements HandlerInterceptor {
    StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    };

    @Override
    public boolean preHandle(HttpServl etRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、获取token
        String token = request.getHeader("authorization");
        // 2、redis中获取登录凭证
        Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3、登录凭证不存在直接放行不进行下列步骤
        if (userMap == null) {
            return true;
        }
        // 4、登录凭证存在刷新登录凭证
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);

        // 5、登录凭证存在则保存用户信息进入threadlocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 线程结束后销毁线程变量,防止内存泄露
        UserHolder.removeUser();
    }
}

2.2第二层拦截登录请求拦截器

public class RefreshTokenInterceptor implements HandlerInterceptor {
    StringRedisTemplate stringRedisTemplate;

    public RefreshTokenInterceptor(StringRedisTemplate stringRedisTemplate){
        this.stringRedisTemplate = stringRedisTemplate;
    };

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1、获取token
        String token = request.getHeader("authorization");
        // 2、redis中获取登录凭证
        Map<Object,Object> userMap = stringRedisTemplate.opsForHash().entries(LOGIN_USER_KEY + token);
        // 3、登录凭证不存在直接放行不进行下列步骤
        if (userMap == null) {
            return true;
        }
        // 4、登录凭证存在刷新登录凭证
        stringRedisTemplate.expire(LOGIN_USER_KEY + token,30, TimeUnit.MINUTES);

        // 5、登录凭证存在则保存用户信息进入threadlocal
        UserDTO userDTO = BeanUtil.fillBeanWithMap(userMap, new UserDTO(), false);
        UserHolder.saveUser(userDTO);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 线程结束后销毁线程变量,防止内存泄露
        UserHolder.removeUser();
    }
}

2.3注册拦截器

@Configuration
public class MvcConfig implements WebMvcConfigurer {

    @Resource
    StringRedisTemplate stringRedisTemplate;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册登录凭证刷新拦截器
        registry.addInterceptor(new RefreshTokenInterceptor(stringRedisTemplate))
                .addPathPatterns("/**").order(0);

        // 注册需登录功能拦截器
        registry.addInterceptor(new LoginInterceptor(stringRedisTemplate))
                .addPathPatterns(
                "/shop/**",
                "/voucher/**",
                "/shop-type/**",
                "/upload/**",
                "/blog/hot",
                "/user/code",
                "/user/login"
                ).order(1);
    }
}

2.4有用知识点

拦截器
1、实现HandlerInterceptor接口,重写preHandler、posthandler、afterCompletion方法,要明白这三个方法都是在哪个阶段执行的。preHandler在Controller方法运行之前执行;postHandler在Controller方法运行之后执行;afterCompletion在请求线程结束(页面渲染)之后执行
2、实现WebMvcConfigurer接口,重新addIntercepteors方法,调用addinterceptor方法添加一条拦截器,其中可以设置或排除拦截的路径,除此之外还可以设置拦截器的优先级

3.线程变量存储用户信息

3.1封装ThreadLocal工具类

线程变量的应用在上面的刷新登录凭证拦截器里面已经使用过,接下来展示一下线程变量工具类的内部实现:

public class UserHolder {
    private static final ThreadLocal<UserDTO> tl = new ThreadLocal<>();

    public static void saveUser(UserDTO user){
        tl.set(user);
    }

    public static UserDTO getUser(){
        return tl.get();
    }

    public static void removeUser(){
        tl.remove();
    }
}

3.2有用知识点

要理解threadlocal原理,明白内存泄漏的原因
源码解读:
threadlocal我们一般都是封装在工具类里面用,在UserHolder工具类中,我们可以看到new了一个ThreadLocal线程变量,并用static、final修饰,也就是说这个线程局部变量会随着程序启动就被加载,且只被加载一次不能被修改。
1.set方法解读:

   //Threadlocal类的方法
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

   //getMap(t)
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    //t.threadLocals
    ThreadLocal.ThreadLocalMap threadLocals = null;

从源码上可以看出,用threadlocal存线程变量的时候首先获取当前线程对象,接着获取当前线程对象的ThreadLocalMap ,然后用当前的ThreadLocal对象当做key,自己传入的值当value来保存信息。如果Threadlocal对象获取不到当前线程的ThreadLocalMap,则在当前线程上创建一个ThreadLocalMap ,然后用当前线程对象当做key,传入的值当做value来保存信息。
2.get方法解读

    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

从源码上可以看出,threadlocal的get方法首先会获取当前的线程对象,然后通过线程对象获取ThreadLocalMap。如果获取到的ThreadLocalMap对象不为空,则以当前的threadlocal实例对象为key,获取里面的存储的值;如果获取的ThreadLocalMap对象为空,则初始化一个ThreadLocalMap,并将初始化的value返回
3.threadlocal结构图
threadlocal结构图

讨论
1、如果在一次请求中,通过线程变量第一次存储的值和第二次存储的值不一样,会出现什么情况?以第一次存储的为准,还是使用后来存储的值,又或者是两者共存呢?
答:如果一次请求中,只是单线程,则threadlocal线程局部变量可以存储多次值,但是每次都以最后存储的值为准。
2、了解了threadlocal存储变量的原理后,思考为什么会出现内存泄漏现象?
答:
在这里插入图片描述
首先明白两个概念:一是当一个类没有线程去调用时,类就会被销毁,包括类的实例对象,类的静态变量、方法、代码块等;二是弱引用,当一个堆对象没有强引用指向他时,弱引用就被会被回收。

结合以上两点加线程池就会造成内存泄漏。
我们的线程不会被频繁的创建和销毁,而是由线程池管控,因此线程一旦被创建,则线程对应的ThreadLocalMap就不会被回收(一直有线程对象的强引用引用),ThreadLocalMap内部是key和value,key是指向ThreadLocal的弱引用,因此当ThreadLocal所在的类没有被线程使用就会被销毁,随之ThreadLocal的强引用回收,因此弱引用也回收,此时map中只剩下一个没有key的value,这个value会随着ThreadLocalMap继续存在,但是又没有key,因此也不能调用map的remove删除,长此以往累积就会把内存干爆。

另外,如若不使用线程池,线程频繁的创建和销毁,则不会出现内存泄漏的现象,线程销毁后线程所创建的ThreadLocalMap也会销毁。还有一种情况就是,不等threadlocal对象的强引用被销毁,接着将他复制一份当做弱引用存入ThreadLocalMap当做key,直接覆盖上一次的数据,只要不间断,则也不会出现内存泄漏的情况。

  • 6
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

java登云楼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值