Spring中的Cache

作者: spilledyear | 来源:发表于2019-01-06 19:32 被阅读17次

    SpringAOP的完美案例

    使用案例

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    
    
    spring:
      redis:
        cluster:
          nodes:
            - 10.9.15.32:6388
            - 10.9.15.33:6383
            - 10.9.15.34:6382
            - 10.9.15.35:6382
            - 10.9.15.36:6389
            - 10.9.15.38:6379
    
    
    
    @Configuration
    public class RedisConfig {
        @Bean
        public CacheManager cacheManager(RedisConnectionFactory factory) {
            RedisSerializer<String> redisSerializer = new StringRedisSerializer();
            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);
    
            // 配置序列化
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
            config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
                    .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
    
            RedisCacheManager cacheManager = RedisCacheManager.builder(factory).cacheDefaults(config).build();
            return cacheManager;
        }
    }
    
    
    
    @SpringBootApplication
    @EnableCaching
    public class CacheApplication {
        public static void main(String[] args) {
            CacheApplication .run(SakuraPortalApplication.class, args);
        }
    }
    
    
    @Cacheable(value = "mockMeta", key = "#serviceId")
    public MockMeta getMockMeta(Long serviceId) {
        return mapper.selectByPrimaryKey(serviceId);
    }
    

    原理解析

    EnableCaching

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(CachingConfigurationSelector.class)
    public @interface EnableCaching {
    
        // 指定动态代理方式,属性值为false时,表示使用jdk代理,为true时则表示使用cglib代理。只有当mode等于PROXY时才生效
        boolean proxyTargetClass() default false;
    
        // aop的模式,有 SpringAOP  和 ASPECTJ 两种
        AdviceMode mode() default AdviceMode.PROXY;
    
        // 当一个joinpoint上有多个advice时,用于指定顺序
        int order() default Ordered.LOWEST_PRECEDENCE;
    }
    

    @Import用来整合所有在@Configuration注解中定义的bean配置

    CachingConfigurationSelector

    public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
        @Override
        public String[] selectImports(AdviceMode adviceMode) {
            switch (adviceMode) {
                case PROXY:
                    return getProxyImports();
                case ASPECTJ:
                    return getAspectJImports();
                default:
                    return null;
            }
        }
    
        private String[] getProxyImports() {
            List<String> result = new ArrayList<>(3);
            result.add(AutoProxyRegistrar.class.getName());
            result.add(ProxyCachingConfiguration.class.getName());
            if (jsr107Present && jcacheImplPresent) {
                result.add(PROXY_JCACHE_CONFIGURATION_CLASS);
            }
            return StringUtils.toStringArray(result);
        }
    }
    

    不关注ASPECTJ的AOP模式

    CachingConfigurationSelector的类继承关系如下:


    在其父类AdviceModeImportSelector的selectImports方法中,最终会回调子类的selectImports方法

    @Override
    public final String[] selectImports(AnnotationMetadata importingClassMetadata) {
        Class<?> annType = GenericTypeResolver.resolveTypeArgument(getClass(), AdviceModeImportSelector.class);
        Assert.state(annType != null, "Unresolvable type argument for AdviceModeImportSelector");
    
        AnnotationAttributes attributes = AnnotationConfigUtils.attributesFor(importingClassMetadata, annType);
        if (attributes == null) {
            throw new IllegalArgumentException(String.format(
                    "@%s is not present on importing class '%s' as expected",
                    annType.getSimpleName(), importingClassMetadata.getClassName()));
        }
    
        AdviceMode adviceMode = attributes.getEnum(getAdviceModeAttributeName());
        String[] imports = selectImports(adviceMode);
        if (imports == null) {
            throw new IllegalArgumentException("Unknown AdviceMode: " + adviceMode);
        }
        return imports;
    }
    

    在getProxyImports方法中,会涉及到两个关键的类的加载:AutoProxyRegistrar、ProxyCachingConfiguration

    AutoProxyRegistrar

    public class AutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            boolean candidateFound = false;
            Set<String> annoTypes = importingClassMetadata.getAnnotationTypes();
            for (String annoType : annoTypes) {
                AnnotationAttributes candidate = AnnotationConfigUtils.attributesFor(importingClassMetadata, annoType);
                if (candidate == null) {
                    continue;
                }
                Object mode = candidate.get("mode");
                Object proxyTargetClass = candidate.get("proxyTargetClass");
                if (mode != null && proxyTargetClass != null && AdviceMode.class == mode.getClass() &&
                        Boolean.class == proxyTargetClass.getClass()) {
                    candidateFound = true;
                    if (mode == AdviceMode.PROXY) {
                        AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
                        if ((Boolean) proxyTargetClass) {
                            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
                            return;
                        }
                    }
                }
            }
        }
    }
    

    所有实现了ImportBeanDefinitionRegistrar接口的类的都会被ConfigurationClassPostProcessor处理,ConfigurationClassPostProcessor实现了BeanFactoryPostProcessor接口,所以ImportBeanDefinitionRegistrar中动态注册的bean是优先于依赖其的bean初始化的,也能被AOP等机制处理。

    从上面的代码中可以发现,如果使用JDK动态代理,会执行以下代码,AutoProxyCreator 代表一个能创建代理对象的对象

    AopConfigUtils.registerAutoProxyCreatorIfNecessary(registry);
    
    
    @Nullable
    public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) {
        return registerAutoProxyCreatorIfNecessary(registry, null);
    }
    
    @Nullable
    public static BeanDefinition registerAutoProxyCreatorIfNecessary(
            BeanDefinitionRegistry registry, @Nullable Object source) {
    
        return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source);
    }
    

    InfrastructureAdvisorAutoProxyCreator

    public class InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator {
    
        @Nullable
        private ConfigurableListableBeanFactory beanFactory;
    
        @Override
        protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) {
            super.initBeanFactory(beanFactory);
            this.beanFactory = beanFactory;
        }
    
        @Override
        protected boolean isEligibleAdvisorBean(String beanName) {
            return (this.beanFactory != null && this.beanFactory.containsBeanDefinition(beanName) &&
                    this.beanFactory.getBeanDefinition(beanName).getRole() == BeanDefinition.ROLE_INFRASTRUCTURE);
        }
    }
    

    有关于的类继承关系如下:


    实现了顶层的BeanPostProcessor接口,这代表在初始换bean前后可以执行相应的逻辑。SpringAOP的起点就是在AbstractAutoProxyCreator中的postProcessAfterInitialization方法中,创建代理之前有个前置校验,如下:

    protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
        if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {
            return bean;
        }
        if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {
            return bean;
        }
    
        // 前置校验,如果不通过就不创建代理对象
        if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return bean;
        }
    
        // Create proxy if we have advice.
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
        if (specificInterceptors != DO_NOT_PROXY) {
            this.advisedBeans.put(cacheKey, Boolean.TRUE);
            Object proxy = createProxy(
                    bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
            this.proxyTypes.put(cacheKey, proxy.getClass());
            return proxy;
        }
    
        this.advisedBeans.put(cacheKey, Boolean.FALSE);
        return bean;
    }
    

    前面已经说过InfrastructureAdvisorAutoProxyCreator的作用是用于创建代理对象,至于为哪些bean创建代理,则是其isEligibleAdvisorBean方法指定,其源码如下:

    @Override
    protected boolean isEligibleAdvisorBean(String beanName) {
        return (this.beanFactory != null && this.beanFactory.containsBeanDefinition(beanName) &&
                this.beanFactory.getBeanDefinition(beanName).getRole() == BeanDefinition.ROLE_INFRASTRUCTURE);
    }
    

    即:只有bean的role属性=BeanDefinition.ROLE_INFRASTRUCTURE的时候才会为这个bean创建代理对象

    ProxyCachingConfiguration

    上面已经创建了一个针对于Cache的AutoProxyCreator,接下来创建的是相关的Advisor

    @Configuration
    @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
    public class ProxyCachingConfiguration extends AbstractCachingConfiguration {
        @Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {
            BeanFactoryCacheOperationSourceAdvisor advisor = new BeanFactoryCacheOperationSourceAdvisor();
            advisor.setCacheOperationSource(cacheOperationSource());
            advisor.setAdvice(cacheInterceptor());
            if (this.enableCaching != null) {
                advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));
            }
            return advisor;
        }
    
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public CacheOperationSource cacheOperationSource() {
            return new AnnotationCacheOperationSource();
        }
    
        @Bean
        @Role(BeanDefinition.ROLE_INFRASTRUCTURE)
        public CacheInterceptor cacheInterceptor() {
            CacheInterceptor interceptor = new CacheInterceptor();
            interceptor.configure(this.errorHandler, this.keyGenerator, this.cacheResolver, this.cacheManager);
            interceptor.setCacheOperationSource(cacheOperationSource());
            return interceptor;
        }
    }
    

    AnnotationCacheOperationSource顶层的接口是CacheOperationSource,用于获取某个方法上缓存相关的操作(Return the collection of cache operations for this method)。

    缓存相关的操作被抽象成CacheOperation,其实现类有:CacheEvictOperation、CachePutOperation、CacheableOperation。

    接下来感觉有点无从下手,其实这里主要就是生成一个advisor,即 BeanFactoryCacheOperationSourceAdvisor。advisor就包括了pointcut和advice,advice就是对应这里的CacheInterceptor

    BeanFactoryCacheOperationSourceAdvisor

    public class BeanFactoryCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor {
        @Nullable
        private CacheOperationSource cacheOperationSource;
    
        private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
            @Override
            @Nullable
            protected CacheOperationSource getCacheOperationSource() {
                return cacheOperationSource;
            }
        };
    
        /**
         * Set the cache operation attribute source which is used to find cache
         * attributes. This should usually be identical to the source reference
         * set on the cache interceptor itself.
         */
        public void setCacheOperationSource(CacheOperationSource cacheOperationSource) {
            this.cacheOperationSource = cacheOperationSource;
        }
    
        /**
         * Set the {@link ClassFilter} to use for this pointcut.
         * Default is {@link ClassFilter#TRUE}.
         */
        public void setClassFilter(ClassFilter classFilter) {
            this.pointcut.setClassFilter(classFilter);
        }
    
        @Override
        public Pointcut getPointcut() {
            return this.pointcut;
        }
    }
    

    CacheOperationSourcePointcut对应pointcut,主要关注它的match方法

    @Override
    public boolean matches(Method method, Class<?> targetClass) {
        if (CacheManager.class.isAssignableFrom(targetClass)) {
            return false;
        }
        CacheOperationSource cas = getCacheOperationSource();
        return (cas != null && !CollectionUtils.isEmpty(cas.getCacheOperations(method, targetClass)));
    }
    

    cas.getCacheOperations 即对应 AnnotationCacheOperationSource的getCacheOperations方法,该方法实现在AbstractFallbackCacheOperationSource中,AnnotationCacheOperationSource继承自AbstractFallbackCacheOperationSource

    public Collection<CacheOperation> getCacheOperations(Method method, @Nullable Class<?> targetClass) {
        if (method.getDeclaringClass() == Object.class) {
            return null;
        }
    
        Object cacheKey = getCacheKey(method, targetClass);
        Collection<CacheOperation> cached = this.attributeCache.get(cacheKey);
    
        if (cached != null) {
            return (cached != NULL_CACHING_ATTRIBUTE ? cached : null);
        }
        else {
            Collection<CacheOperation> cacheOps = computeCacheOperations(method, targetClass);
            if (cacheOps != null) {
                if (logger.isTraceEnabled()) {
                    logger.trace("Adding cacheable method '" + method.getName() + "' with attribute: " + cacheOps);
                }
                this.attributeCache.put(cacheKey, cacheOps);
            }
            else {
                this.attributeCache.put(cacheKey, NULL_CACHING_ATTRIBUTE);
            }
            return cacheOps;
        }
    }
    

    主要就是查到对应的method上是否有那几个Cache注解,调用栈比较深,这里简单概括

    AbstractFallbackCacheOperationSource # getCacheOperations     =>
    
    AbstractFallbackCacheOperationSource # computeCacheOperations     =>
    
    AnnotationCacheOperationSource # findCacheOperations     =>
    
    AnnotationCacheOperationSource # determineCacheOperations     =>
    
    SpringCacheAnnotationParser # parseCacheAnnotations
    

    具体的查找过程,主要就是这几个注解:Cacheable、CacheEvict、CachePut、Caching

    private Collection<CacheOperation> parseCacheAnnotations(
            DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) {
    
        Collection<? extends Annotation> anns = (localOnly ?
                AnnotatedElementUtils.getAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS) :
                AnnotatedElementUtils.findAllMergedAnnotations(ae, CACHE_OPERATION_ANNOTATIONS));
        if (anns.isEmpty()) {
            return null;
        }
    
        final Collection<CacheOperation> ops = new ArrayList<>(1);
        anns.stream().filter(ann -> ann instanceof Cacheable).forEach(
                ann -> ops.add(parseCacheableAnnotation(ae, cachingConfig, (Cacheable) ann)));
        anns.stream().filter(ann -> ann instanceof CacheEvict).forEach(
                ann -> ops.add(parseEvictAnnotation(ae, cachingConfig, (CacheEvict) ann)));
        anns.stream().filter(ann -> ann instanceof CachePut).forEach(
                ann -> ops.add(parsePutAnnotation(ae, cachingConfig, (CachePut) ann)));
        anns.stream().filter(ann -> ann instanceof Caching).forEach(
                ann -> parseCachingAnnotation(ae, cachingConfig, (Caching) ann, ops));
        return ops;
    }
    

    CacheInterceptor

    CacheInterceptor其实就是插入的逻辑

    public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable {
        @Override
        @Nullable
        public Object invoke(final MethodInvocation invocation) throws Throwable {
            Method method = invocation.getMethod();
            CacheOperationInvoker aopAllianceInvoker = () -> {
                try {
                    // 执行目标方法
                    return invocation.proceed();
                }
                catch (Throwable ex) {
                    throw new CacheOperationInvoker.ThrowableWrapper(ex);
                }
            };
    
            try {
                return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
            }
            catch (CacheOperationInvoker.ThrowableWrapper th) {
                throw th.getOriginal();
            }
        }
    }
    

    CacheAspectSupport中的execute方法

    protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
        if (this.initialized) {
            Class<?> targetClass = getTargetClass(target);
            CacheOperationSource cacheOperationSource = getCacheOperationSource();
            if (cacheOperationSource != null) {
                Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
                if (!CollectionUtils.isEmpty(operations)) {
                    return execute(invoker, method,
                            new CacheOperationContexts(operations, method, args, target, targetClass));
                }
            }
        }
    
        return invoker.invoke();
    }
    

    CacheOperationContexts中有一个MultiValueMap类型的contexts属性

    private class CacheOperationContexts {
    
        private final MultiValueMap<Class<? extends CacheOperation>, CacheOperationContext> contexts;
    
        private final boolean sync;
    
        public CacheOperationContexts(Collection<? extends CacheOperation> operations, Method method,
                Object[] args, Object target, Class<?> targetClass) {
    
            this.contexts = new LinkedMultiValueMap<>(operations.size());
            for (CacheOperation op : operations) {
                this.contexts.add(op.getClass(), getOperationContext(op, method, args, target, targetClass));
            }
            this.sync = determineSyncFlag(method);
        }
    }
    
    
    protected class CacheOperationContext implements CacheOperationInvocationContext<CacheOperation> {
    
        private final CacheOperationMetadata metadata;
    
        private final Object[] args;
    
        private final Object target;
    
        private final Collection<? extends Cache> caches;
    
        private final Collection<String> cacheNames;
    }
    

    LinkedMultiValueMap中维护的是: key -> LinkList 的数据结构,所以每个CacheOperation对应一个CacheOperationContext列表。

    这里value有三个值,也就相当于有三个Cache
    
    @Cacheable(value = {"mockMeta", "mockMeta2", "mockMeta3"}, key = "#serviceId")
    public MockMeta getMockMeta(Long serviceId) {
    ......
    }
    

    封装好CacheOperationContexts之后,接下来执行excute方法

    @Nullable
    private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) {
        // 执行@Cacheable注解对应的操作,只有@Cacheable注解有sync属性,当sync为true时,contexts.isSynchronized()返回true,执行以下方法。不关注!
        if (contexts.isSynchronized()) {
            CacheOperationContext context = contexts.get(CacheableOperation.class).iterator().next();
            if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) {
                Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT);
                Cache cache = context.getCaches().iterator().next();
                try {
                    return wrapCacheValue(method, cache.get(key, () -> unwrapReturnValue(invokeOperation(invoker))));
                }
                catch (Cache.ValueRetrievalException ex) {
                    // The invoker wraps any Throwable in a ThrowableWrapper instance so we
                    // can just make sure that one bubbles up the stack.
                    throw (CacheOperationInvoker.ThrowableWrapper) ex.getCause();
                }
            }
            else {
                // No caching required, only call the underlying method
                return invokeOperation(invoker);
            }
        }
    
    
        // 对应@CacheEvict注解,有一个beforeInvocation属性,默认为false。如果beforeInvocation为true,则在执行目标方法前清除缓存
        processCacheEvicts(contexts.get(CacheEvictOperation.class), true,
                CacheOperationExpressionEvaluator.NO_RESULT);
    
        // 对应@Cacheable注解,尝试从缓存中获得key对应的值
        Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
    
        // 如果没有获取到缓存,就将CacheableOperation类型的CacheOperationContext封装到CachePutRequest中,并保存到cachePutRequests集合内
        List<CachePutRequest> cachePutRequests = new LinkedList<>();
        if (cacheHit == null) {
            collectPutRequests(contexts.get(CacheableOperation.class),
                    CacheOperationExpressionEvaluator.NO_RESULT, cachePutRequests);
        }
    
        Object cacheValue;
        Object returnValue;
    
        // 找到了缓存,就包装返回值
        if (cacheHit != null && !hasCachePut(contexts)) {
            // If there are no put requests, just use the cache hit
            cacheValue = cacheHit.get();
            returnValue = wrapCacheValue(method, cacheValue);
        }
        // 没有找到缓存,通过执行目标方法拿到返回值
        else {
            // 执行目标方法
            returnValue = invokeOperation(invoker);
            cacheValue = unwrapReturnValue(returnValue);
        }
    
        // 对应@CachePut注解,将CachePutOperation类型的CacheOperationContext封装到CachePutRequest中,并保存到cachePutRequests集合内
        collectPutRequests(contexts.get(CachePutOperation.class), cacheValue, cachePutRequests);
    
        // 处理cachePutRequests,即将值放到缓存!从上面的代码流程可以发现,只有当@Cacheable不命中或者有@CachePut注解的时候,才会生成requests,也只有这种情况下才需要将值放到缓存中
        for (CachePutRequest cachePutRequest : cachePutRequests) {
            // 写入缓存
            cachePutRequest.apply(cacheValue);
        }
    
        // 如果@CacheEvict注解的beforeInvocation属性为false,则在执行目标方法之后清除缓存
        processCacheEvicts(contexts.get(CacheEvictOperation.class), false, cacheValue);
    
        return returnValue;
    }
    

    写入缓存

    private class CachePutRequest {
        private final CacheOperationContext context;
        private final Object key;
    
        public CachePutRequest(CacheOperationContext context, Object key) {
            this.context = context;
            this.key = key;
        }
    
        public void apply(@Nullable Object result) {
            if (this.context.canPutToCache(result)) {
                for (Cache cache : this.context.getCaches()) {
                    doPut(cache, this.key, result);
                }
            }
        }
    }
    
    
    
    // AbstractCacheInvoker # doPut
    protected void doPut(Cache cache, Object key, @Nullable Object result) {
        try {
            cache.put(key, result);
        }
        catch (RuntimeException ex) {
            getErrorHandler().handleCachePutError(ex, cache, key, result);
        }
    }
    
    
    
    // 例如 RedisCache # put
    public void put(Object key, @Nullable Object value) {
        Object cacheValue = this.preProcessCacheValue(value);
        if (!this.isAllowNullValues() && cacheValue == null) {
            throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", this.name));
        } else {
            this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
        }
    }
    

    总结一下这部分流程:

    1、处理@Cacheable注解sync属性为true情况;
    
    2、如果@CacheEvict注解的beforeInvocation属性为true,则清除缓存;
    
    3、根据@Cacheable注解,尝试从缓存中获得key对应的值:如果命中,包装返回值;如果没有命中,执行名表方法的到返回值,然后包装返回值;
    
    4、如果@Cacheable没有命中,将CacheableOperation类型的CacheOperationContext封装到CachePutRequest中,并保存到cachePutRequests集合内;
    
    5、如果有@CachePut注解,将CachePutOperation类型的CacheOperationContext封装到CachePutRequest中,并保存到cachePutRequests集合内;
    
    6、遍历cachePutRequests,写入缓存;
    
    7、返回值;
    

    相关文章

      网友评论

        本文标题:Spring中的Cache

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