1. 程式人生 > >記一次SpringAOP實踐過程-包掃描和巢狀註解

記一次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.java
import 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了。

期間的胡思亂想

  1. context:component-scan 會不會不掃依賴的jar中的bean(因為註解時在依賴的jar包中定義的)
  2. 會不會掃描有順序,先掃自己的,再掃依賴的jar的,由於有先後順序,導致Spring載入Service類時需要的註解類先沒掃到
  3. <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的類生效。

解答胡思亂想

看似詭異的問題,更大的可能是我們瞭解的不夠透徹,不夠深入;如果規範一點,詭異的問題往往都不出現。

  1. 會掃描依賴的jar,根據base-package的定義
  2. 有先後順序,但是掃描某個類的時候,如果有依賴的類還沒掃,會馬上掃,所以先後沒有關係(Spring很強,不會這麼弱)
  3. <aop:aspectj-autoproxy>定義在哪個檔案,就對哪個檔案scan的類生效

第二個問題:巢狀註解

Spring AOP,在同一個類中,巢狀註解時,只對最外層的註解生效,這個有很多文章,解釋的很清楚了,可以參考:

當前SpringAOP的實現,在同一個類中,是不支援巢狀註解的,說不定在以後會實現。

對於巢狀註解,如果一定需要,可以通過aspectJ通過LTW解決,這個我試驗過,確實可以解決,但是成本較高,首先jvm需要增加-javaagent,然後專案中的單元測試之類,都要加這個引數,所以如果不是一定需要,就不要這樣做了,參考文章:

對於巢狀註解,還有一種解決辦法,也是通過aspectJ,在編譯期解決,這個連編譯器都換了,成本太高,沒有去實踐。

備註:

  1. 對於不同的類之間,巢狀是沒有問題的,這個我們相信Spring
  2. 在同一個類中,是不支援巢狀註解的,說不定在以後Spring自己就會支援

總結

以上,一次知識總結,沒有放過,沒有看似解決了,就這樣吧,找出了根本問題,很好。