前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一不小心,弄了一个开源组件:caffeine+redis实现的多级缓存自定义注解组件

一不小心,弄了一个开源组件:caffeine+redis实现的多级缓存自定义注解组件

原创
作者头像
程序员小义
发布2024-08-10 19:44:30
1200
发布2024-08-10 19:44:30
举报
文章被收录于专栏:小义思

大家好,我是小义,这段时间有点事耽搁了,好久没写文章了,今天介绍一下如何构建一个基于springboot实现的自定义starter组件,即一个可灵活配置过期时间的多级缓存框架。

组件化是SpringBoot一大优点。 starter组件是Spring Boot生态系统的一部分,它们帮助开发者快速搭建项目,减少配置的复杂性,并且确保依赖管理的一致性。开发者可以根据自己的需求选择合适的starter来集成到项目中。

尽管各种starters在实现细节上各具特色,但它们普遍遵循两个核心原则:配置属性(ConfigurationProperties)和自动配置(AutoConfiguration)。这一设计哲学源自Spring Boot所倡导的“约定优于配置”(Convention Over Configuration)的理念。

一个简单的starter项目应包含以下目录。

代码语言:java
复制
demo-spring-boot-starter

  |-src

    |-main

      |-java

        |-xxx.xxx 

          |-DemoConfig.java

      |-resource

        |-META-INF

          |-spring.factories

        |-application.properties

  |-pom.xml

自定义starter的步骤如下:

  1. 定义坐标:创建一个新的Maven项目,并定义其坐标(groupId, artifactId, version)。
  2. 编写自动配置类:创建一个带有@Configuration注解的类,实现自定义配置。
  3. 创建META-INF/spring.factories文件:指定自动配置类,让Spring Boot识别。
  4. 打包和发布:将starter下载到本地或发布到Maven仓库。

下面来一一实现。

首先,在spring.factories中,我们指定一下要自动装配的配置类,这样就可以将设置的包扫描路径下的相关bean部署到SpringBoot 中。

代码语言:java
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\

com.xiaoyi.multiTtlCache.config.CustomizedRedisAutoConfiguration

在pom.xml中,需要引入autoconfigure自动装配的maven依赖包。

代码语言:java
复制
<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-autoconfigure</artifactId>

    <version>${spring-boot.version}</version>

</dependency>



<!--spring-boot-configuration-processor的作用是生成配置的元数据信息,即META-INF目录下的spring-configuration-metadata.json文件,从而告诉spring这个jar包中有哪些自定义的配置-->

<dependency>

    <groupId>org.springframework.boot</groupId>

    <artifactId>spring-boot-configuration-processor</artifactId>

    <version>${spring-boot.version}</version>

</dependency>

先设置缓存配置类,类名和spring.factories中的对应上。这其中涉及到本地缓存caffeine和redis缓存配置,关于caffeine的相关内容可以看之前的文章。

代码语言:java
复制
@Configuration

@Import(SpringUtil.class)

@ComponentScan(basePackages = "com.xiaoyi.multiTtlCache")

@EnableCaching

public class CustomizedRedisAutoConfiguration {



    public static final String REDISTEMPLATE\_BEAN\_NAME = "cacheRedisTemplate";



    @Bean(REDISTEMPLATE\_BEAN\_NAME)

    public RedisTemplate<String, Object> cacheRedisTemplate(RedisConnectionFactory redisConnectionFactory){

        RedisTemplate<String, Object> template = new RedisTemplate<>();

        template.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

        ObjectMapper om = new ObjectMapper();

        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);

        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON\_FINAL);

        jackson2JsonRedisSerializer.setObjectMapper(om);

        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        // key采用String的序列化方式

        template.setKeySerializer(stringRedisSerializer);

        // hash的key也采用String的序列化方式

        template.setHashKeySerializer(stringRedisSerializer);

        // value序列化方式采用jackson

        template.setValueSerializer(jackson2JsonRedisSerializer);

        // hash的value序列化方式采用jackson

        template.setHashValueSerializer(jackson2JsonRedisSerializer);

        template.afterPropertiesSet();

        return template;

    }



    @Bean

    public MyCaffeineCache myCaffeineCache() {

        MyCaffeineCache myCaffeineCache = new MyCaffeineCache();

        myCaffeineCache.init();

        return myCaffeineCache;

    }

}

创建一个自定义注解类,添加ttl等时间设置属性。

代码语言:java
复制
@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(RetentionPolicy.RUNTIME)

@Inherited

@Documented

public @interface CustomizedCacheable {





