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();
}
}
}
结果
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删除缓存,然后去更新数据库
- 线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存
- 线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除
- 如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值
在更新文章和写文章之前先删除缓存
通过线程池,在开一个线程,先延时再去删除这个缓存。延迟时间,要大于第二个线程读数据+写缓存的时间。才能刚好删掉缓存。
@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.引入依赖
<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事务管理分为编程式事务管理和声明式事务管理两种
-
编程式事务:允许用户在实现代码中使用显式的方式调用beginTransaction()开启事务、commit()提交事务、rollback()回滚事务,从而可以达到精确定义事务的边界。
-
声明式事务管理:底层是建立在Spring AOP的基础上,在方式执行前后进行拦截,并在目标方法开始执行前创建新事务或加入一个已存在事务,最后在目标方法执行完后根据情况提交或者回滚事务。声明式事务的最大优点就是不需要编程,将事务管理从复杂业务逻辑中抽离,只需要在配置文件中配置并在目标方法上添加@Transactional注解即可实现
@Transactional一般用于service层, @Transactional放在类级别上是否等同于该类的每个方法都放上了@Transactional?
是的
Spring事务的隔离级别
- @Transactional(isolation = Isolation.READ_UNCOMMITTED)
读取未提交数据(会出现脏读, 不可重复读) 基本不使用
- @Transactional(isolation = Isolation.READ_COMMITTED)
读取已提交数据(会出现不可重复读和幻读)
- @Transactional(isolation = Isolation.REPEATABLE_READ)
可重复读(会出现幻读)
- @Transactional(isolation = Isolation.SERIALIZABLE)
串行化
-
@Transactional(isolation = Isolation.DEFAULT)
默认级别,MYSQL: 默认为REPEATABLE_READ级别 SQLSERVER: 默认为READ_COMMITTED -
总结 隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。
Spring事务回滚规则
默认是error和runtimeexception进行回滚
- @Transactional(rollbackFor=RuntimeException.class)
用于设置需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,则事务回滚。
- @Transactional(rollbackForClassName="RuntimeException")
用于设置需要进行回滚的异常类名称数组,当方法中抛出指定异常名称数组中的异常时,则进行事务回滚
- @Transactional(noRollbackFor=RuntimeException.class)
用于设置不需要进行回滚的异常类数组,当方法中抛出指定异常数组中的异常时,不进行事务回滚
-
@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);
}