博客项目技术总结

209 阅读13分钟

1.AOP日志

spring aop的介绍 www.cnblogs.com/joy99/p/109…

AOP 领域中的特性术语:

  • 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增
  • 切点(PointCut): 可以插入增强处理的连接点。
  • 切面(Aspect): 切面是通知和切点的结合。

总结:

首先我们定义一个自己的注解,注解包括模块名和方法名(用于后续反射获取,记录日志)。然后把注解用到需要记录日志得方法,填写注解的两个参数。然后新建一个类,开始编写切面,需要使用@Aspect,定义切点,这里的切点就是加注解的方法。开启环绕通知,编写记录日志代码。

代码:

首先我们找到需要加日志的方法,并且自定义一个注解。@LogAnnotation

@PostMapping
    //加上此注解,代表要对此接口记录日志
    @LogAnnotation(module = "文章",operation = "获取文章列表")
    @Cache(expire = 5 * 60 * 1000,name = "listArticle")
    public Result listArticle(@RequestBody PageParams pageParams){

        return articleService.listArticle(pageParams);
    }

下面是自定义注解

package com.mszlu.blog.common.aop;

import java.lang.annotation.*;

/**
 * 日志注解
 */
 //ElementType.TYPE代表可以放在类上面  method代表可以放在方法上
@Target(ElementType.METHOD)
//表示注解的信息被保留在class文件 (字节码文件)中当程序编译时,会被虚拟机保留在运行时
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface LogAnnotation {
    //模块名称
    String module() default "";
    //方法名称
    String operation() default "";
}

光有这个注解肯定是不行的。我们是面向切面编程,所以需要@Aspect注解

@Component
@Slf4j
/**
 * 切面,定义了通知和切点的关系
 */
@Aspect
public class LogAspect {
    /**
     * 切点:就是注解加哪,哪个就是切点
     */
    @Pointcut("@annotation(com.cjh.blog.common.aop.LogAnnotation)")
    public void pt(){

    }

    /**
     * 环绕通知
     * @param joinPoint
     * @return
     */
    @Around("pt()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable {
        //执行方法之前
        long beginTime = System.currentTimeMillis();
        //执行方法
        Object result = joinPoint.proceed();
        //执行方法之后
        //执行时长(毫秒)
        long time = System.currentTimeMillis() - beginTime;
        //保存日志
        recordLog(joinPoint, time);
        return result;
    }

    //记录日志
    private void recordLog(ProceedingJoinPoint joinPoint, long time) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        LogAnnotation logAnnotation = method.getAnnotation(LogAnnotation.class);
        log.info("=====================log start================================");
        log.info("模块module:{}",logAnnotation.module());
        log.info("功能operation:{}",logAnnotation.operation());

        //请求的方法名
        String className = joinPoint.getTarget().getClass().getName();
        String methodName = signature.getName();
        log.info("请求方法request method:{}",className + "." + methodName + "()");

        //请求的参数
        Object[] args = joinPoint.getArgs();
        String params = JSON.toJSONString(args[0]);
        log.info("请求参数params:{}",params);

        //获取request 设置IP地址
        HttpServletRequest request = HttpContextUtils.getHttpServletRequest();
        log.info("ip:{}", IpUtils.getIpAddr(request));


        log.info("excute time : {} ms",time);
        log.info("=====================log end================================");
    }

}

为什么用@annotation做切点

我们的切点是用execution表达式来配置的,这种方式有一些局限性在里面:

  • 灵活性不高,一个表达式只能切到某种同类型的方法
  • 个性化不足,很难对切面切到的所有方法中的一些做一些个性化的设置

今天我们就来讲讲切点的另一种配置方式: @annotation,通过@annotation配置切点,我们可以灵活的控制切到哪个方法,同时可以进行一些个性化的设置,今天我们就用它来实现一个记录所有接口请求功能吧。

package com.sharpcj.aopdemo.test1;

import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.*; import org.springframework.stereotype.Component;

@Aspect @Component public class BuyAspectJ {

@Pointcut("execution(* com.sharpcj.aopdemo.test1.IBuy.buy(..))")
public void point(){}

@Before("point()")
public void hehe() {
    System.out.println("before ...");
}

@After("point()")
public void haha() {
    System.out.println("After ...");
}

@AfterReturning("point()")
public void xixi() {
    System.out.println("AfterReturning ...");
}

@Around("point()")
public void xxx(ProceedingJoinPoint pj) {
    try {
        System.out.println("Around aaa ...");
        pj.proceed();
        System.out.println("Around bbb ...");
    } catch (Throwable throwable) {
        throwable.printStackTrace();
    }
}

}

