背景
在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{
//.....
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
private String[] getProxyImports() {
List 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.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("[email protected]");
u.setBirthday(Calendar.getInstance().getTime());
return u;
}
请求接口,进入LogInterceptor.invoke方法,打印如下
执行方法==>deteail
方法参数:
参数:004
返回值===>User{uid='004', name='null', email='[email protected]', 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的方式