在 SpringBoot 中使用 AOP

本文將學(xué)習(xí)如何在 SpringBoot 使用 AOP 攔截一個(gè)類的方法榆俺,以及如何使用 Redis 實(shí)現(xiàn)緩存。本文將使用《SpringBoot MyBatis + 頁面渲染》中排行榜的例子跪削。實(shí)現(xiàn)的東西很簡(jiǎn)單谴仙,就是給 RankService.getRank() 方法加上一個(gè)緩存功能。分為兩步走碾盐,一是基于內(nèi)存的緩存晃跺,二是會(huì)初步使用Redis實(shí)現(xiàn)緩存。如果你還不知道 AOP 是什么毫玖,歡迎閱讀《Java AOP與裝飾器模式》掀虎。

使用AOP實(shí)現(xiàn)基于內(nèi)存的緩存

和所有 SpringBoot 引入依賴的方式相同,我們需要一個(gè) spring-boot-starter-aop付枫。我們要做的是攔截 RankService.getRank 方法烹玉,并給其加上一個(gè)緩存。我們?cè)?a href="http://www.reibang.com/p/b4b86e9c25c2" target="_blank">《Java AOP與裝飾器模式》這篇文章中提到 JDK 動(dòng)態(tài)代理只適用于接口阐滩,但是這里很明顯是個(gè)類的方法二打。所以我們需要考慮一個(gè)問題:Spring 是如何切換 JDK 動(dòng)態(tài)代理和 CGLIB 的?答案是:使用 spring.aop.proxy-target-class=true 這樣一條配置掂榔。不過我在官網(wǎng)沒有找到這樣的寫法继效,先附上一個(gè)有提到這條配置的鏈接以及一篇文章作為考正症杏。

@Service
public class RankService {
    @Autowired
    private RankDao rankDao;

    public List<RankItem> getRank() {
        return rankDao.getRank();
    }
}

我們需要去聲明一個(gè)切面 CacheAspect 類,在這個(gè)類中完成相應(yīng)的功能瑞信。這個(gè)類上需要聲明有 @Aspect 和 一個(gè)讓 Spring 能夠識(shí)別的注解包括@Service厉颤,@Component@Configuration凡简,這些都是可以的(因?yàn)槲以囘^)逼友。
緩存是怎么做的呢?一般我們是根據(jù)注解秤涩,所以我們還需要聲明一個(gè)注解 @Cahce帜乞。接下來,我們要考慮的就是讓每一個(gè)標(biāo)注了 @Cache 注解的方法溉仑,都進(jìn)入到 CacheAspect 中來挖函。我們可以定義很多種切面状植,即 @Aspect 聲明切?有很多種浊竟,包括 @Before@After津畸,@Around振定,根據(jù)字面意思也很容易知道它們?cè)谧鍪裁矗瑹o非是在方法前肉拓、后乃至包裹住方法后频,做些什么事。方法參數(shù)很奇怪暖途,是 ProceedingJoinPoint卑惜,這里做一個(gè)簡(jiǎn)單的解釋。JoinPoint 對(duì)象封裝了 SpringAop 中切面方法的信息驻售,在切面方法中添加 JoinPoint 參數(shù)露久,就可以獲取到封裝了該方法信息的 JoinPoint 對(duì)象。ProceedingJoinPoint 對(duì)象是 JoinPoint 的子接口欺栗,該對(duì)象只用在 @Around的切面方法中毫痕。添加了以下兩個(gè)方法:

Object proceed() throws Throwable //執(zhí)行目標(biāo)方法 
Object proceed(Object[] var1) throws Throwable //傳入的新的參數(shù)去執(zhí)行目標(biāo)方法
@Aspect
@Service
public class CacheAspect {
    @Around("@annotation(hello.anno.Cache)")
    public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
        System.out.println("method is called");
        return joinPoint.proceed();
    }
}
@Retention(RetentionPolicy.RUNTIME)
public @interface Cache {
}

