乐趣区

关于springboot:模仿Cacheables实现方法拦截

背景

在 SpringBoot 开发中,通过 @Cacheable 注解便能够实现办法级别缓存,如下

 @GetMapping(value = "/user/detail")
 @Cacheable(value = "user", key = "#uid")
 public User deteail(@RequestParam(value = "uid") String uid) {...}

Cacheable 的逻辑

  • 如果缓存中没有 key 为 #uid 的数据就执行 detail 函数并且把后果放到缓存中
  • 如果缓存中存在 key 为 #uid 的数据就间接返回,不执行 detail 函数

通过 Cacheable 咱们能够十分不便的在代码中应用缓存,那么 Cacheable 是如何实现的,一开始认为是通过 AOP 实现,然而通过查看源码,发现跟 AOP 又有点不一样。

Cacheable 原理

如果要应用 Cacheable 就必须在启动类上加上@EnableCaching(),该注解定义如下

@Import(CachingConfigurationSelector.class)
public @interface EnableCaching {...}

CachingConfigurationSelector 继承了 AdviceModeImportSelector,次要看 selectImports 办法

public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching>{
  //.....
  @Override
    public String[] selectImports(AdviceMode adviceMode) {switch (adviceMode) {
            case PROXY:
                return getProxyImports();
            case ASPECTJ:
                return getAspectJImports();
            default:
                return null;
        }
    }
  
  private String[] getProxyImports() {List<String> result = new ArrayList<>(3);
        result.add(AutoProxyRegistrar.class.getName());
        result.add(ProxyCachingConfiguration.class.getName());
        if (jsr107Present && jcacheImplPresent) {result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
        }
        return StringUtils.toStringArray(result);
    }
}
  • 判断实现模式是基于代理 (PROXY) 还是 ASPECTJ,Spring-AOP 模式应用时代理模式,所以这边会走到 getProxyImports 这里
  • getProxyImports 中退出两个代理类,咱们次要看 ProxyCachingConfiguration
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class ProxyCachingConfiguration extends AbstractCachingConfiguration {@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
        advisor.setCacheOperationSource(cacheOperationSource());
        advisor.setAdvice(cacheInterceptor());
        if (this.enableCaching != null) {advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
        }
        return advisor;
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheOperationSource cacheOperationSource() {return new AnnotationCacheOperationSource();
    }

    @Bean
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public CacheInterceptor cacheInterceptor() {CacheInterceptor interceptor = new CacheInterceptor();
        interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
        interceptor.setCacheOperationSource(cacheOperationSource());
        return interceptor;
    }

}

这里创立了几个类,咱们重点关注以下两个类

  • cacheAdvisor:缓存加强类,能够了解为 AOP 中的 Aspect,能够定义切点 (Pointcut) 和告诉(advice)
  • cacheInterceptor:缓存中断器,缓存逻辑的具体执行,能够了解为 AOP 中的告诉(advice)

BeanFactoryCacheOperationSourceAdvisor局部代码如下

public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {

    @Nullable
    private CacheOperationSource cacheOperationSource;

    private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
        @Override
        @Nullable
        protected CacheOperationSource getCacheOperationSource() {return cacheOperationSource;}
    };
  //....
}

在这里定义了一个切点(pointcut),加上下面代码中定义了一个告诉

advisor.setAdvice(cacheInterceptor());

所以这就是一个残缺的 Aspect,切点负责定义拦挡的类,CacheOperationSourcePointcut 局部代码如下

abstract class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable {

    @Override
    public boolean matches(Method method, Class<?> targetClass) {if (CacheManager.class.isAssignableFrom(targetClass)) {return false;}
        CacheOperationSource cas = getCacheOperationSource();
        return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
    }
  //...
}

其中 matchs 就是负责过滤的类和办法,如果返回 true 那么该办法就会被拦挡,拦挡形式对应 AOP 中的Around,具体过滤规定咱们就不持续往下看,总之 Cacheable 的实现能够概括如下

定义 Advisor-> 定义中断(interceptor)-> 定义切点(Pointcut)

那么接下来咱们模拟 Cacheable 实现日志的打印,在办法进入前打印日志,在办法执行后打印日志

模拟 Cacheable 实现日志打印

  • 定义注解,作用相似 Cacheable

Logable.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Logable {String value() default "";
}
  • 定义配置类,为了简略这里就不采纳 EnableCache 的形式,间接定义配置类

LogProxyConfiguration.java

@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class LogProxyConfiguration {@Bean(name = "com.poc.aop.log.LogAdvisor")
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public LogAdvisor logAdvisor() {LogAdvisor advisor = new LogAdvisor();
        advisor.setAdvice(new LogInterceptor());
        return advisor;
    }
}
  • 定义日志加强类 LogAdvisor

LogAdvisor.java

public class LogAdvisor extends AbstractBeanFactoryPointcutAdvisor {LogPointcut pointcut=new LogPointcut();
    @Override
    public Pointcut getPointcut() {return pointcut;}
}
  • 定义日志切点

LogPointcut.java

public class LogPointcut extends StaticMethodMatcherPointcut {
    @Override
    public boolean matches(Method method, Class<?> targetClass) {return method.getAnnotation(Logable.class) != null;
    }
}

这里逻辑判断很简略,只有带有 Logable 的办法就会被拦挡

  • 定义日志办法中断,也就是告诉 advice
public class LogInterceptor implements MethodInterceptor {
    @Override
    public Object invoke(MethodInvocation invocation) throws Throwable {System.out.println("执行办法 ==>" + invocation.getMethod().getName());
        System.out.println("办法参数:");
        for (Object arg : invocation.getArguments()) {System.out.println("参数:" + arg);
        }
        Object returnValue = invocation.proceed();
        System.out.println("返回值 ===>" + returnValue);
        return returnValue;
    }
}

测试

咱们筹备一个测试接口

@GetMapping(value = "/user/detail")
@Logable
public User deteail(@RequestParam(value = "uid") String uid) {User u = new User();
    u.setUid(uid);
    u.setAge(10);
    u.setEmail("jianfeng.zheng@definesys.com");
    u.setBirthday(Calendar.getInstance().getTime());
    return u;
}

申请接口,进入 LogInterceptor.invoke 办法,打印如下

执行办法 ==>deteail
办法参数:
参数:004
返回值 ===>User{uid='004', name='null', email='jianfeng.zheng@definesys.com', birthday=Mon Mar 15 19:32:22 CST 2021, age=10}

用 Spring AOP 能不能实现

当然是能够的,只有编写一个 Aspect 类就行

@Aspect
@Component
public class LogAspect {@Pointcut("@annotation(com.poc.aop.log.Logable)")
    public void logPointCut() {}

    @Around("logPointCut()")
    public void aroundAdvice(ProceedingJoinPoint joinPoint) {System.out.println("执行办法 ==>" + joinPoint.getSignature().getName());
        System.out.println("办法参数:");
        for (Object arg : joinPoint.getArgs()) {System.out.println("参数:" + arg);
        }
        Object returnValue = null;
        try {returnValue = joinPoint.proceed();
        } catch (Throwable throwable) {throwable.printStackTrace();
        }
        System.out.println("返回值 ===>" + returnValue);
    }
}

一样的成果,那为什么 Cacheable 要应用下面那种形式?我猜是因为 advisor 这种代理的形式切点灵活性更高,如下

public boolean matches(Method method, Class<?> targetClass) 

能够依据 method 和 targetClass 灵便定义切点,当然我还是更喜爱 Aspect 的形式

退出移动版