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

2024-08-10 19:44:30 浏览数 (2)

大家好,我是小义,这段时间有点事耽搁了,好久没写文章了,今天介绍一下如何构建一个基于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

0 人点赞