本文将学习如何在 SpringBoot 使用 AOP 拦截一个类的方法,以及如何使用 Redis 实现缓存。本文将使用《SpringBoot MyBatis + 页面渲染》中排行榜的例子。实现的东西很简单,就是给 RankService.getRank()
方法加上一个缓存功能。分为两步走,一是基于内存的缓存,二是会初步使用Redis实现缓存。如果你还不知道 AOP 是什么,欢迎阅读《Java AOP与装饰器模式》。
使用AOP实现基于内存的缓存
和所有 SpringBoot 引入依赖的方式相同,我们需要一个 spring-boot-starter-aop。我们要做的是拦截 RankService.getRank 方法,并给其加上一个缓存。我们在《Java AOP与装饰器模式》这篇文章中提到 JDK 动态代理只适用于接口,但是这里很明显是个类的方法。所以我们需要考虑一个问题:Spring 是如何切换 JDK 动态代理和 CGLIB 的?答案是:使用 spring.aop.proxy-target-class=true
这样一条配置。不过我在官网没有找到这样的写法,先附上一个有提到这条配置的链接以及一篇文章作为考正。
@Service
public class RankService {
@Autowired
private RankDao rankDao;
public List<RankItem> getRank() {
return rankDao.getRank();
}
}
我们需要去声明一个切面 CacheAspect
类,在这个类中完成相应的功能。这个类上需要声明有 @Aspect
和 一个让 Spring 能够识别的注解包括@Service
,@Component
,@Configuration
,这些都是可以的(因为我试过)。
缓存是怎么做的呢?一般我们是根据注解,所以我们还需要声明一个注解 @Cahce
。接下来,我们要考虑的就是让每一个标注了 @Cache
注解的方法,都进入到 CacheAspect
中来。我们可以定义很多种切面,即 @Aspect
声明切⾯有很多种,包括 @Before
,@After
,@Around
,根据字面意思也很容易知道它们在做什么,无非是在方法前、后乃至包裹住方法,做些什么事。方法参数很奇怪,是 ProceedingJoinPoint
,这里做一个简单的解释。JoinPoint
对象封装了 SpringAop
中切面方法的信息,在切面方法中添加 JoinPoint
参数,就可以获取到封装了该方法信息的 JoinPoint
对象。ProceedingJoinPoint
对象是 JoinPoint
的子接口,该对象只用在 @Around
的切面方法中。添加了以下两个方法:
Object proceed() throws Throwable //执行目标方法
Object proceed(Object[] var1) throws Throwable //传入的新的参数去执行目标方法
@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 {
}
基本的拦截生效后,我们考虑给它上一个基于内存的缓存。这里只是实现一个简答的缓存,现实中我们可能还要考虑方法的参数等等。如何拿到方法名呢?需要按照以下的写法,背一背 API 就行了。以下是 JoinPoint 的常用 API:
方法名 | 功能 |
---|---|
Signature getSignature(); | 获取封装了署名信息的对象,在该对象中可以获取到目标方法名,所属类的Class等信息 |
Object[] getArgs(); | 获取传入目标方法的参数对象 |
Object getTarget(); | 获取被代理的对象 |
Object getThis(); | 获取代理对象 |
@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实现基于Redis的缓存
Redis是世界上广泛使用的基于内存的缓存,Redis为什么这么快呢?有以下几点原因:
- 完全基于内存
- 优秀的数据结构设计
- 单一线程,避免上下文切换开销
- 事件驱动,非阻塞。其他的缓存系统可能需要轮询网络io或是一些文件描述符
直接基于内存也能做缓存,我们为什么需要 Redis 呢?生产环境中,一般都是分布式部署的,如果直接做内存缓存,每一个 JVM 都有一套属于自己的内存缓存。如何让所有 JVM 共享一个共用的缓存呢?Redis 最大的意义就在于此。
接下来,是一些基本的初始化操作。我们使用 docker 启动一个 Redis。补充 Redis 的配置。引入关于 Redis 的 spring-boot-redis-data-starter 依赖。
docker run -p 6379:6379 -d redis
spring.redis.host=localhost
spring.redis.port=6379
我们在 AppConfig 中声明 Redis。我们需要一个RedisTemplate 用于和Redis交互。用 SpringBoot 的好处也在于,我们根本不用考虑 RedisConnectionFactory
这个类到底在哪。SpringBoot 会帮我们自动装配。
@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的数据操作为: RedisTemplate.opsForValue() 的get和set方法用于取值和设置值。
@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;
}
}
这时候报错了,如下图所示。说的是 RankItem
没有办法序列化。这是什么意思呢?和 Redis 打交道的时候,它使用通过网络通信,传递的是字节流。我们怎么把一个 Java 对象传递给 Redis 呢?所以我们要把一个 Java 对象变成字节流,这个过程就是序列化。Redis 默认的序列化的库是 Java 自带的序列化工具,Serializable
接口。任何一个类只要实现了 java.io.Serializable
这个接口,就可以开启序列化,使得这个 Java 对象可以自动的变成字节流。所以解决办法很简单,我们只要让 RankItem
实现这个接口就可以了。
public class RankItem implements Serializable {
// ...
}