記一次SpringAOP實踐過程-包掃描和巢狀註解
每一次實踐得出結論,得出的對過往理論的印證,都是一次悟道,其收益遠大於爭論和抱怨。
技術是一件比較客觀的事,正確與錯誤,其實就擺在哪裡,意見不統一,寫段程式碼試驗一下就好了,一段程式碼印證不了的時候,就多寫幾段。
先同一個案例說起
挺簡單的一個案例,通過SpringAOP和註解,使用Guava快取。程式碼如下:
GuavaCache.java
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface GuavaCache { /** * group : 一個group代表一個cache,不傳或者""則使用 class+method 作為cache名字 * @return */ public String group() default ""; /** * key : 注意,所有引數必須實現GuavaCacheInterface介面,如果不實現,則會用toString()的MD5作為Key * @return */ public String key() default ""; /** * 過期時間,預設30秒 * @return */ public long timeout() default 30; /** * 快取最大條目,預設10000 * @return */ public long size() default 10000; /** * 是否列印日誌 * @return */ public boolean debug() default false; }
GuavaInterface.java
/**
* 使用GuavaCache註解時,如果傳入引數是物件,則必須實現這個類
*
*/
public interface GuavaCacheInterface {
public String getCacheKey();
}
GuavaCacheProcessor.javaimport java.lang.reflect.Method; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.concurrent.TimeUnit; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.collect.Maps; /** * GuavaCache註解處理器 * */ @Component @Aspect public class GuavaCacheProcessor { private static final Logger logger = LoggerFactory.getLogger(GuavaCacheProcessor.class); private static Map<String, Cache<String, Object>> cacheMap = Maps.newConcurrentMap(); @Around("execution(* *(..)) && @annotation(guavaCache)") public Object aroundMethod(ProceedingJoinPoint pjd, GuavaCache guavaCache) throws Throwable { Cache<String, Object> cache = getCache(pjd, guavaCache); String key = getKey(pjd); boolean keyisnull = (null == key) || ("".equals(key)); if(guavaCache.debug()) { logger.info("GuavaCache key : {} begin", key); } Object result = null; if(!keyisnull) { result = cache.getIfPresent(key); if(result != null) { return result; } } try { result = pjd.proceed(); if(!keyisnull) { cache.put(key, result); } } catch (Exception e) { throw e; } if(guavaCache.debug()) { logger.info("GuavaCache key : {} end", key); } return result; } /** * 獲取Cache * @param pjd * @param guavaCache * @return */ private Cache<String, Object> getCache(ProceedingJoinPoint pjd, GuavaCache guavaCache) { String group = guavaCache.group(); if(group == null || "".equals(group)) { MethodSignature signature = (MethodSignature) pjd.getSignature(); Method method = signature.getMethod(); Class<?> clazz = method.getDeclaringClass(); group = clazz.getName(); } Cache<String, Object> cache = cacheMap.get(group); if(cache == null) { cache = CacheBuilder.newBuilder() .maximumSize(guavaCache.size()) .expireAfterWrite(guavaCache.timeout(), TimeUnit.SECONDS) .build(); cacheMap.put(group, cache); } return cache; } /** * 獲取Key:方法名+getCacheKey方法(如果沒有,則用toString())的MD5值 * @param pjd * @return */ private String getKey(ProceedingJoinPoint pjd) { StringBuilder sb = new StringBuilder(); MethodSignature signature = (MethodSignature) pjd.getSignature(); Method method = signature.getMethod(); sb.append(method.getName()); for(Object param : pjd.getArgs()) { if(GuavaCacheInterface.class.isAssignableFrom(param.getClass())) { sb.append(((GuavaCacheInterface)param).getCacheKey()); } else { if(!param.getClass().isPrimitive()) { return null; } sb.append(param.toString()); } } String key = md5(sb.toString()); return key; } /** * 進行MD5加密 * * @param info * 要加密的資訊 * @return String 加密後的字串 */ private static String md5(String info) { byte[] digesta = null; try { // 得到一個md5的訊息摘要 MessageDigest alga = MessageDigest.getInstance("MD5"); // 新增要進行計算摘要的資訊 alga.update(info.getBytes()); // 得到該摘要 digesta = alga.digest(); } catch (NoSuchAlgorithmException e) { e.printStackTrace(); } // 將摘要轉為字串 String rs = byte2hex(digesta); return rs; } /** * 將二進位制轉化為16進位制字串 * * @param b * 二進位制位元組陣列 * @return String */ private static String byte2hex(byte[] b) { String hs = ""; String stmp = ""; for (int n = 0; n < b.length; n++) { stmp = (java.lang.Integer.toHexString(b[n] & 0XFF)); if (stmp.length() == 1) { hs = hs + "0" + stmp; } else { hs = hs + stmp; } } return hs.toUpperCase(); } }
遇到的第一個問題
問題出現
當bean在applicationContext.xml中通過<bean...>定義時,註解正常;當通過@Service定義時,註解失效。
初步解決
把對於AOP的定義<aop:aspectj-autoproxy proxy-target-class="true" />移到servlet-context.xml(這是controller定義檔案)中,OK了。
期間的胡思亂想
- context:component-scan 會不會不掃依賴的jar中的bean(因為註解時在依賴的jar包中定義的)
- 會不會掃描有順序,先掃自己的,再掃依賴的jar的,由於有先後順序,導致Spring載入Service類時需要的註解類先沒掃到
- <aop:aspectj-autoproxy>會不會因為是Web應用,所以就需要在servlet-context.xml中定義呢?
找到真實的原因
類載入兩次
監測Service類和註解處理類(通過在建構函式增加System.out),發現類被載入兩次,疑惑了一下,載入兩次?
看applicationContext.xml,配置正常;看servlet-context.xml,居然掃描的不僅僅是controller類,而是把所有的類都掃描了一遍,這就解釋了,為什麼<aop:aspectj-autoproxy proxy-target-class="true" />配置在servlet-context.xml中,aop才生效。
因為根據web.xml的載入順序:context-param>listener>filter>servlet,servlet-context.xml是最後載入的,spring又掃描了一遍,如果不寫<aop:...>,就會載入沒有aop的類。
<aop:aspectj-autoproxy proxy-target-class="true" />在哪裡生效
這個配置,配置在哪個檔案中,就會對哪個檔案scan的類生效。
解答胡思亂想
看似詭異的問題,更大的可能是我們瞭解的不夠透徹,不夠深入;如果規範一點,詭異的問題往往都不出現。
- 會掃描依賴的jar,根據base-package的定義
- 有先後順序,但是掃描某個類的時候,如果有依賴的類還沒掃,會馬上掃,所以先後沒有關係(Spring很強,不會這麼弱)
- <aop:aspectj-autoproxy>定義在哪個檔案,就對哪個檔案scan的類生效
第二個問題:巢狀註解
Spring AOP,在同一個類中,巢狀註解時,只對最外層的註解生效,這個有很多文章,解釋的很清楚了,可以參考:
當前SpringAOP的實現,在同一個類中,是不支援巢狀註解的,說不定在以後會實現。
對於巢狀註解,如果一定需要,可以通過aspectJ通過LTW解決,這個我試驗過,確實可以解決,但是成本較高,首先jvm需要增加-javaagent,然後專案中的單元測試之類,都要加這個引數,所以如果不是一定需要,就不要這樣做了,參考文章:
對於巢狀註解,還有一種解決辦法,也是通過aspectJ,在編譯期解決,這個連編譯器都換了,成本太高,沒有去實踐。
備註:
- 對於不同的類之間,巢狀是沒有問題的,這個我們相信Spring
- 在同一個類中,是不支援巢狀註解的,說不定在以後Spring自己就會支援
總結
以上,一次知識總結,沒有放過,沒有看似解決了,就這樣吧,找出了根本問題,很好。