基本的攔截生效后,我們考慮給它上一個(gè)基于內(nèi)存的緩存迟几。這里只是實(shí)現(xiàn)一個(gè)簡(jiǎn)答的緩存消请,現(xiàn)實(shí)中我們可能還要考慮方法的參數(shù)等等。如何拿到方法名呢类腮?需要按照以下的寫法臊泰,背一背 API 就行了。以下是 JoinPoint 的常用 API:

方法名 功能
Signature getSignature(); 獲取封裝了署名信息的對(duì)象蚜枢,在該對(duì)象中可以獲取到目標(biāo)方法名缸逃,所屬類的Class等信息
Object[] getArgs(); 獲取傳入目標(biāo)方法的參數(shù)對(duì)象
Object getTarget(); 獲取被代理的對(duì)象
Object getThis(); 獲取代理對(duì)象
@Aspect
@Service
public class CacheAspect {
    private final Map<String, Object> cache = new HashMap<>();

    @Around("@annotation(hello.anno.Cache)")
    public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getName();
        Object cacheValue = this.cache.get(methodName);

        if (cacheValue == null) {
            System.out.println("get result from database");
            cacheValue = joinPoint.proceed();
            cache.put(methodName, cacheValue);
        } else {
            System.out.println("get result from cache");
        }
        return cacheValue;
    }
}

使用AOP實(shí)現(xiàn)基于Redis的緩存

Redis是世界上廣泛使用的基于內(nèi)存的緩存七婴,Redis為什么這么快呢?有以下幾點(diǎn)原因:

  • 完全基于內(nèi)存
  • 優(yōu)秀的數(shù)據(jù)結(jié)構(gòu)設(shè)計(jì)
  • 單一線程察滑,避免上下文切換開銷
  • 事件驅(qū)動(dòng)打厘,非阻塞。其他的緩存系統(tǒng)可能需要輪詢網(wǎng)絡(luò)io或是一些文件描述符

直接基于內(nèi)存也能做緩存贺辰,我們?yōu)槭裁葱枰?Redis 呢户盯?生產(chǎn)環(huán)境中,一般都是分布式部署的饲化,如果直接做內(nèi)存緩存莽鸭,每一個(gè) JVM 都有一套屬于自己的內(nèi)存緩存。如何讓所有 JVM 共享一個(gè)共用的緩存呢吃靠?Redis 最大的意義就在于此硫眨。


接下來,是一些基本的初始化操作巢块。我們使用 docker 啟動(dòng)一個(gè) Redis礁阁。補(bǔ)充 Redis 的配置。引入關(guān)于 Redis 的 spring-boot-redis-data-starter 依賴族奢。

docker run -p 6379:6379 -d redis
spring.redis.host=localhost
spring.redis.port=6379

我們?cè)?AppConfig 中聲明 Redis姥闭。我們需要一個(gè)RedisTemplate 用于和Redis交互。用 SpringBoot 的好處也在于越走,我們根本不用考慮 RedisConnectionFactory 這個(gè)類到底在哪棚品。SpringBoot 會(huì)幫我們自動(dòng)裝配。

@Configuration
public class AppConfig {
    @Bean
    RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

CacheAspect 中的代碼更換為 Redis 的廊敌。類似于 HashMap铜跑,Redis的數(shù)據(jù)操作為: RedisTemplate.opsForValue() 的get和set方法用于取值和設(shè)置值。

@Aspect
@Service
public class CacheAspect {
    @Autowired
    RedisTemplate<String, Object> redisTemplate;

