自定義springcache實現事務提交後處理緩存

 

之前說到緩存的管理問題,具體看redis緩存管理問題,我想要實現緩存的邏輯在事務提交之後,考慮使用事務監聽器,這個我之前也用過,使用監聽器實現在事務提交後發送消息,那麼問題是我如何攔截到註解,然後發出事件。

有兩種方案,一是使用自定義註解,然後用aop自己實現一整套緩存體系,但是有一個我之前就遇到過的問題,就是aop在接口上不起效,而spring-data-jpa的dao層都是直接用接口的,爲了緩存而改換整個查詢體系成本太大,也是不現實的,或者我可以對應每個dao封裝一層service,然後把註解加在service實現類上,很麻煩,改動量極大,而且很不優雅,破壞了架構。

那麼回過頭來,springcache是怎麼實現在接口層上加註解的呢?只要知道了原理我就可以對其模仿。

SpringCache實現原理及核心業務邏輯(二)

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<CacheOperation> 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.<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.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}.
	 * <p>Take care of adding the necessary JSR-107 import if it is available.
	 */
	private String[] getProxyImports() {
		List<String> result = new ArrayList<String>();
		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<Object>() {
						@Override
						public Object call() throws Exception {
							return unwrapReturnValue(invokeOperation(invoker));
						}
					}));
				}
				catch (Cache.ValueRetrievalException ex) {
					// The invoker wraps any Throwable in a ThrowableWrapper instance so we
					// can just make sure that one bubbles up the stack.
					throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
				}
			}
			else {
				// No caching required, only call the underlying method
				return invokeOperation(invoker);
			}
		}


		// Process any early evictions
		processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
				CacheOperationExpressionEvaluator.NO_RESULT);

		// Check if we have a cached item matching the conditions
		Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));

		// Collect puts from any @Cacheable miss, if no cached item is found
		List<CachePutRequest> cachePutRequests = new LinkedList<CachePutRequest>();
		if (cacheHit == null) {
			collectPutRequests(contexts.get(CacheableOperation.class),
					CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
		}
		Object cacheValue;
		Object returnValue;

		if (cacheHit != null && cachePutRequests.isEmpty() && !hasCachePut(contexts)) {
			// If there are no put requests, just use the cache hit
			cacheValue = cacheHit.get();
			returnValue = wrapCacheValue(method, cacheValue);
		}
		else {
			// Invoke the method if we don't have a cache hit
			returnValue = invokeOperation(invoker);
			cacheValue = unwrapReturnValue(returnValue);
		}

		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}
        //發佈事務事件
		publisher.publishEvent(new CacheDTO(cacheValue, cachePutRequests, this, contexts));
		// Collect any explicit @CachePuts
//		collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
//		// Process any collected put requests, either from @CachePut or a @Cacheable miss
//		for (CachePutRequest cachePutRequest : cachePutRequests) {
//			cachePutRequest.apply(cacheValue);
//		}
//		// Process any late evictions
//		processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);

		return returnValue;
	}

我把處理緩存的邏輯換成了發出事件,並把相關的參數傳進去,注意裏面有些類在源代碼中是私有的,我給改成公有的了,不然傳不出去

public class CacheDTO {
	private Object cacheValue;
	private List<CachePutRequest> cachePutRequests;
	private CacheAspectSupport cacheAspectSupport;
	private CacheOperationContexts contexts;
	public CacheDTO(Object cacheValue) {
		super();
		this.cacheValue = cacheValue;
	}
	
	public CacheDTO(Object cacheValue, List<CachePutRequest> cachePutRequests) {
		super();
		this.cacheValue = cacheValue;
		this.cachePutRequests = cachePutRequests;
	}
	

	public CacheDTO(Object cacheValue, List<CachePutRequest> cachePutRequests, CacheAspectSupport cacheAspectSupport, CacheOperationContexts contexts) {
		super();
		this.cacheValue = cacheValue;
		this.cachePutRequests = cachePutRequests;
		this.cacheAspectSupport = cacheAspectSupport;
		this.contexts = contexts;
	}

	public CacheDTO() {
		super();
		// TODO Auto-generated constructor stub
	}

	public List<CachePutRequest> getCachePutRequests() {
		return cachePutRequests;
	}

	public void setCachePutRequests(List<CachePutRequest> cachePutRequests) {
		this.cachePutRequests = cachePutRequests;
	}

	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<CachePutRequest> cachePutRequests = cache.getCachePutRequests();
		Object cacheValue = cache.getCacheValue();
		cache.getCacheAspectSupport().collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
		// Process any collected put requests, either from @CachePut or a @Cacheable miss
		for (CachePutRequest cachePutRequest : cachePutRequests) {
			cachePutRequest.apply(cacheValue);
		}
		// Process any late evictions
		cache.getCacheAspectSupport().processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
    }  
}

最後成功實現了在事務後提交緩存的功能。這次收穫還是比較大,清楚了springboot的配置入口,修改了源代碼,有了這次的經驗未來修改源碼就更有底氣了

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章