美文网首页
在 SpringBoot 中使用 AOP

在 SpringBoot 中使用 AOP

作者: BitterOutsider | 来源:发表于2021-01-07 20:49 被阅读0次

    本文将学习如何在 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 {
        // ...
    }
    

    相关文章

      网友评论

          本文标题:在 SpringBoot 中使用 AOP

          本文链接:https://www.haomeiwen.com/subject/wsqboktx.html