利用自定义注解与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语句。

相关推荐
ok!ko3 小时前
设计模式之原型模式(通俗易懂--代码辅助理解【Java版】)
java·设计模式·原型模式
2401_857622663 小时前
SpringBoot框架下校园资料库的构建与优化
spring boot·后端·php
2402_857589364 小时前
“衣依”服装销售平台:Spring Boot框架的设计与实现
java·spring boot·后端
吾爱星辰4 小时前
Kotlin 处理字符串和正则表达式(二十一)
java·开发语言·jvm·正则表达式·kotlin
哎呦没5 小时前
大学生就业招聘:Spring Boot系统的架构分析
java·spring boot·后端
_.Switch5 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
编程、小哥哥5 小时前
netty之Netty与SpringBoot整合
java·spring boot·spring
IT学长编程6 小时前
计算机毕业设计 玩具租赁系统的设计与实现 Java实战项目 附源码+文档+视频讲解
java·spring boot·毕业设计·课程设计·毕业论文·计算机毕业设计选题·玩具租赁系统
莹雨潇潇6 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
杨哥带你写代码7 小时前
足球青训俱乐部管理:Spring Boot技术驱动
java·spring boot·后端