一、前言
redis是分布式微服务中必用的基础组件之一,现在国内的大部分项目基本上用到,缓存是其主要作用之一,而在项目中频繁使用set()方法添加注解,会造成代码的重复和臃肿,对于开发经验不足的小白,甚至会因为缓存的添加不当直接影响到正常的业务流程,从而酿成事故,因此成熟的公司都会通过封装基础组件,实现通过注解自动添加redis缓存,本文会从原理出发,带领大家亲自实现自定义注解,完成redis缓存的开发,学会了,你可以在同事面前秀一把了。
二、自定义注解的参数说明
@Target: 注解的作用目标,即注解可以使用的位置,通常有
@Target(ElementType.TYPE)——接口、类、枚举、注解
@Target(ElementType.FIELD)——字段、枚举的常量
@Target(ElementType.METHOD)——方法
@Target(ElementType.PARAMETER)——方法参数
@Target(ElementType.CONSTRUCTOR) ——构造函数
@Target(ElementType.LOCAL_VARIABLE)——局部变量
@Target(ElementType.ANNOTATION_TYPE)——注解
@Target(ElementType.PACKAGE)——包
@Retention 注解的保留位置 用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicy,RetentionPolicy有三种策略,分别是:
SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。
@Documented 注解只是用来做标识,没什么实际作用,了解就好。
如果使用@Documented标注了,在生成javadoc的时候就会把@Documented注解给显示出来。
三、自定义redis注解
JhRedisCache——添加redis缓存的注解
/**
* @author ljx
* @Description: 添加redis缓存的注解
* @date 2020/6/9 4:11 下午
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCache {
/*
* redis缓存中的key,支持spel表达式
*/
String key() default "";
/*
* 缓存时间,默认缓存时间是一天 60*60*24
*/
long expire() default 86400L;
/*
* 如果注解添加在返回list的方法上,则需要通过该字段指定list中的class类型
*/
Class type() default Object.class;
}
JhRedisCacheEvict——删除redis缓存的注解
/**
* @author ljx
* @Description: 删除redis缓存的注解
* @date 2020/6/10 3:38 下午
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface JhRedisCacheEvict {
String key() default "";
}
四、自定义AOP切面
/**
* @author ljx
* @Description: 注解切面
* @date 2020/6/9 11:12 上午
*/
@Component
@Aspect
public class RedisCacheAspect {
private static final String SPEL = "#";
private static final String KEY_SEPARATOR = "_";
private static final int TWO = 2;
private static final Logger logger = LoggerFactory.getLogger(RedisCacheAspect.class);
private RedisClient redisClient;
private AppInfo appInfo;
/**
* @Description: 在使用JhRedisCache注解的地方切入此切点,查询缓存是否在redis中存在,若已存在,则直接返回,否则查询数据库
* @param pjp 切入点信息
* @return java.lang.Object 方法返回值
* @Author: ljx
* @Date: 2020/6/9 4:20 下午
*/
@Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCache)")
private Object handleCache(final ProceedingJoinPoint pjp) throws Throwable {
// 获取切入的方法对象
// 这个m是代理对象的,没有包含注解
Method m = ((MethodSignature) pjp.getSignature()).getMethod();
// this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
// 根据目标方法对象获取注解对象
JhRedisCache cacheAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCache.class);
// 解析key
String keyExpr = cacheAnnotation.key();
Object[] as = pjp.getArgs();
String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
// 到redis中获取缓存
String cache = null;
try {
cache = redisClient.get(key);
} catch (Exception e) {
logger.error("{}查询redis缓存异常:{}",keyExpr,e.getMessage());
}
if (StringUtils.isBlank(cache)) {
// 若不存在,则到数据库中去获取
Object result = pjp.proceed();
// 从数据库获取后存入redis,若有指定过期时间,则设置
try {
long expireTime = cacheAnnotation.expire();
if (expireTime > 0) {
redisClient.set(key,JSON.toJSONString(result), expireTime, TimeUnit.SECONDS);
}else{
redisClient.set(key, JSON.toJSONString(result));
}
} catch (Exception e) {
logger.warn("{}{}缓存redis异常:{}",keyExpr,e.getMessage(),result);
}
return result;
}
// 得到被代理的方法上的注解
Class modelType = cacheAnnotation.type();
// 得到被代理方法的返回值类型
Class returnType = ((MethodSignature) pjp.getSignature()).getReturnType();
// 返回反序列化从缓存中拿到的json
return deserialize(cache, returnType, modelType);
}@Around("@annotation(edu.jiahui.redis.starter.annotation.JhRedisCacheEvict)")
private Object handleCacheEvict(ProceedingJoinPoint pjp) throws Throwable {
// 获取切入的方法对象
// 这个m是代理对象的,没有包含注解
Method m = ((MethodSignature) pjp.getSignature()).getMethod();
// this()返回代理对象,target()返回目标对象,目标对象反射获取的method对象才包含注解
Method methodWithAnnotations = pjp.getTarget().getClass().getDeclaredMethod(pjp.getSignature().getName(), m.getParameterTypes());
// 根据目标方法对象获取注解对象
JhRedisCacheEvict cacheEvictAnnotation = methodWithAnnotations.getDeclaredAnnotation(JhRedisCacheEvict.class);
// 解析key
String keyExpr = cacheEvictAnnotation.key();
Object[] as = pjp.getArgs();
String key = getRedisKeyBySpel(keyExpr,methodWithAnnotations, as);
// 先删除数据库中的用户信息再删除缓存
Object result = pjp.proceed();
redisClient.delete(key);
return result;
}public RedisCacheAspect() {
// 初始化redisClient对象,不同的项目可能实现不同,此处是结合自己项目中的redis实现的
appInfo= SpringContext.getBean(AppInfo.class);
String appName = appInfo.getAppName();
this.redisClient = SpringContext.getBean(appName, RedisClient.class);
}/**
* @Description: 解析注解中的key,支持spel表达式的解析
* @param spelExpress 注解中spel表达式
* @param method 方法对象
* @param params 方法参数
* @return java.lang.String
* @Author: ljx
* @Date: 2020/6/10 3:16 下午
*/
private String getRedisKeyBySpel(String spelExpress, Method method, Object[] params) {
String redisKey = appInfo.getAppName()+KEY_SEPARATOR+method.getName();
// 如果为空,则默认服务名_方法名
if (StringUtils.isBlank(spelExpress)){
return redisKey;
}
// 如果不是spel表达式,则直接使用用户传入的key
if(!spelExpress.contains(SPEL)){
return spelExpress;
}
// 如果是spel表达式,但是参数为空,则默认服务名_方法名
if(params==null){
return redisKey;
}
ExpressionParser parser = new SpelExpressionParser();
EvaluationContext context = new StandardEvaluationContext();
// spel表达式用到的变量,设置第一个参数
context.setVariable("entity", params[0]);
// 设置第二个参数
if(params.length>1&¶ms[1]!=null){
context.setVariable("entityTwo", params[1]);
}
// 设置第三个参数
if(params.length>TWO&¶ms[TWO]!=null){
context.setVariable("entityTrd", params[2]);
}
// 解析spel表达式
Expression expression = parser.parseExpression(spelExpress, new TemplateParserContext());
final Object value = https://www.it610.com/article/expression.getValue(context);
return redisKey + KEY_SEPARATOR+"_"+Objects.toString(value,"");
}/**
* @Description: FastJSON反序列化获得对象
* @param json 从redis缓存中获取的字符串
* @param clazz 添加注解的方法返回值的class类型
* @param modelType 转换成list中的class类型
* @return java.lang.Object
* @Author: ljx
* @Date: 2020/6/11 3:50 下午
*/
private Object deserialize(String json, Class clazz, Class modelType) {
return clazz.isAssignableFrom(List.class) ? JSON.parseArray(json, modelType) : JSON.parseObject(json, clazz);
}}
五、使用案例
【高并发|Spring自定义注解实现redis缓存】简单的使用:
@JhRedisCache(key = "#{#entity}")
public TeachCenter selectTeacherCenter(Integer teachCenterId) {
return teachCenterMapper.selectByPrimaryKey(teachCenterId);
}
复杂使用:
@JhRedisCache(key = "#{#entity.getProvinceId()}", type = TeachCenter.class)
public List selectTeachCenterList(TeachCenterCommonRequest teacherCenterCommonRequest) {
//获取地区id;
Integer provinceId = teacherCenterCommonRequest.getProvinceId();
//分页
PageHelper.startPage(teacherCenterCommonRequest.getPageNum(), teacherCenterCommonRequest.getPageSize());
//获取所有教学中心
List teachCenterList = teachCenterMapper.selectByProvinceId(provinceId);
return teachCenterList;
}
本人亲测有效,并已在公司项目中大规模使用,因为依赖redis的配置,这里不再带领大家测试,有兴趣的小伙伴可以在项目中测试看看,欢迎有问题随时沟通。
推荐阅读
- 分布式技术原理与算法解析|分布式数据(缓存技术)
- Redis|27 Redis 缓存污染问题
- Redis|24 Redis 缓存替换时的数据淘汰策略
- #yyds干货盘点# Spring嵌套事务是怎么回滚的()
- springboot|Springboot 启动流程源码解析(广播器,以及内嵌tomcat启动)
- 编程|手写Spring核心原理MVC实现
- Spring|《SpringBoot启动流程六》(源码分析SpringBoot如何内嵌并启动Tomcat服务器的())
- 图解 Eureka 的缓存架构 #yyds干货盘点#
- 详细讲解Spring中的@Bean注解