在 SpringBoot 中使用 AOP

本文将学习如何在 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 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 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 redisTemplate(RedisConnectionFactory factory) {
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }
}

CacheAspect 中的代码更换为 Redis 的。类似于 HashMap,Redis的数据操作为: RedisTemplate.opsForValue() 的get和set方法用于取值和设置值。

@Aspect
@Service
public class CacheAspect {
    @Autowired
    RedisTemplate 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 {
    // ...
}

你可能感兴趣的:(在 SpringBoot 中使用 AOP)