在 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<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 {
    // ...
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,185评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,445评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,684评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,564评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,681评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,874评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,025评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,761评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,217评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,545评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,694评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,351评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,988评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,778评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,007评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,427评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,580评论 2 349

推荐阅读更多精彩内容