likes
comments
collection
share

利用自定义注解与AOP实现高效缓存更新策略

作者站长头像
站长
· 阅读数 5

前言

哈喽,大家好,今天和大家分享在重构过程中使用注解和反射的一些心得,本章主要包括以下几个问题:

  1. 在什么场景下使用自定义注解
  2. 自定义注解的使用方法介绍
  3. 自定义注解的优缺点
  4. 反射的使用
1.在什么场景下使用自定义注解

在重构过程中遇到这样一个需求,就是系统会将一部分不易变动的数据存放在redis中,如果用户在界面上修改了这部分数据,就会将新数据重新加载到redis中,这类数据比较多,新增、修改、删除都要对缓存进行更新,旧的代码框架在处理时,是通过硬编码,将更新操作编码在每一个增删改的接口中,根据业务场景不同,分别调用不同的loadRedis方法(如:loadRedis100,loadRedis200)。

代码如下:

//缓存更新场景1,每次的增删改都要调用serviceOne的loadRedis方法

@PostMapping("update")
public Result update(@RequestBody UpdateEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceOne.loadRedis(vo.getRedisKey());
}
@PostMapping("insert")
public Result add(@RequestBody InsertEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceOne.loadRedis(vo.getRedisKey());
}
@PostMapping("delete")
public Result delete(@RequestBody DeleteEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceOne.loadRedis(vo.getRedisKey());
}

--------------------------------------------------------------

//缓存更新场景2,每次的增删改都要调用serviceTwo的loadRedis方法

@PostMapping("update")
public Result update(@RequestBody UpdateEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceTwo.loadRedis(vo.getRedisKey());
}
@PostMapping("insert")
public Result add(@RequestBody InsertEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceTwo.loadRedis(vo.getRedisKey());
}
@PostMapping("delete")
public Result delete(@RequestBody DeleteEntity vo) {
    //1.业务操作
    doBusiness();
    //2.更新缓存
    serviceTwo.loadRedis(vo.getRedisKey());
}

如果loadRedis()只是写几处,可能问题还不大,然而我们老系统中涉及到的地方有上百处,如果依旧采取这种方式,无法进行统一管理,对后期的代码维护是非常困难的,这里把开发变成体力活,肯定不是我们希望看到的。

看过面试题的小伙伴肯定一眼就看出来这种情况非常适合aop,我也是这么处理的,首先得理一理思路,明确我们要解决的问题是什么,然后列出解决的思路。

我们要解决的问题:

  1. 统一管理redis缓存的更新业务
  2. 减少硬编码
  3. 动态获取接口入参

2.自定义注解+Aop的示例

  • 创建自定义注解

首先定义一个注解,包括两个参数(演示一个静态参数、一个动态参数的获取),customParam 参数是为了从接口入参中获取一些动态数据,比如body中的userName的值,redisKey 则是告诉aop,我需要调用哪个刷新方法(如loadRedis100、loadRedis200)。

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RedisLoadAnnotation {
    String customParam() default "";
    String redisKey() default "";
}
  • 使用注解

使用注解时,传入了两个参数,我们后面需要通过反射获取到request对象中的userName的值

@PostMapping("test")
@RedisLoadAnnotation(redisKey = "key_animal",customParam = "request.userName")
public void test(String param1 , @RequestBody AnnotationTest request){
    logger.info("业务处理");
}
  • 定义切面类

接下来定义一个切面类,我使用的是 @AfterReturning,因为我需要在整个业务处理完成并且正常返回才刷新redis。

@Slf4j
@Aspect
@Component
public class ReloadRedisAspect {
    private static final Logger logger = LoggerFactory.getLogger(ReloadRedisAspect.class);

    @Pointcut("@annotation(com.huage.demo.annotation.RedisLoadAnnotation)")
    public void redisLoad(){

    }

    @AfterReturning(pointcut ="redisLoad()")
    public void afterReturning(JoinPoint joinPoint) {
        MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
        Method method = methodSignature.getMethod();

        //1.获取注解
        RedisLoadAnnotation redisLoadAnnotation = method.getAnnotation(RedisLoadAnnotation.class);
        String customParamName = redisLoadAnnotation.customParam();

        //2.从入参中获取动态数据
        String customParamValue = getCustomParam(customParamName, method, joinPoint);

        //3.从注解中获取静态参数
        String redisKey = redisLoadAnnotation.redisKey();

        //4.更新缓存
        logger.info("执行更新缓存逻辑");
        customService.reloadRedis(customParamValue,redisKey);
    }
}

  • 获取自定义参数

获取自定义参数我是通过反射进行对比,具体可以看下面的注释

/**
 * 通过反射获取自定义参数
 **/
private String getCustomParam(String customParamName,Method method,JoinPoint joinPoint){
    Parameter[] parameters = method.getParameters();
    //获取注解的参数classParam值:即request.userName
    String[] customParamArr = customParamName.split("\.");
    String instanceName = customParamArr[0];
    String paramName = customParamArr[1];

    //获取注解配置的参数下标,例如一个方法有多个参数,现在需要找到参数名为即request的参数
    Integer index = -1;
    for (Parameter parameter : parameters) {
        index++;
        if(instanceName.equals(parameter.getName())){
            break;
        }
    }

    //没有找到注解配置的customParamName对应参数,直接返回
    if(index == -1){
        return null;
    }

    //通过反射获取customParamName实际参数值
    Object[] args = joinPoint.getArgs();
    Object params = args[index];
    try {
        Field[] declaredFields = params.getClass().getDeclaredFields();
        for (Field field : declaredFields) {
            field.setAccessible(true);
            if(field.getName().equals(paramName)){
                return field.get(params).toString();
            }
        }
    } catch (Exception e) {
        logger.error("执行失败");
    }
    return null;
}
  • 更新缓存

在更新缓存时,因为我们有很多业务场景类,每个类中都有一个同名的方法loadRedis,所以我考虑是将这些场景类的包路径维护到数据库中,与redisKey进行映射,这样我们就可以写一个公共方法,通过反射调用不同场景类的loadRedis方法,即: customService.reloadRedis(customParamValue,redisKey) ;

Class clazz = Class.forName(classPath);
Method method = clazz.getDeclaredMethod("loadRedis",Integer.class);
Object obj = clazz.newInstance();
method.invoke(obj,customParamValue);

最后

aop其实大家都很熟悉,也都知道怎么使用,不过之前在开发过程中,遇到类似的场景,有时候为了省事或者说是懒吧,就宁愿去费时费力的复制粘贴,现在我就努力强迫自己,尽量多想一想,就算很简单的技巧,也要动手去做出来,尽量让代码保持干净。

下一篇会继续聊聊重构过程中,遇到的一个有意思的小技巧,如果让代码免于重新打包编译,就能够适应不同的数据源和不同的sql语句。