    String[] value() default {};





    String[] cacheNames() default {};





    String key() default "";





    String keyGenerator() default "";





    String cacheResolver() default "";





    String condition() default "";





    String unless() default "";





    boolean sync() default false;



    //String cacheManager() default "redisCacheManager";



    /\*\*

     \* 过期时间

     \* @return

     \*/

    long expiredTimeSecond() default 0;



    /\*\*

     \* 预刷新时间

     \* @return

     \*/

    long preLoadTimeSecond() default 0;



    /\*\*

     \* 缓存级别,1-本地缓存,2-redis缓存,3-本地+redis

     \* @return

     \*/

    String cacheType() default "1";



    /\*\*

     \* 一二级缓存之间的缓存过期时间差

     \* @return

     \*/

    long expiredInterval() default 0;



    /\*\*

     \* 是否开启缓存

     \* @return

     \*/

    String cacheEnabled() default "1";



    long test() default 1;

}

增加自定义注解的拦截器,根据设置的缓存等级决定走本地缓存还是redis缓存,同时比较缓存的剩余过期时间是否小于阈值(preLoadTimeSecond),小于则重新刷新缓存,达到缓存预热的效果,同时减少缓存击穿的问题。

核心代码如下:

代码语言:java
复制
@Component

@Aspect

@Slf4j

@Order(1)

public class CacheReloadAspect {



    @Autowired

    private Environment environment;



    @Autowired

    private ApplicationContext applicationContext;



    private ReentrantLock lock = new ReentrantLock();



    @SneakyThrows

    @Around(value = "@annotation(com.xiaoyi.multiTtlCache.annotation.CustomizedCacheable)")

    public Object around(ProceedingJoinPoint proceedingJoinPoint){

        //方法入参对象数组

        Object[] args = proceedingJoinPoint.getArgs();

        //方法实体

        Method method = MethodSignature.class.cast(proceedingJoinPoint.getSignature()).getMethod();

        //自定义注解

        CustomizedCacheable cacheable = method.getAnnotation(CustomizedCacheable.class);

        String cacheEnabled = cacheable.cacheEnabled();

        //根据配置判断是否开启缓存

        String property = environment.getProperty(cacheEnabled);

        if (!ObjectUtil.isEmpty(property)) {

            return proceedingJoinPoint.proceed();

        }

        //解析上下文

        StandardEvaluationContext standardEvaluationContext = new StandardEvaluationContext();

        //参数名称

        String[] parameterNames = new LocalVariableTableParameterNameDiscoverer().getParameterNames(method);

        for (int i = 0; i < parameterNames.length; i++) {

            standardEvaluationContext.setVariable(parameterNames[i], args[i]);

        }

        //解析SPEL表达式的key,获取真正存入缓存中的key值

        String key = parseSPELKey(cacheable, standardEvaluationContext);

        Object result = null;

        String cacheType = cacheable.cacheType();

        switch (cacheType) {

            case CacheConstant.LOCAL\_CACHE:

                result = useCaffeineCache(key, cacheable, proceedingJoinPoint);

            case CacheConstant.REDIS\_CACHE:

                result = useRedisCache(key, cacheable, proceedingJoinPoint);

            case CacheConstant.BOTH\_CACHE:

                result = useBothCache(key, cacheable, proceedingJoinPoint);

            default:

                result = null;

        }

        return result;



    }



    @SneakyThrows

    private Object useBothCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {

        long expiredInterval = cacheable.expiredInterval();

        MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);

        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");

        Object o = myCaffeineCache.get(key);

        if (o != null) {

            Long ttl = myCaffeineCache.getTtl(key);

            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){

                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());

                ThreadUtil.execute(()->{

                    lock.lock();

                    try{

                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);

                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);

                        myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());

