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

前言

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

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

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

代码如下:

less 复制代码
//缓存更新场景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)。

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

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

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

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

less 复制代码
@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);
    }
}
​
  • 获取自定义参数

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

ini 复制代码
/**
 * 通过反射获取自定义参数
 **/
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) ;

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

最后

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

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

相关推荐
苹果醋310 分钟前
AI大模型竞赛升温:百度发布文心大模型4.5和X1
java·运维·spring boot·mysql·nginx
网安INF15 分钟前
CVE-2020-1938源码分析与漏洞复现(Tomcat 文件包含/读取)
java·网络·web安全·网络安全·tomcat·漏洞复现
nenchoumi311926 分钟前
UE5 学习系列(九)光照系统介绍
java·学习·ue5
江梦寻29 分钟前
软件工程教学评价
开发语言·后端·macos·架构·github·软件工程
张乔2437 分钟前
spring boot项目整合mybatis实现多数据源的配置
java·spring boot·多数据源
GzlAndy41 分钟前
Tomcat调优
java·tomcat
美好的事情能不能发生在我身上44 分钟前
苍穹外卖Day11代码解析以及深入思考
java·spring boot·后端·spring·架构
辉辉健身中1 小时前
Maven入门(够用)
java·maven
星火飞码iFlyCode1 小时前
【无标题】
java·前端·人工智能·算法
不良手残1 小时前
Redisson + Lettuce 在 Spring Boot 中的最佳实践方案
java·spring boot·redis·后端