结果

image.png

2.AOP缓存

和日志差不多的过程

先定义一个注解

package com.cjh.blog.common.cache;

import java.lang.annotation.*;

/**
 * @author 86187
 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Cache {

    //缓存的过期时间,默认1分钟
    long expire() default 1 * 60 * 1000;

    //缓存标识
    String name() default "";

}
   @PostMapping("hot")
    @Cache(expire = 5 * 60 * 1000,name = "hot_article")
    public Result hotArticle(){
        int limit = 5;
        return articleService.hotArticle(limit);
    }

缓存的key

String redisKey = name + "::" + className+"::"+methodName+"::"+params;

然后通过key拿到value

String redisValue = redisTemplate.opsForValue().get(redisKey);

第一次读文章value肯定是null所以不会走缓存,会先走数据库

Object proceed = pjp.proceed();

然后在存缓存

//然后我们把方法返回的结果,存到缓存中,并设置过期时间,通过注解获取的
redisTemplate.opsForValue().set
(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));

第二次阅读就能走缓存了

package com.cjh.blog.common.cache;

import com.alibaba.fastjson.JSON;
import com.cjh.blog.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AliasFor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.Duration;
import java.util.Arrays;

@Aspect
@Component
@Slf4j
public class CacheAspect {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Pointcut("@annotation(com.cjh.blog.common.cache.Cache)")
    public void pt(){}

    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp){
        try {
            //这里以方法hot文章为例子
            Signature signature = pjp.getSignature();
            //ArticleController类名
            String className = pjp.getTarget().getClass().getSimpleName();
            //调用的方法名hotArticle
            String methodName = signature.getName();

            //拿到对应参数的类
            Class[] parameterTypes = new Class[pjp.getArgs().length];
            //拿到参数
            Object[] args = pjp.getArgs();
            //参数
            String params = "";
            for(int i=0; i<args.length; i++) {
                if(args[i] != null) {
                    //参数转成json,因为参数有可能是个类,便于显示
                    params += JSON.toJSONString(args[i]);
                    //这个获取的类主要是用来拿到对应的方法
                    parameterTypes[i] = args[i].getClass();
                }else {
                    parameterTypes[i] = null;
                }
            }
            if (StringUtils.isNotEmpty(params)) {
                //加密 以防出现key过长以及字符转义获取不到的情况。用于redis key的一部分
                params = DigestUtils.md5Hex(params);
            }
            Method method = pjp.getSignature().getDeclaringType().getMethod(methodName, parameterTypes);
            //拿到对应方法之后,就能获取对应方法的Cache注解
            Cache annotation = method.getAnnotation(Cache.class);
            //缓存过期时间
            long expire = annotation.expire();
            //缓存名称
            String name = annotation.name();
            //先从redis获取
            String redisKey = name + "::" + className+"::"+methodName+"::"+params;
            //通过key拿到value
            String redisValue = redisTemplate.opsForValue().get(redisKey);
            //成功拿到value,说明走了缓存
            if (StringUtils.isNotEmpty(redisValue)){
                log.info("走了缓存~~~方法名,类名,参数,{},{},{}",className,methodName,Arrays.toString(args));
                return JSON.parseObject(redisValue, Result.class);
            }
            //如果为null还是调用方法,乖乖走数据库吧
            Object proceed = pjp.proceed();
            //然后我们把方法返回的结果,存到缓存中,并设置过期时间,通过注解获取的
            redisTemplate.opsForValue().set(redisKey,JSON.toJSONString(proceed), Duration.ofMillis(expire));
            log.info("存入缓存~~~方法名,类名,参数,{},{},{}",className,methodName,Arrays.toString(args));
            return proceed;
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return Result.fail(-999,"系统错误");
    }

}

3.缓存一致的问题

面试官:缓存一致性问题怎么解决? - 知乎 (zhihu.com)

产生原因

我们在页面的首页,如文章列表,最新文章,最热文章都加了缓存。但是由于写文章或者编辑文章,他还是读的缓存,但是数据库已经更新了。导致缓存一致性问题。

解决方法有下面:

1.更新数据库

2.删除缓存

不论是先更新数据库,再删除缓存,还是先删除缓存,再更新数据库都会造成并发问题。

所以不能这样简单的处理,必须改进,尽量不发生并发问题。

1)第一种方案:当请求1执行update操作后,还未来得及进行缓存清除, 此时请求2查询到并使用了redis中的旧数据。

(2)第二种方案:当请求1执行清除缓存后,还未进行update操作,此时请求2进行查询到了旧数据并写入了redis。

延迟双删策略

先进行缓存清除,再执行update,最后(延迟N秒)再执行缓存清除。

  1. 线程1删除缓存,然后去更新数据库
  2. 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
  3. 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
  4. 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值

在更新文章和写文章之前先删除缓存

通过线程池,在开一个线程,先延时再去删除这个缓存。延迟时间,要大于第二个线程读数据+写缓存的时间。才能刚好删掉缓存。

@Component
@Slf4j
public class LateTimeDelete {

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Async("taskExecutor")
    public void DoubleDelete(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        log.info("延时开始----------------------end");
        //这里我们试试延迟双删
        //listArticle
        Set<String> keys = redisTemplate.keys("listArticle*");
        log.info("需要删除的key值为:{}",keys);
        for (String key : keys) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //hot_article
        Set<String> keys2 = redisTemplate.keys("hot_article*");
        log.info("需要删除的key值为:{}",keys2);
        for (String key : keys2) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //new_article
        Set<String> keys3 = redisTemplate.keys("new_article*");
        log.info("需要删除的key值为:{}",keys3);
        for (String key : keys3) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //view_article
        Set<String> keys4 = redisTemplate.keys("view_article*");
        log.info("需要删除的key值为:{}",keys4);
        for (String key : keys4) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        log.info("延时双删----------------------end");
    }
}

消息队列

先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。

这个解决方案其实问题更多。

  1. 引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦
  2. 就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的

具体步骤

1.引入依赖

<dependency>
    <groupId>org.apache.rocketmq</groupId>
    <artifactId>rocketmq-spring-boot-starter</artifactId>
    <version>2.2.0</version>
</dependency>

2.配置属性

#rocketmq
rocketmq.name-server=localhost:9876
#这个生产者组必须得配置,这个组代表一个组可能有多个服务器,组成一个集群效率更高
rocketmq.producer.group=blog_group

添加一个类,传送消息需要

package com.cjh.blog.vo;

import lombok.Data;

import java.io.Serializable;

@Data
public class ArticleMessage implements Serializable {
    private Long articleId;
}

使用RocketMQTemplate发送各种消息 - 掘金 (juejin.cn)

3.生产者

com/cjh/blog/service/impl/ArticleServiceImpl.java


@Autowired
    private RocketMQTemplate rocketMQTemplate;
//如果是编辑,这是我们需要发送一条消息给mq,当前文章更新,你去删除一下缓存
/**
 * 1.有多个队列,所以我们现在需要队列一个名字blog-update-article
 * 2.第二个参数就是我们要发的消息,一个普通消息,也就是文章id
 */