                        redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond() + expiredInterval, TimeUnit.SECONDS);

                    }catch (Exception e){

                        log.error("{}",e.getMessage(),e);

                    }finally {

                        lock.unlock();

                    }

                });

            }

            return o;

        } else {

            Object o1 = redisTemplate.opsForValue().get(key);

            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);

            if(o1 != null){

                myCaffeineCache.set(key, o1, ttl);

                return o1;

            }

        }

        Object result = proceedingJoinPoint.proceed();

        myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());

        redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond() + expiredInterval, TimeUnit.SECONDS);

        return result;

    }



    @SneakyThrows

    private Object useRedisCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {

        RedisTemplate redisTemplate = (RedisTemplate) SpringUtil.getBean("cacheRedisTemplate");

        Object o = redisTemplate.opsForValue().get(key);

        if (o != null) {

            Long ttl = redisTemplate.getExpire(key, TimeUnit.SECONDS);

            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){

                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}", key, ttl, cacheable.preLoadTimeSecond());

                ThreadUtil.execute(()->{

                    lock.lock();

                    try{

                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);

                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);

                        redisTemplate.opsForValue().set(key, o1, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);

                    }catch (Exception e){

                        log.error("{}",e.getMessage(),e);

                    }finally {

                        lock.unlock();

                    }

                });

            }

            return o;

        }

        Object result = proceedingJoinPoint.proceed();

        redisTemplate.opsForValue().set(key, result, cacheable.expiredTimeSecond(), TimeUnit.SECONDS);

        return result;

    }



    @SneakyThrows

    private Object useCaffeineCache(String key, CustomizedCacheable cacheable, ProceedingJoinPoint proceedingJoinPoint) {

        MyCaffeineCache myCaffeineCache = (MyCaffeineCache) SpringUtil.getBean(MyCaffeineCache.class);

        Object o = myCaffeineCache.get(key);

        if (o != null) {

            Long ttl = myCaffeineCache.getTtl(key);

            if(ObjectUtil.isNotEmpty(ttl) && ttl <= cacheable.preLoadTimeSecond()){

                log.info(">>>>>>>>>>> cacheKey:{}, ttl: {},preLoadTimeSecond: {}",key,ttl,cacheable.preLoadTimeSecond());

                ThreadUtil.execute(()->{

                    lock.lock();

                    try{

                        CachedInvocation cachedInvocation = buildCachedInvocation(proceedingJoinPoint, cacheable);

                        Object o1 = CacheHelper.exeInvocation(cachedInvocation);

                        myCaffeineCache.set(key, o1, cacheable.expiredTimeSecond());

                    }catch (Exception e){

                        log.error("{}",e.getMessage(),e);

                    }finally {

                        lock.unlock();

                    }

                });

            }

            return o;

        }

        Object result = proceedingJoinPoint.proceed();

        myCaffeineCache.set(key, result, cacheable.expiredTimeSecond());

        return result;

    }



    private CachedInvocation buildCachedInvocation(ProceedingJoinPoint proceedingJoinPoint,CustomizedCacheable customizedCacheable){

        Method method = this.getSpecificmethod(proceedingJoinPoint);

        String[] cacheNames = customizedCacheable.cacheNames();

        Object targetBean = proceedingJoinPoint.getTarget();

        Object[] arguments = proceedingJoinPoint.getArgs();

        Object key = customizedCacheable.key();

        CachedInvocation cachedInvocation = CachedInvocation.builder()

                .arguments(arguments)

                .targetBean(targetBean)

                .targetMethod(method)

                .cacheNames(cacheNames)

                .key(key)

                .expiredTimeSecond(customizedCacheable.expiredTimeSecond())

                .preLoadTimeSecond(customizedCacheable.preLoadTimeSecond())

                .build();

        return cachedInvocation;

    }



    private Method getSpecificmethod(ProceedingJoinPoint pjp) {

        MethodSignature methodSignature = (MethodSignature) pjp.getSignature();

        Method method = methodSignature.getMethod();

        // The method may be on an interface, but we need attributes from the

        // target class. If the target class is null, the method will be

        // unchanged.

        Class<?> targetClass = AopProxyUtils.ultimateTargetClass(pjp.getTarget());

        if (targetClass == null && pjp.getTarget() != null) {

            targetClass = pjp.getTarget().getClass();

        }

        Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);

        // If we are dealing with method with generic parameters, find the

        // original method.

        specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);

        return specificMethod;

    }



    private String parseSPELKey(CustomizedCacheable cacheable, StandardEvaluationContext context) {

        String keySpel = cacheable.key();

        Expression expression = new SpelExpressionParser().parseExpression(keySpel);

        String key = expression.getValue(context, String.class);

        return key;

    }



}

拦截器利用了AOP思想,达到对业务代码的无侵入性。通过mvn install下载到本地maven仓库或mvn deploy部署到远程仓库。这样其他项目在使用该组件时,只需要在pom中引入该依赖包,然后在方法上加上自定义注解即可。

代码语言:java
复制
@PostMapping("/2")

@CustomizedCacheable(value = "22", key = "11", cacheType = "2", expiredTimeSecond = 100, preLoadTimeSecond = 40)

public String test2() {

    //...

}

完整项目地址:https://github.com/xiaoyir/multiTtlCache

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档