背景

在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")@Logablepublic 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@Componentpublic 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的形式