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)——包
用来定义注解的生命周期的,并且在使用时需要指定RetentionPolicy,RetentionPolicy有三种策略,分别是:
SOURCE - 注解只保留在源文件,当Java文件编译成class文件的时候,注解被遗弃。
CLASS - 注解被保留到class文件,但jvm加载class文件时候被遗弃,这是默认的生命周期。
RUNTIME - 注解不仅被保存到class文件中,jvm加载class文件之后,仍然存在。
注解只是用来做标识,没什么实际作用,了解就好。
如果使用@documented标注了,在生成javadoc的时候就会把@documented注解给显示出来。
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@documented
public @interface JhRedisCache {
String key() default "";
long expire() default 86400L;
Class type() default Object.class;
}
JhRedisCacheEvict——删除redis缓存的注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@documented
public @interface JhRedisCacheEvict {
String key() default "";
}
四、自定义AOP切面
@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;
@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);
}
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 = expression.getValue(context);
return redisKey + KEY_SEPARATOR+"_"+Objects.toString(value,"");
}
private Object deserialize(String json, Class clazz, Class modelType) {
return clazz.isAssignableFrom(List.class) ? JSON.parseArray(json, modelType) : JSON.parseObject(json, clazz);
}
}
五、使用案例
简单的使用:
@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的配置,这里不再带领大家测试,有兴趣的小伙伴可以在项目中测试看看,欢迎有问题随时沟通。



