四时宝库

程序员的知识宝库

对Spring CacheManager的进一步抽象

在Spring项目当中,我们使用缓存时,往往会直接使用redis,memcache,Guava。你曾经使用过Spring CacheManager吗?

Spring CacheManager实际上是Spring框架对底层缓存接口的一层抽象,也就是说,用户在使用Spring CacheManager时,可以不用关心底层到底使用的是什么缓存,即使该缓存从redis替换成了memcache,缓存接口并不需要作任何改动。

不仅仅如此,笔者在项目当中,曾经再一次对该CacheManager作了封装,实现了网易云NCR缓存集群在集群故障的情况下的动态切换,这里我会写专门的一篇文章来介绍怎么来实现该机制。

所以,如果你的项目有改动底层缓存的可能,或许Spring CacheManager就是你的一个选择。

当然,使用过的人也清楚,Spring CacheManager并没有封装所有的底层接口,比如redis的pipeline,lpush,lpop等。所以,笔者在实际项目中的使用状态是:混合使用Spring CacheManager和jedis。

下面进入正文。

对Spring CacheManager的进一步封装,在Spring容器启动过程中,扫描被@AutowireCache标注的字段或者方法,从而将Spring CacheManager生成的缓存bean注入到service bean中:

package com.wisexplore.base.spec.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
自定义一个标签,用于在service bean中注入缓存bean
**/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.CONSTRUCTOR, ElementType.METHOD, ElementType.PARAMETER, ElementType.FIELD, ElementType.ANNOTATION_TYPE})
@Documented
public @interface AutowireCache {

    boolean required() default true; //功能同IOC注入时的required

    String value(); //单个缓存名称,针对特定业务可以定义不同的名称
}
package com.wisexplore.base.core.beans;

import com.wisexplore.base.spec.annotation.AutowireCache;
import com.wisexplore.base.spec.api.ExpressionResolver;
import com.wisexplore.base.spec.support.ExceptionUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.PropertyValues;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanFactoryAware;
import org.springframework.beans.factory.annotation.InjectionMetadata;
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessorAdapter;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.core.BridgeMethodResolver;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.stereotype.Component;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.util.StringUtils;

import java.beans.PropertyDescriptor;
import java.lang.reflect.AccessibleObject;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
**封装的核心逻辑
**扫描所有bean,被@AutowireCache标注的属性或者方法,会将Spring CacheManager生成的bean注入到service bean中
**/
@Component
public class CacheAutowireBeanPostProcessor extends InstantiationAwareBeanPostProcessorAdapter implements BeanFactoryAware {

    private static final Logger logger = LoggerFactory.getLogger(CacheAutowireBeanPostProcessor.class);

    private ConfigurableListableBeanFactory beanFactory;

    //InjectionMetadata按字面理解就知道是IOC注入时的元数据,
    //这里可能存在多个bean同时注入时的并发可能,所以用的是并发集合ConcurrentHashMap
    private final Map<String, InjectionMetadata> injectionMetadataCache = new ConcurrentHashMap<>(256);

    @Override
    public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
        //要求必须是ConfigurableListableBeanFactory
        if (!(beanFactory instanceof ConfigurableListableBeanFactory)) {
            throw new IllegalArgumentException(
                "CacheAutowireBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory);
        }
        this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
    }

    @Override
    public PropertyValues postProcessPropertyValues(
        PropertyValues pvs, PropertyDescriptor[] pds, Object bean, String beanName) throws BeanCreationException
    {
        //通过Spring标签动态注入缓存bean的逻辑定义在下面的findAutowiringMetadata()方法中
        InjectionMetadata metadata = findAutowiringMetadata(beanName, bean.getClass(), pvs);
        try {
            metadata.inject(bean, beanName, pvs);
        }
        catch (BeanCreationException ex) {
            throw ex;
        }
        catch (Throwable ex) {
            throw new BeanCreationException(beanName, "Injection of AutowireCache dependencies failed", ex);
        }
        return pvs;
    }