ArticleMessage articleMessage = new ArticleMessage();
articleMessage.setArticleId(article.getId());
rocketMQTemplate.convertAndSend("blog-update-article",articleMessage);

消费者 com/cjh/blog/service/mq/ArticleListener.java

package com.cjh.blog.service.mq;

import com.cjh.blog.vo.ArticleMessage;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Set;

@Slf4j
@Component
/**
 * topic就是我们发送消息队列的名称
 * consumerGroup的作用和生成者组是一样的
 * 实现的RocketMQListener<T>这个泛型就是我们发送的消息
 */
@RocketMQMessageListener(topic = "blog-update-article",consumerGroup = "blog-update-article-group")
public class ArticleListener implements RocketMQListener<ArticleMessage> {

    /**
     * 注意要用StringRedisTemplate,不知道为啥RedisTemplate不行。
     * 反正用了RedisTemplate就无法获取keys。
     */
    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void onMessage(ArticleMessage message) {
        //这里就能收到消息了
        log.info("mq--------------------start");
        log.info("收到的消息:{}",message);
        //listArticle。通过这个方式就不用消息了
        Set<String> keys = redisTemplate.keys("listArticle*");
        log.info("需要删除的key值为:{}",keys);
        for (String key : keys) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //hot_article
        Set<String> keys2 = redisTemplate.keys("hot_article*");
        log.info("需要删除的key值为:{}",keys2);
        for (String key : keys2) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //new_article
        Set<String> keys3 = redisTemplate.keys("new_article*");
        log.info("需要删除的key值为:{}",keys3);
        for (String key : keys3) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        //view_article
        Set<String> keys4 = redisTemplate.keys("view_article*");
        log.info("需要删除的key值为:{}",keys4);
        for (String key : keys4) {
            redisTemplate.delete(key);
            log.info("删除了文章列表的缓存key:{}",key);
        }
        log.info("mq----------------------end");
    }
}

