自定义缓存切面注解
1:展示效果
- 如果使用这种方式,建议使用切面缓存异常类,如果当前方法缓存失败,也应该执行具体业务逻辑而非报错终止程序,具体参考切面缓存@CacheException
- 1.1: 成功保存到redis效果
- 第一次Redis没有值,所以保存查询了数据库。
1.2:第二次请求
2.1:前置条件
-
技术栈需要会简单的redis存、取、删和SpringAop即可
-
2.2: 安装redis
链接地址: 官网链接
-
下载完成之后解压到安装路径,如果需要设置redis密码,自己去配置文件中设置即可。我这边就是简单的下载然后再windows中演示了。
3:解压完成之后
4:如何启动redis
没设置密码直接双击即可
启动成功能看到如下界面
如果需要操作查看key则需要启动
4: 后端的application.yml
- 我这边项目是微服务的,所以配置都在nacos中,这边只是截取为了方便。如果需要则将nacos的依赖删除
server:
port: 8002
servlet:
context-path: /system # 上下文件路径,请求前缀 ip:port/article
spring:
cloud:
nacos:
discovery:
# 服务注册中心地址
server-addr: localhost:8848
servlet:
multipart:
max-request-size: 200MB
max-file-size: 200MB
application:
name: system-service # 应用名
# 数据源配置
datasource:
type: com.alibaba.druid.pool.DruidDataSource
username: root
password: 2829
url: jdbc:mysql://127.0.0.1:3306/qycq?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=GMT%2B8&allowMultiQueries=true
hikari:
# 连接池名
pool-name: DateHikariCP
# 最小空闲连接数
minimum-idle: 5
# 空闲连接存活最大时间,默认600000(10分钟)
idle-timeout: 180000
# 最大连接数,默认10
maximum-pool-size: 10
# 从连接池返回的连接的自动提交
auto-commit: true
# 连接最大存活时间,0表示永久存活,默认1800000(30分钟)
max-lifetime: 1800000
# 连接超时时间,默认30000(30秒)
connection-timeout: 30000
# 测试连接是否可用的查询语句
connection-test-query: SELECT 1 FROM DUAL
#redis
redis:
host: 127.0.0.1
port: 6379
database: 0
timeout: 100000ms #超时时间
lettuce:
pool:
max-active: 1024 #最大连接数
max-wait: 10000ms #最大等待时间
max-idle: 200 #最大空闲连接
min-idle: 5 #最小空闲链接
mybatis-plus:
type-aliases-package: com.qycq.api.pojo
# xxxMapper.xml 路径
mapper-locations: classpath*:/mapper/*Mapper.xml
# 日志级别,会打印sql语句
logging:
level:
com.qycq.system.mapper: debug
5:自定义注解
package com.qycq.api.annotations;
import io.swagger.annotations.ApiModel;
import java.lang.annotation.*;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 6:02
*/
@Documented
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@ApiModel(value = "自定义redis缓存注解")
public @interface CacheRedis {
/**
* redis的key
* @return
*/
String key() default "";
/**
* 缓存失效时间,默认为300秒
* @return
*/
long expireTime() default 300;
/**
* 是否存储空值,默认为true,防止雪崩
* @return
*/
boolean cacheNullValue() default true;
/**
* 缓存方法描述
* @return
*/
String describe() default "";
}
6:pom依赖
<!-- web依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Druid连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!--mybatis-plus启动器-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!-- 配置处理器处理yml文件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<!--lombok setter,getter-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<!--swagger依赖-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<!--swaggerUI依赖-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
</dependency>
<!-- 工具类依赖 -->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!-- redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--对象池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
7:Redis工具类
- RedisService
package com.qycq.common.service;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 2:40
*/
public interface RedisService {
/**
* 根据key和类型获取缓存数据
*
* @param key
* @param clazz
* @param <T>
* @return
*/
<T> Optional<T> get(String key, Class<T> clazz);
/**
* 将数据以键值对的方式加入缓存
*
* @param key
* @param value
*/
void set(String key, Object value);
/**
* 将数据以键值对的方式加入缓存并设置失效时间,默认1天
*
* @param key
* @param value
* @param timeout
* @param day
*/
void setDay(String key, Object value, long timeout, final TimeUnit day);
/**
* 将数据以键值对的方式加入缓存并设置失效时间,默认为5分钟
*
* @param key
* @param value
* @param timeout
*/
void set(String key, Object value, long timeout);
/**
* 将数据以键值对的方式加入缓存并设置失效时间,默认为5秒
*
* @param key
* @param value
* @param miao
*/
void set(Object key, Object value, long miao,final TimeUnit second);
/**
* 删除多个key
*
* @param keys
*/
void deletes(String... keys);
/**
* 从缓存中删除
*
* @param keyPattern
*/
void deleteWithPattern(String keyPattern);
/**
* 获取string类型的值
*
* @param key
* @return
*/
Object getJson(String key);
/**
* 设置失效时间
*
* @param key
* @param timeout
* @param timeUnit
* @return
*/
boolean expired(String key, long timeout, TimeUnit timeUnit);
/**
* 查看当前key是否存在
* @param key
* @return
*/
boolean hasKey(String key);
}
2: impl
package com.qycq.common.service;
import com.qycq.common.utils.JsonUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.Cursor;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ScanOptions;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashSet;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 2:41
*/
@Service("redisService")
@Transactional
@Slf4j
public class RedisServiceImpl implements RedisService{
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Override
public <T> Optional<T> get(String key, Class<T> clazz) {
String value = (String) redisTemplate.opsForValue().get(key);
if (value == null) {
return Optional.empty();
}
return Optional.ofNullable(JsonUtil.toObject(value, clazz));
}
@Override
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, Objects.requireNonNull(value));
}
@Override
public void setDay(String key, Object value, long timeout, TimeUnit day) {
redisTemplate.opsForValue().set(key, Objects.requireNonNull(value), timeout, TimeUnit.DAYS);
}
@Override
public void set(String key, Object value, long timeout) {
redisTemplate.opsForValue().set(key, Objects.requireNonNull(value), 5, TimeUnit.MINUTES);
}
/**
* second
*
* @param key
* @param value
* @param miao
* @param second
*/
@Override
public void set(Object key, Object value, long miao, TimeUnit second) {
redisTemplate.opsForValue().set((String) key, Objects.requireNonNull(value), 300, TimeUnit.SECONDS);
}
@Override
public void deletes(String... keys) {
if (keys != null) {
for (String key : keys) {
redisTemplate.delete(key);
}
}
}
@Override
public void deleteWithPattern(String keyPattern) {
Set<String> keys = scan(keyPattern);
if (keys != null) {
redisTemplate.delete(keys);
}
}
private Set<String> scan(String matchKey) {
return redisTemplate.execute((RedisCallback<Set<String>>) connection -> {
Set<String> keysTmp = new HashSet<>();
Cursor<byte[]> cursor = connection.scan(
new ScanOptions.ScanOptionsBuilder().match("*" + matchKey + "*").count(1000).build()
);
while (cursor.hasNext()) {
keysTmp.add(new String(cursor.next()));
}
return keysTmp;
});
}
@Override
public Object getJson(String key) {
return redisTemplate.opsForValue().get(key);
}
/**
* 判断是否过期
*
* @param key
* @param timeout
* @param timeUnit
* @return
*/
@Override
public boolean expired(String key, long timeout, TimeUnit timeUnit) {
return Objects.requireNonNull(redisTemplate.expire(key, timeout, timeUnit));
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
@Override
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
log.error(key, e);
return false;
}
}
}
3: 暴露bean,这便是做成了父类,因为其他模块引用直接继承就好了或者向下面这样写也可以
package com.qycq.common.config;
import com.qycq.common.service.RedisService;
import com.qycq.common.service.RedisServiceImpl;
import io.swagger.annotations.ApiModel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 2:45
*/
@Configuration
@ApiModel(value = "redis配置类")
public class BaseRedisConfig {
@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory();
}
/**
* 配置redis组件
*
* @return
*/
@Bean
// @Primary
public RedisTemplate<String, Object> redisTemplate() {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
//序列化key value
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
//序列化Hash
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
return redisTemplate;
}
/**
* 暴露bean
* @return
*/
@Bean
public RedisService redisService(){
return new RedisServiceImpl();
}
}
4: 此处制作展示,继承/import注解导入也行,然后将这个注解加到启动类
package com.qycq.api.annotations;
import com.qycq.common.config.BaseRedisConfig;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import java.lang.annotation.*;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
* 开启redis配置
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 3:01
*/
@Documented
@Target({ElementType.TYPE})
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Configuration
@Import({BaseRedisConfig.class})
public @interface EnableRedisConfig {
Class<? extends BaseRedisConfig> value() default BaseRedisConfig.class;
boolean required() default true;
}
8:AOP切面
package com.qycq.system.aspect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.qycq.api.annotations.CacheException;
import com.qycq.api.annotations.CacheRedis;
import com.qycq.common.service.RedisService;
import com.qycq.common.utils.HttpServletRequestUtil;
import com.qycq.common.utils.IpUtils;
import io.swagger.annotations.ApiModel;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* 概要:
* <p> As we all know, where there is Wolong, there must be phoenix!</p>
*
* @author 七月初七
* @version 1.0
* @date 2021/11/17 6:07
*/
@Configuration
@Slf4j
@Aspect
@ApiModel(value = "redis缓存切面类")
@Order(2)
public class CacheRedisAspect {
@Autowired
private RedisService redisService;
/**
* 默认为查询数据空也存入到redis,防止雪崩
*/
private boolean FLAG = true;
/**
* redis的key,输入则走输入的,不输入默认以前缀 + 接口方法名为主
*/
private String KEY = "";
/**
* 默认存在时间
*/
public long EXPIRE_TIME = 300;
/**
* 切点
*/
@Pointcut("@annotation(com.qycq.api.annotations.CacheRedis)")
public void pointcut() {
// TODO document why this method is empty
}
/**
* 打印日志
*
* @param joinPoint
* @param httpServletRequest
*/
private void setLog(JoinPoint joinPoint, HttpServletRequest httpServletRequest) {
log.info("methodType:{}", httpServletRequest.getMethod());
log.info("requestMethodName:{}", joinPoint.getSignature().getName());
log.info("requestURL:{}", httpServletRequest.getRequestURL());
log.info("requestURI:{}", IpUtils.getIpAddr(httpServletRequest));
log.info("className:{}", joinPoint.getTarget().getClass().getName() + "." + joinPoint.getSignature().getName());
log.info("time:{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
}
/**
* 环绕增强
*
* @param point
* @return
*/
@Around("pointcut()")
public Object around(ProceedingJoinPoint point){
log.info("。。。。。。进入到环绕增强。。。。。。");
Object result = null;
/**
* 判断当前的key是否存在
*/
if (redisService.hasKey(KEY)) {
log.warn("...该数据在缓存中已存在,去缓存查数据....");
result = redisService.getJson(KEY);
return result;
}
//获取全局上下文的HttpServletRequest
HttpServletRequest httpServletRequest = HttpServletRequestUtil.getContextHttpServletRequest();
Method method = getMethod(point);
try {
//先判断当前方法上是否含有注解或注解为空
if (method.isAnnotationPresent(CacheRedis.class)) {
CacheRedis annotation = method.getAnnotation(CacheRedis.class);
if (annotation == null) {
log.warn("当前方法" + "[" + point.getSignature().getName() + "]不需要拦截,因为判断没有注解!");
return point.proceed();
} else {
/**
* 获取redis的key
*/
if (StringUtils.isEmpty(annotation.key())) {
//名字默认为方法名称
KEY = point.getSignature().getName();
log.info("当前key为:》》》》》》》》》》》{}", KEY);
} else {
KEY = annotation.key();
}
/**
* 获取超时时间
*/
long expireTime = annotation.expireTime();
if (expireTime == EXPIRE_TIME) {
//说明没有设置时间
EXPIRE_TIME = 300;
} else {
//否则设置过期时间
EXPIRE_TIME = annotation.expireTime();
}
/**
* 如果为true则不需要动
*/
if (annotation.cacheNullValue()) {
FLAG = annotation.cacheNullValue();
} else {
FLAG = false;
}
//将数据存到redis
result = point.proceed();
redisService.set(KEY, result, EXPIRE_TIME, TimeUnit.SECONDS);
log.info("数据存入redis成功!当前的redisKey为:=========>{}", KEY);
}
}
this.setLog(point, httpServletRequest);
} catch (Throwable throwable) {
log.error("缓存方法发生异常,打印的堆栈信息为:{}", throwable.getMessage());
throwable.printStackTrace();
}
//输出返回结果
return result;
}
/**
* 获取被拦截方法对象
*
* @param pjp
* @return
*/
private Method getMethod(ProceedingJoinPoint pjp) {
Signature signature = pjp.getSignature();
MethodSignature methodSignature = (MethodSignature) signature;
return methodSignature.getMethod();
}
}
9: 最后将注解放到你的方法上即可,效果如刚进来展示那个样子
10:项目地址
注解在api模块, 切面在system下面。