    //获取@AutowireCache修饰的标签属性
    private AnnotationAttributes findAnnotation(AccessibleObject ao) {
        return ao.getAnnotations().length > 0
            ? AnnotatedElementUtils.getMergedAnnotationAttributes(ao, AutowireCache.class)
            : null;
    }

    //该属性是否是required
    private boolean determineRequiredStatus(AnnotationAttributes ann) {
        String name = "required";
        return !ann.containsKey(name) || ann.getBoolean(name);
    }

    private String determineCacheName(AnnotationAttributes ann) {
        String name = "value";
        if (!ann.containsKey(name)) {
            return null;
        }

        ExpressionResolver resolver = null;
        try {
            //缓存名称支持spel表达式
            resolver = beanFactory.getBean(ExpressionResolver.class);
        } catch (Exception ex) {
            ExceptionUtils.swallow(ex, false);
        }

        return resolver == null ? ann.getString(name) : resolver.resolveAsString(ann.getString(name));
    }

    private InjectionMetadata findAutowiringMetadata(String beanName, Class<?> clazz, PropertyValues pvs) {
        String cacheKey = StringUtils.hasLength(beanName) ? beanName : clazz.getName();

        InjectionMetadata metadata = injectionMetadataCache.get(cacheKey);
        if (InjectionMetadata.needsRefresh(metadata, clazz)) {
            synchronized (injectionMetadataCache) { //同步
                metadata = injectionMetadataCache.get(cacheKey);
                if (InjectionMetadata.needsRefresh(metadata, clazz)) {
                    if (metadata != null) {
                        metadata.clear(pvs);
                    }
                    try {
                        metadata = buildAutowiringMetadata(clazz);
                        injectionMetadataCache.put(cacheKey, metadata);
                    }
                    catch (NoClassDefFoundError err) {
                        throw new IllegalStateException("Failed to introspect bean class [" + clazz.getName() +
                            "] for AutowireCache metadata: could not find class that it depends on", err);
                    }
                }
            }
        }

        return metadata;
    }

    private InjectionMetadata buildAutowiringMetadata(final Class<?> clazz) {
        LinkedList<InjectionMetadata.InjectedElement> elements = new LinkedList<>();
        Class<?> targetClass = clazz;

        do {
            final LinkedList<InjectionMetadata.InjectedElement> currElements = new LinkedList<>();

            ReflectionUtils.doWithLocalFields(targetClass, field -> {
                AnnotationAttributes ann = findAnnotation(field);
                if (ann != null) {
                    if (Modifier.isStatic(field.getModifiers())) {
                        logger.warn("@AutowireCache is not supported on static fields: " + field);
                        return;
                    }
                    if (field.getType() != Cache.class) {
                        logger.warn("@AutowireCache should be used on Cache type");
                        return;
                    }

                    currElements.add(new AutowireCacheFieldElement(field, determineRequiredStatus(ann), determineCacheName(ann)));
                }
            });

            ReflectionUtils.doWithLocalMethods(targetClass, method -> {
                Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(method);
                if (!BridgeMethodResolver.isVisibilityBridgeMethodPair(method, bridgedMethod)) {
                    return;
                }

                AnnotationAttributes ann = findAnnotation(bridgedMethod);
                if (ann != null && method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) {
                    if (Modifier.isStatic(method.getModifiers())) {
                        logger.warn("@AutowireCache is not supported on static methods: " + method);
                        return;
                    }
                    if (method.getParameterTypes().length != 1 || method.getParameterTypes()[0] != Cache.class) {
                        logger.warn("@AutowireCache should only be used on methods with parameters: " + method);
                        return;
                    }

                    boolean required = determineRequiredStatus(ann);
                    String cacheName = determineCacheName(ann);
                    PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz);
                    currElements.add(new AutowireCacheMethodElement(method, required, cacheName, pd));
                }
            });

            elements.addAll(0, currElements);
            targetClass = targetClass.getSuperclass();
        }
        while (targetClass != null && targetClass != Object.class);
        