我们没有配置其他策略,他的消息队列是会持久化的,也就是说消息不会丢,最多就是晚些时间生效

注意:

1.redisTemplate只能读取字节数组,不能读取字符串形式的。

2.字符串形式的值,只能使用StringRedisTemplate读取。

JWT+redis实现登录

了解JWT

JWT生成的token

第一部分叫头部(header), 第二部分叫载荷(payload), 第三部分叫签证(signature)

头部(header)

头部通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。一般是固定的不要存放信息

例如:

{
  "alg": "HS256",
  "typ": "JWT"
}

然后,此JSON被Base64Url编码以形成JWT的第一部分。

载荷(payload)组成部分:这里是可以被破解的

{
  "sub": "1234567890",
  "name": "NILIUSHIGUANG",
  "admin": true
}

签证(signature)

要创建签名部分,您必须获取编码的标头,编码的有效负载,机密,标头中指定的算法,并对其进行签名。

例如,如果要使用HMAC SHA256算法,则将通过以下方式创建签名:

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

将三者放在一起

输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。

前端的token储存

在查阅大量资料后得知,jwt中的token是储存在客户端中,通过请求头添加

所以我们想获取token,得去http的header获取

Authorization: Bearer <token>

创建和解析token的代码

package com.cjh.blog.utils;

import io.jsonwebtoken.Jwt;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JWTUtils {
//    public static void main(String[] args) {
//        String token = JWTUtils.createToken(100L);
//        System.out.println(token);
//        Map<String, Object> map = JWTUtils.checkToken(token);
//        System.out.println(map.get("userId"));
//    }

    //这个就是我们的密钥,不丢就安全
    private static final String jwtToken = "123456Mszlu!@#$$";

    public static String createToken(Long userId){
        //这个就是我们的B部分,用map存
        Map<String,Object> claims = new HashMap<>();
        claims.put("userId",userId);
        //
        JwtBuilder jwtBuilder = Jwts.builder()
                .signWith(SignatureAlgorithm.HS256, jwtToken) // 签发算法,秘钥为jwtToken
                .setClaims(claims) // body数据,要唯一,自行设置
                .setIssuedAt(new Date()) // 设置签发时间
                .setExpiration(new Date(System.currentTimeMillis() + 24 * 60 * 60 * 1000));// 一天的有效时间
        String token = jwtBuilder.compact();
        return token;
    }

    /**
     * 解析token。
     * @param token
     * @return
     */
    public static Map<String, Object> checkToken(String token){
        try {
            Jwt parse = Jwts.parser().setSigningKey(jwtToken).parse(token);
            return (Map<String, Object>) parse.getBody();
        }catch (Exception e){
            e.printStackTrace();
        }
        return null;

    }

}

登录代码

注意我们这里登录分两步 1.成功查到用户信息,这里有登录错误信息统一返回用了枚举,并且把生成的token返回给前端,同时存入redis用户信息

2.通过前端再次发一个请求currentUser,带有token信息,校验token信息,由于我们用了redis可以双重校验,可以通过redis获取用户信息,登录成功

为什么加redis

貌似直接用jwt就能登录啊,用redis干啥。那还不如直接用session

个人认为:

1.可以做主动的登出功能。由于我们登录第二步获取用户信息时,有redis验证,所以登出只要删除redis的信息就行了。

2.token令牌的登录方式,访问认证速度快,天然session共享,安全性

3.灵活控制用户的过期(续期,踢掉线)。续期代码

if (redisService.getExpireTime(userTokenDTO.getId()) < 1 * 60 * 30) 
{ 
redisService.set(userTokenDTO.getId(), token); 
log.error("update token info, id is:{}, user info is:{}", userTokenDTO.getId(), token); 
}

注册功能以及事务的介绍

和登录唯一的区别就是,注册是要insert数据库,另外要保证账号的唯一性,然后把token加入redis。此时我们需要事务支持防止注册失败(比如redis关闭了),但是插入数据库成功。 对了插入如果使用了@TableId注解,不用管xx.setId(),会自动生成。直接插入对象就行

Spring事务

事务管理概述

