之前说到缓存的管理问题,具体看redis缓存管理问题,我想要实现缓存的逻辑在事务提交之后,考虑使用事务监听器,这个我之前也用过,使用监听器实现在事务提交后发送消息,那么问题是我如何拦截到注解,然后发出事件。
有两种方案,一是使用自定义注解,然后用aop自己实现一整套缓存体系,但是有一个我之前就遇到过的问题,就是aop在接口上不起效,而spring-data-jpa的dao层都是直接用接口的,为了缓存而改换整个查询体系成本太大,也是不现实的,或者我可以对应每个dao封装一层service,然后把注解加在service实现类上,很麻烦,改动量极大,而且很不优雅,破坏了架构。
那么回过头来,springcache是怎么实现在接口层上加注解的呢?只要知道了原理我就可以对其模仿。
看完之后原来就是CacheInterceptor实现了MethodInterceptor接口,一个拦截器接口,能够拦截到所有方法的执行,在初始化阶段就扫描所有的方法,把对应的缓存注解进行管理,拦截到方法的时候,判断该方法上有没有注解就行了。实际上是很简单的原理,而且拦截器会拦截所有的方法,跟spring-aop没什么关系。
这就是拦截器,然后核心代码在CacheAspectSupport的execute方法里面
@SuppressWarnings("serial")
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
@Override
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() {
@Override
public Object invoke() {
try {
return invocation.proceed();
}
catch (Throwable ex) {
throw new ThrowableWrapper(ex);
}
}
};
try {//继承自CacheAspectSupport类
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
catch (CacheOperationInvoker.ThrowableWrapper th) {
throw th.getOriginal();
}
}
}
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically)
if (this.initialized) {
Class> targetClass = getTargetClass(target);
Collection operations = getCacheOperationSource().getCacheOperations(method, targetClass);
if (!CollectionUtils.isEmpty(operations)) {//判断该方法是否存在缓存注解
return execute(invoker, method, new CacheOperationContexts(operations, method, args, target, targetClass));
}
}
return invoker.invoke();
}
那么接下来有两种办法,一个是我修改源码然后打包,另一个方法是我进行配置,用自己的类代替关键的业务代码。
这是关键的配置类
@Configuration
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());
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.setCacheOperationSources(cacheOperationSource());
if (this.cacheResolver != null) {
interceptor.setCacheResolver(this.cacheResolver);
}
else if (this.cacheManager != null) {
interceptor.setCacheManager(this.cacheManager);
}
if (this.keyGenerator != null) {
interceptor.setKeyGenerator(this.keyGenerator);
}
if (this.errorHandler != null) {
interceptor.setErrorHandler(this.errorHandler);
}
return interceptor;
}
}
在这一步折腾了很久,刚开始我想用@Bean直接配置CacheInterceptor,但是总是报各种奇怪的错误或者是Bean重名之类的,让我很奇怪,因为我对springboot的配置原理一知半解,如果不能进行同名替换,那么@Bean是如何进行配置的。我不得不去探寻springboot的配置原理,至少知道配置路径把,如此这般倒是有所收获。springboot——自动配置。
倒是知道了springboot自动配置的入口,但是我在spring.factories中并没有找到ProxyCachingConfiguration,后来发现实际上入口在@EnableCaching里,EnableCachine导入了 CachingConfigurationSelector
@Override
public String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return getProxyImports();
case ASPECTJ:
return getAspectJImports();
default:
return null;
}
}
/**
* Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#PROXY}.
* Take care of adding the necessary JSR-107 import if it is available.
*/
private String[] getProxyImports() {
List result = new ArrayList();
result.add(AutoProxyRegistrar.class.getName());
result.add(ProxyCachingConfiguration.class.getName());//导入缓存配置
if (jsr107Present && jcacheImplPresent) {
result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
}
return result.toArray(new String[result.size()]);
}
于是我有了一个大胆的想法,那就是自己写一个@EnabledCaching类和CachineConfigurationSelector,导入自己的ProxyCachineConfiguration类,最后导入自己写的CacheInterceptor,其他的部分就继续导入它已有的类。最后实践证明我成功了。这一部分就不展开了,除了以上的几个类,只要有编译错误的类就自己写一个就行。这样算不算变相地修改源码?
然后是核心代码,CacheAspectSupport类中
private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
logger.info("=====================myIntercepter=======================");
// Special handling of synchronized invocation
if (contexts.isSynchronized()) {
CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
Cache cache = context.getCaches().iterator().next();
try {
return wrapCacheValue(method, cache.get(key, new Callable
我把处理缓存的逻辑换成了发出事件,并把相关的参数传进去,注意里面有些类在源代码中是私有的,我给改成公有的了,不然传不出去
public class CacheDTO {
private Object cacheValue;
private CacheAspectSupport cacheAspectSupport;
private CacheOperationContexts contexts;
public CacheDTO(Object cacheValue) {
super();
this.cacheValue = cacheValue;
}
public CacheDTO(Object cacheValue) {
super();
this.cacheValue = cacheValue;
}
public CacheDTO(Object cacheValue, CacheAspectSupport cacheAspectSupport, CacheOperationContexts contexts) {
super();
this.cacheValue = cacheValue;
this.cacheAspectSupport = cacheAspectSupport;
this.contexts = contexts;
}
public CacheDTO() {
super();
// TODO Auto-generated constructor stub
}
public Object getCacheValue() {
return cacheValue;
}
public void setCacheValue(Object cacheValue) {
this.cacheValue = cacheValue;
}
public CacheAspectSupport getCacheAspectSupport() {
return cacheAspectSupport;
}
public void setCacheAspectSupport(CacheAspectSupport cacheAspectSupport) {
this.cacheAspectSupport = cacheAspectSupport;
}
public CacheOperationContexts getContexts() {
return contexts;
}
public void setContexts(CacheOperationContexts contexts) {
this.contexts = contexts;
}
}
@Component
public class MachineTransactionalEventListener{
Logger logger = LoggerFactory.getLogger(this.getClass());
@TransactionalEventListener(fallbackExecution = true,phase=TransactionPhase.AFTER_COMMIT)
public void handleCachePut(CacheDTO cache) throws Exception{
logger.info("====================handleCachePut=======================");
CacheOperationContexts contexts = cache.getContexts();
List cachePutRequests = new LinkedList();
Object cacheValue = cache.getCacheValue();
//CachePut和CacheEvict在事务提交之后执行 cache.getCacheAspectSupport().collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(cacheValue);
}
// Process any late evictions
cache.getCacheAspectSupport().processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
}
}
最后成功实现了在事务后提交缓存的功能。这次收获还是比较大,清楚了springboot的配置入口,修改了源代码,有了这次的经验未来修改源码就更有底气了。