        //这里的elements实际上就是Spring CacheManager生成的cache bean
        return new InjectionMetadata(clazz, elements);
    }

    //AutowireCacheFieldElement按字面理解,即是用@AutowireCache修饰的bean的属性
    private class AutowireCacheFieldElement extends InjectionMetadata.InjectedElement {

        private final boolean required;

        private final String cacheName;

        public AutowireCacheFieldElement(Field field, boolean required, String cacheName) {
            super(field, null);
            this.required = required;
            this.cacheName = cacheName;
        }

        @Override
        protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
            Object value = null;

            try {
                value = beanFactory.getBean(CacheManager.class).getCache(cacheName);
            } catch (Exception ex) {
                ExceptionUtils.swallow(ex, false);
            }

            if (value != null) {
                Field field = (Field) member;
                ReflectionUtils.makeAccessible(field);
                field.set(bean, value);
            } else if (required) {
                throw new IllegalStateException("@AutowireCache injection failed");
            }
        }
    }

    //AutowireCacheMethodElement按字面理解,即是用@AutowireCache修饰的bean的方法
    private class AutowireCacheMethodElement extends InjectionMetadata.InjectedElement {

        private final boolean required;

        private final String cacheName;

        public AutowireCacheMethodElement(Method method, boolean required, String cacheName, PropertyDescriptor pd) {
            super(method, pd);
            this.required = required;
            this.cacheName = cacheName;
        }

        @Override
        protected void inject(Object bean, String beanName, PropertyValues pvs) throws Throwable {
            if (checkPropertySkipping(pvs)) {
                return;
            }

            Object value = null;

            try {
                //这里是Spring CacheManager生成的cache bean
                value = beanFactory.getBean(CacheManager.class).getCache(cacheName);
            } catch (Exception ex) {
                ExceptionUtils.swallow(ex, false);
            }

            if (value != null) {
                Method method = (Method) member;

                try {
                    ReflectionUtils.makeAccessible(method);
                    method.invoke(bean, value);
                } catch (InvocationTargetException ex) {
                    throw ex.getTargetException();
                }
            } else if (required) {
                throw new IllegalStateException("@AutowireCache injection failed");
            }
        }
    }
}


第二步,配置Spring configuration:

package com.wisexplore.redis.main.config;