Spring事务管理分为编程式事务管理和声明式事务管理两种

  1. 编程式事务:允许用户在实现代码中使用显式的方式调用beginTransaction()开启事务、commit()提交事务、rollback()回滚事务,从而可以达到精确定义事务的边界。

  2. 声明式事务管理:底层是建立在Spring AOP的基础上,在方式执行前后进行拦截,并在目标方法开始执行前创建新事务或加入一个已存在事务,最后在目标方法执行完后根据情况提交或者回滚事务。声明式事务的最大优点就是不需要编程,将事务管理从复杂业务逻辑中抽离,只需要在配置文件中配置并在目标方法上添加@Transactional注解即可实现

@Transactional一般用于service层, @Transactional放在类级别上是否等同于该类的每个方法都放上了@Transactional?

是的

Spring事务的隔离级别

  1. @Transactional(isolation = Isolation.READ_UNCOMMITTED)
    读取未提交数据(会出现脏读, 不可重复读) 基本不使用
  1. @Transactional(isolation = Isolation.READ_COMMITTED)
    读取已提交数据(会出现不可重复读和幻读)
  1. @Transactional(isolation = Isolation.REPEATABLE_READ)
    可重复读(会出现幻读)
  1. @Transactional(isolation = Isolation.SERIALIZABLE)
    串行化
  1. @Transactional(isolation = Isolation.DEFAULT)
    默认级别,MYSQL: 默认为REPEATABLE_READ级别 SQLSERVER: 默认为READ_COMMITTED

  2. 总结 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。

Spring事务回滚规则

默认是error和runtimeexception进行回滚

  1. @Transactional(rollbackFor=RuntimeException.class)
    用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则事务回滚。
  1. @Transactional(rollbackForClassName="RuntimeException")
    用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚
  1. @Transactional(noRollbackFor=RuntimeException.class)
    用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚
  1. @Transactional(noRollbackForClassName=RuntimeException.class)
    用于设置不需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,不进行事务回滚

登录拦截器以及thredlocal

每次访问需要登录的资源的时候,都需要在代码中进行判断,一旦登录的逻辑有所改变,代码都得进行变动,非常不合适。

那么可不可以统一进行登录判断呢?

可以,使用拦截器,进行登录拦截,如果遇到需要登录才能访问的接口,如果未登录,拦截器直接返回,并跳转登录页面。

想用拦截器,首先要实现自己的拦截器,然后注入ioc。

步骤:先判断请求是不是访问controller方法,不是放行。

后,校验token成功放行

@Component
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private LoginService loginService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //在执行controller方法(Handler)之前进行执行
        /**
         * 1. 需要判断 请求的接口路径 是否为 HandlerMethod (controller方法)
         * 2. 判断 token是否为空,如果为空 未登录
         * 3. 如果token 不为空,登录验证 loginService checkToken
         * 4. 如果认证成功 放行即可
         */
        //如果不是我们的方法进行放行
        if (!(handler instanceof HandlerMethod)){
            //handler 可能是 RequestResourceHandler springboot 程序 访问静态资源 默认去classpath下的static目录去查询
            return true;
        }
        String token = request.getHeader("Authorization");

        log.info("=================request start===========================");
        String requestURI = request.getRequestURI();
        log.info("请求路径request uri:{}",requestURI);
        log.info("请求方法request method:{}",request.getMethod());
        log.info("登录者验证token:{}", token);
        log.info("=================request end===========================");

        if(StringUtils.isBlank(token)){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            //设置浏览器识别返回的是json
            response.setContentType("application/json;charset=utf-8");
            //https://www.cnblogs.com/qlqwjy/p/7455706.html response.getWriter().print()
            //SON.toJSONString则是将对象转化为Json字符串
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        SysUser sysUser = loginService.checkToken(token);
        if (sysUser == null){
            Result result = Result.fail(ErrorCode.NO_LOGIN.getCode(), "未登录");
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().print(JSON.toJSONString(result));
            return false;
        }
        //是登录状态,放行
        //登录验证成功,放行
        //我希望在controller中 直接获取用户的信息 怎么获取?
        //再去test测试
        UserThreadLocal.put(sysUser);

        return true;
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        //如果不删除,ThreadLocal会有内存泄漏的风险
        UserThreadLocal.remove();
    }
}

配置需要拦截的方法

package com.cjh.blog.config;

import com.cjh.blog.handler.LoginInterceptor;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Autowired
    private LoginInterceptor loginInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //假设拦截test接口后续实际遇到拦截的接口是时,再配置真正的拦截接口

        //用到ThreadLocal获取登录状态的基本都要用拦截器
        //@PostMapping("create/change"),内容就是mapping的值

        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/test")
                .addPathPatterns("/comments/create/change")
                .addPathPatterns("/articles/publish");
    }
}

