本文將學(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 {
// ...
}