import com.wisexplore.base.spec.annotation.CacheManagerBean;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cache.CacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
import redis.clients.jedis.JedisPoolConfig;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class WisexploreRedisMainConfig {

    //jedis pool配置
    @Bean("mainJedisPoolConfig")
    JedisPoolConfig mainJedisPoolConfig(
            @Value("${redis.main.jedisPoolConfig.blockWhenExhausted}") boolean blockWhenExhausted,
            @Value("${redis.main.jedisPoolConfig.lifo}") boolean lifo,
            @Value("${redis.main.jedisPoolConfig.fairness}") boolean fairness,
            @Value("${redis.main.jedisPoolConfig.jmxEnabled}") boolean jmxEnabled,
            @Value("${redis.main.jedisPoolConfig.maxTotal}") int maxTotal,
            @Value("${redis.main.jedisPoolConfig.maxIdle}") int maxIdle,
            @Value("${redis.main.jedisPoolConfig.minIdle}") int minIdle,
            @Value("${redis.main.jedisPoolConfig.maxWaitMillis}") long maxWaitMillis,
            @Value("${redis.main.jedisPoolConfig.minEvictableIdleTimeMillis}") long minEvictableIdleTimeMillis,
            @Value("${redis.main.jedisPoolConfig.numTestsPerEvictionRun}") int numTestsPerEvictionRun,
            @Value("${redis.main.jedisPoolConfig.softMinEvictableIdleTimeMillis}") long softMinEvictableIdleTimeMillis,
            @Value("${redis.main.jedisPoolConfig.timeBetweenEvictionRunsMillis}") long timeBetweenEvictionRunsMillis,
            @Value("${redis.main.jedisPoolConfig.testOnBorrow}") boolean testOnBorrow,
            @Value("${redis.main.jedisPoolConfig.testOnCreate}") boolean testOnCreate,
            @Value("${redis.main.jedisPoolConfig.testOnReturn}") boolean testOnReturn,
            @Value("${redis.main.jedisPoolConfig.testWhileIdle}") boolean testWhileIdle) {
        JedisPoolConfig config = new JedisPoolConfig();
        config.setBlockWhenExhausted(blockWhenExhausted);
        config.setLifo(lifo);
        config.setFairness(fairness);
        config.setJmxEnabled(jmxEnabled);
        config.setMaxTotal(maxTotal);
        config.setMaxIdle(maxIdle);
        config.setMinIdle(minIdle);
        config.setMaxWaitMillis(maxWaitMillis);
        config.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
        config.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
        config.setNumTestsPerEvictionRun(numTestsPerEvictionRun);
        config.setSoftMinEvictableIdleTimeMillis(softMinEvictableIdleTimeMillis);
        config.setTestOnBorrow(testOnBorrow);
        config.setTestOnCreate(testOnCreate);
        config.setTestOnReturn(testOnReturn);
        config.setTestWhileIdle(testWhileIdle);

        return config;
    }

    //配置jedis连接工厂
    @Bean("mainJedisConnectionFactory")
    JedisConnectionFactory mainJedisConnectionFactory(
            @Value("${redis.main.jedisConnectionFactory.usePool}") boolean usePool,
            @Value("${redis.main.jedisConnectionFactory.hostname}") String hostname,
            @Value("${redis.main.jedisConnectionFactory.port}") int port,
            @Value("${redis.main.jedisConnectionFactory.database}") int database,
            @Value("${redis.main.jedisConnectionFactory.password}") String password,
            @Value("${redis.main.jedisConnectionFactory.timeout}") int timeout,
            @Value("${redis.main.jedisConnectionFactory.useSsl}") boolean useSsl,
            @Value("${redis.main.jedisConnectionFactory.useSentinel}") boolean useSentinel,
            @Value("${redis.main.jedisConnectionFactory.masterName}") String masterName,
            @Value("${redis.main.jedisConnectionFactory.sentinelHostPorts}") String sentinelHostPorts,
            @Qualifier("mainJedisPoolConfig") JedisPoolConfig config) {
        JedisConnectionFactory factory;
        if (!useSentinel) {
            factory = new JedisConnectionFactory();
            factory.setHostName(hostname);
            factory.setPort(port);
            factory.setDatabase(database);
        } else {
            RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(masterName,
                    StringUtils.commaDelimitedListToSet(sentinelHostPorts));

            factory = new JedisConnectionFactory(sentinelConfig);
        }

        factory.setUsePool(usePool);
        factory.setPassword(password);
        factory.setTimeout(timeout);
        factory.setPoolConfig(config);
        factory.setUseSsl(useSsl);
        return factory;

    }

    //配置redisTemplate
    @Bean("mainRedisTemplate")
    RedisTemplate<Object, Object> mainRedisTemplate(
            @Qualifier("mainJedisConnectionFactory") JedisConnectionFactory connectionFactory,
            @Value("${redis.main.redisTemplate.enableTransactionSupport}") boolean enableTransactionSupport) {
        RedisTemplate<Object, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);
        template.setEnableTransactionSupport(enableTransactionSupport);
        RedisSerializer stringSerializer = new StringRedisSerializer();
        template.setKeySerializer(stringSerializer);
        template.setValueSerializer(stringSerializer);
        template.setHashKeySerializer(stringSerializer);
        template.setHashValueSerializer(stringSerializer);
        return template;
    }

    //配置基于redis缓存的CacheManager
    @Bean
    @Order(Ordered.LOWEST_PRECEDENCE)
    CacheManager mainRedisCacheManager(
            @Qualifier("mainJedisConnectionFactory") JedisConnectionFactory connectionFactory,
            @Value("${redis.main.redisCacheManager.defaultExpiration}") long defaultExpiration,
            @Value("${redis.main.redisCacheManager.transactionAware}") boolean transactionAware,
            @Value("${redis.main.redisCacheManager.loadRemoteCachesOnStartup}") boolean loadRemoteCachesOnStartup,
            @Value("${redis.main.redisCacheManager.usePrefix}") boolean usePrefix,
            @Value("${redis.main.redisCacheManager.useDefaultCache}") boolean useDefaultCache,
            @Value("${redis.main.redisCacheManager.caches}") String caches) {
        RedisCacheManager.RedisCacheManagerBuilder builder = RedisCacheManager.builder(connectionFactory);

        if (StringUtils.hasText(caches)) {
            Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
            RedisCacheConfiguration redisCacheConfiguration;
            for (String cache : caches.split("[ ,;\n]")) {
                if (!StringUtils.hasText(cache)) {
                    continue;
                }

                int split = cache.indexOf(":");
                //配置缓存名称和缓存过期时间
                if (split < 0) {
                    redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(0l));
                    cacheConfigurations.put(cache, redisCacheConfiguration);
                } else {
                    redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ofSeconds(Long.valueOf(cache.substring(split + 1))));
                    cacheConfigurations.put(cache.substring(0, split), redisCacheConfiguration);
                }
            }
            builder.withInitialCacheConfigurations(cacheConfigurations);
        }

        return builder.build();
    }
}