threadlocal

前面我们用到了拦截器,并且通过拦截器校验token的过程中checkedtoken()获取到了user的信息,此时我们可以保存到ThreadLocal变量中,那么只要有拦截的方法,就能直接从这个变量取得用户信息,而不用再去redis中取值。多线程的情况下也不会有并发问题,因为是线程隔离的。效率比较高

package com.cjh.blog.utils;

import com.cjh.blog.dao.pojo.SysUser;

public class UserThreadLocal {

    private UserThreadLocal(){}
    //线程变量隔离
    private static final ThreadLocal<SysUser> LOCAL = new ThreadLocal<>();

    public static void put(SysUser sysUser){
        LOCAL.set(sysUser);
    }
    public static SysUser get(){
        return LOCAL.get();
    }
    public static void remove(){
        LOCAL.remove();
    }
}

(24条消息) 使用ThreadLocal保存用户登录信息_Mitsuha三葉的博客-CSDN博客_threadlocal保存用户登录信息

非常方便,记得在使用完后remove防止内存泄露

@RequestMapping
public Result test(){
    SysUser sysUser = UserThreadLocal.get();
    System.out.println(sysUser);
    return Result.success(null);
}

spring线程池与乐观锁

@Configuration
/**
 * 开启线程池
 */
@EnableAsync
public class ThreadPoolConfig {
    @Bean("taskExecutor")
    public Executor asyncServiceExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        // 设置核心线程数
        executor.setCorePoolSize(5);
        // 设置最大线程数
        executor.setMaxPoolSize(20);
        //配置队列大小
        executor.setQueueCapacity(10);
        // 设置线程活跃时间(秒)
        executor.setKeepAliveSeconds(60);
        // 设置默认线程名称
        executor.setThreadNamePrefix("myBlog");
        //丢弃线称队列的旧的任务,将新的任务添加
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
        // 等待所有任务结束后再关闭线程池
        executor.setWaitForTasksToCompleteOnShutdown(true);
        //执行初始化
        executor.initialize();
        return executor;
    }
}

乐观锁

阅读数+1,用了线程池,会出现并发问题。由于没有循环,所以高并发时,会丢掉大部分数据

update article set view_count=100 where view_count=99 and id =111

@Component
public class ThreadService {
    /**
     * 加入这个线程池,就会发现,不需要在等待5秒,就能查看文章详情
     * @param articleMapper
     * @param article
     *这个注解就是使用线程的
     */
    @Async("taskExecutor")
    //期望此操作在线程池 执行不会影响原有的主线程
    public void updateArticleViewCount(ArticleMapper articleMapper, Article article){

        int viewCounts = article.getViewCounts();
        //由于使用的是mybatis-plus,他的更新需要对象和wrapper条件。所以我们要new个对象
        Article articleUpdate = new Article();
        articleUpdate.setViewCounts(viewCounts + 1);
        LambdaUpdateWrapper<Article> updateWrapper = new LambdaUpdateWrapper<>();
        updateWrapper.eq(Article::getId,article.getId());
        //下面的条件:设置一个为了在多线程的环境下线程安全
        //改之前再确认这个值有没有被其他线程抢先修改,类似于CAS操作 cas加自旋,加个循环就是cas。这是乐观锁
        updateWrapper.eq(Article ::getViewCounts,viewCounts );
        // update article set view_count=100 where view_count=99 and id =111
        int update = articleMapper.update(articleUpdate, updateWrapper);
    }
}

为什么要用线程池

查看完文章了,新增阅读数,有没有问题呢?

查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低

更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作

线程池 可以把更新操作 扔到线程池中去执行,和主线程就不相关了

@Override
public Result findArticleById(Long articleId) {
    /**
     * 1. 根据id查询 文章信息
     * 2. 根据bodyId和categoryid 去做关联查询
     */
    Article article = this.articleMapper.selectById(articleId);
    threadService.updateArticleViewCount(articleMapper,article);
    ArticleVo articleVo = copy(article, true, true,true,true);
    //查看完文章了,新增阅读数,有没有问题呢?
    //查看完文章之后,本应该直接返回数据了,这时候做了一个更新操作,更新时加写锁,阻塞其他的读操作,性能就会比较低
    // 更新 增加了此次接口的 耗时 如果一旦更新出问题,不能影响 查看文章的操作
    //线程池  可以把更新操作 扔到线程池中去执行,和主线程就不相关了
    //threadService.updateArticleViewCount(articleMapper,article);

    return Result.success(articleVo);
}