    @Around("@annotation(hello.anno.Cache)")
    public Object cache(ProceedingJoinPoint joinPoint) throws Throwable {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String methodName = signature.getName();
        Object cacheValue = redisTemplate.opsForValue().get(methodName);

        if (cacheValue == null) {
            System.out.println("get result from database");
            cacheValue = joinPoint.proceed();
            redisTemplate.opsForValue().set(methodName, cacheValue);
        } else {
            System.out.println("get result from cache");
        }
        return cacheValue;
    }
}

這時(shí)候報(bào)錯(cuò)了骡澈,如下圖所示锅纺。說的是 RankItem 沒有辦法序列化。這是什么意思呢秧廉?和 Redis 打交道的時(shí)候伞广,它使用通過網(wǎng)絡(luò)通信,傳遞的是字節(jié)流疼电。我們?cè)趺窗岩粋€(gè) Java 對(duì)象傳遞給 Redis 呢嚼锄?所以我們要把一個(gè) Java 對(duì)象變成字節(jié)流,這個(gè)過程就是序列化蔽豺。Redis 默認(rèn)的序列化的庫是 Java 自帶的序列化工具区丑,Serializable 接口。任何一個(gè)類只要實(shí)現(xiàn)了 java.io.Serializable 這個(gè)接口,就可以開啟序列化沧侥,使得這個(gè) Java 對(duì)象可以自動(dòng)的變成字節(jié)流可霎。所以解決辦法很簡(jiǎn)單,我們只要讓 RankItem 實(shí)現(xiàn)這個(gè)接口就可以了宴杀。

public class RankItem implements Serializable {
    // ...
}
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末癣朗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子旺罢,更是在濱河造成了極大的恐慌旷余,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,042評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扁达,死亡現(xiàn)場(chǎng)離奇詭異正卧,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)跪解,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,996評(píng)論 2 384
  • 文/潘曉璐 我一進(jìn)店門炉旷,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人叉讥,你說我怎么就攤上這事窘行。” “怎么了节吮?”我有些...
    開封第一講書人閱讀 156,674評(píng)論 0 345
  • 文/不壞的土叔 我叫張陵抽高,是天一觀的道長。 經(jīng)常有香客問我透绩,道長,這世上最難降的妖魔是什么壁熄? 我笑而不...
    開封第一講書人閱讀 56,340評(píng)論 1 283
  • 正文 為了忘掉前任帚豪,我火速辦了婚禮,結(jié)果婚禮上草丧,老公的妹妹穿的比我還像新娘狸臣。我一直安慰自己,他們只是感情好昌执,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,404評(píng)論 5 384
  • 文/花漫 我一把揭開白布啄清。 她就那樣靜靜地躺著脂矫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上港准,一...
    開封第一講書人閱讀 49,749評(píng)論 1 289
  • 那天,我揣著相機(jī)與錄音映挂,去河邊找鬼鞭莽。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的选脊。 我是一名探鬼主播杭抠,決...
    沈念sama閱讀 38,902評(píng)論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼恳啥!你這毒婦竟也來了偏灿?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,662評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤钝的,失蹤者是張志新(化名)和其女友劉穎菩混,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扁藕,經(jīng)...
    沈念sama閱讀 44,110評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡沮峡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,451評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了亿柑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片邢疙。...
    茶點(diǎn)故事閱讀 38,577評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖望薄,靈堂內(nèi)的尸體忽然破棺而出疟游,到底是詐尸還是另有隱情,我是刑警寧澤痕支,帶...
    沈念sama閱讀 34,258評(píng)論 4 328
  • 正文 年R本政府宣布颁虐,位于F島的核電站,受9級(jí)特大地震影響卧须,放射性物質(zhì)發(fā)生泄漏另绩。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,848評(píng)論 3 312
  • 文/蒙蒙 一花嘶、第九天 我趴在偏房一處隱蔽的房頂上張望笋籽。 院中可真熱鬧,春花似錦椭员、人聲如沸车海。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,726評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽侍芝。三九已至,卻和暖如春埋同,著一層夾襖步出監(jiān)牢的瞬間州叠,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,952評(píng)論 1 264
  • 我被黑心中介騙來泰國打工莺禁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留留量,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,271評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像楼熄,于是被迫代替她去往敵國和親忆绰。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,452評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容