第三步,缓存配置格式如下:

---
redis:
    main:
        jedisPoolConfig:
            blockWhenExhausted: false
            lifo: true
            fairness: false
            jmxEnabled: true
            maxTotal: 50
            maxWaitMillis: -1
            minEvictableIdleTimeMillis: 120000
            minIdle: 0
            maxIdle: 50
            numTestsPerEvictionRun: -1
            softMinEvictableIdleTimeMillis: -1
            timeBetweenEvictionRunsMillis: 60000
            testOnBorrow: false
            testOnCreate: false
            testOnReturn: false
            testWhileIdle: true

        jedisConnectionFactory:
            usePool: true
            hostname: 127.0.0.1
            port: 6379
            database: 0
            password: xxx
            timeout: 15000
            useSsl: false
            useSentinel: false
            masterName: mymaster
            sentinelHostPorts: 127.0.0.1:26371,127.0.0.1:26372

        redisCacheManager:
            defaultExpiration: 0
            transactionAware: false
            loadRemoteCachesOnStartup: false
            usePrefix: true
            useDefaultCache: false
            caches: >
                TOKEN:1800,
                NONCE:180,
                PASSPORT_KEY:3600,
                PASSPORT_CAPTCHA:1200,
                PASSPORT_SHORT_TERM:180,
                VISIT_CACHE:1200,
                PASSPORT_LOCK:1800,
                PASSPORT_CODE:3600,
                USER_FLOW:3,
                COOKIE_TOKEN_FLOW:180,
                SMS:1800,
                PWD_ERROR:1800,
                DEALER_MAPPED:0,
                ALIPAY_CACHE:0,
                RECOMMEND:1800,
                SEARCH_CACHE:600,
                SEARCH_FLOW_CACHE:60,
                SEARCH_LOCK:1800,
                NEURON_AUTO_GEN_CACHE:0,
                OAUTH_STATE:1200,

        redisTemplate:
            enableTransactionSupport: false


最后,您可以在某个service bean中用以下的方式注入并使用它:

@AutowireCache(CacheConstants.TOKEN_CACHE)
private Cache tokenCache;

文章已经收录在以下的知识体系中:分享给您:Spring的深度使用 ,欢迎有意向的同学跟我一起共建该知识体系。在手机浏览器打开该链接,点击上方图片即可下载学识App。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言
    友情链接