SpringCache实现原理及核心业务逻辑(二)

SpringCache是SpringFramework3.1引入的新特性,提供了基于注解的缓存配置方法。它本质上不是一个具体的缓存实现方案(例如EHCache),而是一个对缓存使用的抽象,通过在已有代码中打上几个预定义的注释,就可以实现我们希望达到的缓存效果。SpringCache支持跟第三方缓存例如EHCache集成;另外也提供了开箱即用的默认实现,可以直接拿来使用。
SpringCache支持使用SpEL(Spring Expression Language)来定义缓存的key和各种condition,因此具备相当的灵活性,并可以支持非常复杂的语义。
下面先给出一个使用案例,然后通过源码分析其实现原理及核心业务逻辑。

 

 

第二部分:SpringCache实现原理

 

 

1 Spring AOP介绍

SpringCache使用Spring AOP来实现,当我们在Configuration类打上@EnableCaching注释时,该标注通过ImportSelector机制启动AbstractAdvisorAutoProxyCreator的一个实例,该实例本身是一个Ordered BeanPostProcessor,BeanPostProcessor的作用是在bean创建及初始化的前后对其进行一些额外的操作(包括创建代理),关于BeanPostProcessor及其对bean创建时产生影响的介绍请参照我的另一片文章《BeanPostProcessor加载次序及其对Bean造成的影响分析》。而Spring AOP就是通过AbstractAdvisorAutoProxyCreator来实现的。

 

因此可以认为当在Configuration类打上@EnableCaching注释时,做的第一件事情就是启用Spring AOP机制。下面简单介绍一下Spring AOP机制的实现原理和核心概念,详细的实现原理与源码分析我会另写一篇文章专门说明。

 

从Spring2.0开始,引入了AspectJ的注释,使得定义切面变得非常简单。Spring采用AspectJ注释对POJO进行标注,从而定义一个包含切点信息和增强横切面逻辑的切面,而Spring的底层负责将这个切面织入到匹配的目标bean中。

 

AspectJ注释使用AspectJ切点表达式语法进行切点的定义,可以通过切点函数、运算符、通配符等高级功能进行切点定义,拥有强大的连接点描述能力。

 

在运行中,当创建并初始化一个bean的时候,Spring会判断适用于这个bean上的切点信息以及增强逻辑(advisors),若适用于本bean的advisors不为空,则将切点信息以及增强逻辑织入到这个新创建的bean中,切点及增强逻辑会放置在一个Advisor中,而每个bean有可能有很多个Advisor。织入的方式有两种:动态代理和静态织入。默认使用动态代理的方式。

 

而在实际调用到某个方法时,其实是通过创建代理时设置的回调函数作为调用入口(JdkProxy和cglib大同小异),其中会过滤出可以应用于本方法的Advisor并生成Interceptor或者InterceptorAndDynamicMethodMatcher数组,以类似于FilterChain的方式按照

 

interceptor1.pre()
interceptor2.pre()
.....
interceptorn.pre()
method.invoke()
interceptorn.aft()
......
interceptor2.aft()
interceptor1.aft()

 

的顺序来执行。

 

Spring AOP整体实现逻辑如下:

 

  1. 启动时收集beanfactory中定义的所有的切点Pointcuts及通知函数Advices(拦截器函数),将其封装为切面Advisors。
  2. 创建bean时,判断是否有针对该bean的Advisors,如果有,则为该bean创建代理,并将合适的Advisors封装进代理中。
  3. 在执行过程中,当调用到该bean的方法method时,其实入口是代理类的回调函数。在回调函数中将适用于本方法的Advice(Interceptor)和被调用函数method封装成一个JoinPoint(运行时连接点)使用类似FilterChain的模式来执行拦截器链及方法调用。

如此一来,就可以实现在调用某方法的时候在其前后定义若干的“切面”,在具体方法执行前后执行一些公共的增强逻辑。SpringAOP实现原理的细节请参考《SpringAOP基本概念及实现原理(三)

 

2 Spring Cache集成机制

如上所述,SpringCache使用Spring AOP面向切面编程的机制来实现,当我们在Configuration类打上@EnableCaching注释时,除了启动Spring AOP机制,引入的另一个类ProxyCachingConfiguration就是SpringCache具体实现相关bean的配置类。其代码如下:

 

 

@Configuration
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());
		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.setCacheOperationSources(cacheOperationSource());
		if (this.cacheResolver != null) {
			interceptor.setCacheResolver(this.cacheResolver);
		}
		else if (this.cacheManager != null) {
			interceptor.setCacheManager(this.cacheManager);
		}
		if (this.keyGenerator != null) {
			interceptor.setKeyGenerator(this.keyGenerator);
		}
		if (this.errorHandler != null) {
			interceptor.setErrorHandler(this.errorHandler);
		}
		return interceptor;
	}


}

我们可以看到在其中配置了三个bean:BeanFactoryCacheOperationSourceAdvisor、AnnotationCacheOperationSource、CacheInterceptor。

  • AnnotationCacheOperationSource的主要作用是获取定义在类和方法上的SpringCache相关的标注并将其转换为对应的CacheOperation属性。
  • BeanFactoryCacheOperationSourceAdvisor是一个PointcutAdvisor,是SpringCache使用Spring AOP机制的关键所在,该advisor会织入到需要执行缓存操作的bean的增强代理中形成一个切面。并在方法调用时在该切面上执行拦截器CacheInterceptor的业务逻辑。
  • CacheInterceptor是一个拦截器,当方法调用时碰到了BeanFactoryCacheOperationSourceAdvisor定义的切面,就会执行CacheInterceptor的业务逻辑,该业务逻辑就是缓存的核心业务逻辑。

下面就详细介绍各个Bean及其相关概念:

 

2.1 AnnotationCacheOperationSource

首先分析一下AnnotationCacheOperationSource的源码,其继承路径是

 

AnnotationCacheOperationSource->AbstractFallbackCacheOperationSource->CacheOperationSource

 

其中CacheOperationSource接口定义了一个方法:

Collection<CacheOperation> getCacheOperations(Method method, Class<?> targetClass);

该方法用于根据指定类上的指定方法上打的SpringCache注释来得到对应的CacheOperation集合。

 

AbstractFallbackCacheOperationSource是CacheOperationSource的抽象实现类,采用模板模式将获取某类的某方法上的CacheOperation的业务流程固化。该固化的流程可将方法上的属性缓存,并实现了一个获取CacheOperation的fallback策略,执行的顺序为:1目标方法、2目标类、3声明方法、4声明类/接口。当方法被调用过一次之后,其上的属性就会被缓存。它提供了两个抽象模板方法,供具体的子类类来实现:

protected abstract Collection<CacheOperation> findCacheOperations(Method method);
protected abstract Collection<CacheOperation> findCacheOperations(Class<?> clazz);

 

AnnotationCacheOperationSource内部持有一个Set<CacheAnnotaionParser>的集合,默认包含SpringCacheAnnotationParser,并使用SpringCacheAnnotationParser来实现AbstractFallbackCacheOperationSource定义的两个抽象方法:

 

 

	@Override
	protected Collection<CacheOperation> findCacheOperations(final Class<?> clazz) {
		return determineCacheOperations(new CacheOperationProvider() {
			@Override
			public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
				return parser.parseCacheAnnotations(clazz);
			}
		});


	}


	@Override
	protected Collection<CacheOperation> findCacheOperations(final Method method) {
		return determineCacheOperations(new CacheOperationProvider() {
			@Override
			public Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser) {
				return parser.parseCacheAnnotations(method);
			}
		});
	}


	/**
	 * Determine the cache operation(s) for the given {@link CacheOperationProvider}.
	 * <p>This implementation delegates to configured
	 * {@link CacheAnnotationParser}s for parsing known annotations into
	 * Spring's metadata attribute class.
	 * <p>Can be overridden to support custom annotations that carry
	 * caching metadata.
	 * @param provider the cache operation provider to use
	 * @return the configured caching operations, or {@code null} if none found
	 */
	protected Collection<CacheOperation> determineCacheOperations(CacheOperationProvider provider) {
		Collection<CacheOperation> ops = null;
		for (CacheAnnotationParser annotationParser : this.annotationParsers) {
			Collection<CacheOperation> annOps = provider.getCacheOperations(annotationParser);
			if (annOps != null) {
				if (ops == null) {
					ops = new ArrayList<CacheOperation>();
				}
				ops.addAll(annOps);
			}
		}
		return ops;
	}
	/**
	 * Callback interface providing {@link CacheOperation} instance(s) based on
	 * a given {@link CacheAnnotationParser}.
	 */
	protected interface CacheOperationProvider {


		/**
		 * Returns the {@link CacheOperation} instance(s) provided by the specified parser.
		 *
		 * @param parser the parser to use
		 * @return the cache operations or {@code null} if none is found
		 */
		Collection<CacheOperation> getCacheOperations(CacheAnnotationParser parser);
	}

 

 

 

具体实现使用回调模式,用Set<CacheAnnotaionParser>中的每一个CacheAnnotaionParser去解析一个方法或类,然后将得到的List<CacheOperation>合并,最终返回。

 

接着看一看其中使用的CacheAnnotaionParser,该接口定义了两个方法:

Collection<CacheOperation> parseCacheAnnotations(Class<?> type);	//解析类上的标注,并相应创建CacheOperation
Collection<CacheOperation> parseCacheAnnotations(Method method);	//解析方法上的标注,并相应创建CacheOperation


其默认实现类为SpringCacheAnnotationParser,在其内部对SpringCache的几个标注@Cacheable、@CachePut、@CacheEvict、@Caching进行了解析,并相应的创建CacheableOperation、CacheEvictOperation、CachePutOperation,核心方法如下:

 

protected Collection<CacheOperation> parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) {
		Collection<CacheOperation> ops = null;


		Collection<Cacheable> cacheables = AnnotatedElementUtils.findAllMergedAnnotations(ae, Cacheable.class);
		if (!cacheables.isEmpty()) {
			ops = lazyInit(ops);
			for (Cacheable cacheable : cacheables) {
				ops.add(parseCacheableAnnotation(ae, cachingConfig, cacheable));
			}
		}
		Collection<CacheEvict> evicts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CacheEvict.class);
		if (!evicts.isEmpty()) {
			ops = lazyInit(ops);
			for (CacheEvict evict : evicts) {
				ops.add(parseEvictAnnotation(ae, cachingConfig, evict));
			}
		}
		Collection<CachePut> puts = AnnotatedElementUtils.findAllMergedAnnotations(ae, CachePut.class);
		if (!puts.isEmpty()) {
			ops = lazyInit(ops);
			for (CachePut put : puts) {
				ops.add(parsePutAnnotation(ae, cachingConfig, put));
			}
		}
		Collection<Caching> cachings = AnnotatedElementUtils.findAllMergedAnnotations(ae, Caching.class);
		if (!cachings.isEmpty()) {
			ops = lazyInit(ops);
			for (Caching caching : cachings) {
				Collection<CacheOperation> cachingOps = parseCachingAnnotation(ae, cachingConfig, caching);
				if (cachingOps != null) {
					ops.addAll(cachingOps);
				}
			}
		}


		return ops;
}

参数中AnnotatedElement是Class和Method的父类,代表了一个可被注释的元素,该参数可以是一个Class也可以是一个Method。

 

 

 

该方法业务逻辑很清晰:首先查找该类/方法上的Cacheable标注并进行合并。针对合并后的每个Cacheable创建对应的CacheableOperation;然后同样逻辑执行CacheEvict和CachePut。最后处理Caching,Caching表示的是若干组Cache标注的集合,将其解析成一组CacheOperation并添加到Collection<CacheOperation> ops中。

2.2 CacheInterceptor

CacheInterceptor继承了CacheAspectSupport并实现了MethodInterceptor接口,因此它本质上是一个Advice也就是可在切面上执行的增强逻辑。CacheInterceptor切面的拦截方法代码如下:

 

	@Override
	public Object invoke(final MethodInvocation invocation) throws Throwable {
		Method method = invocation.getMethod();


		CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() {
			@Override
			public Object invoke() {
				try {
					return invocation.proceed();
				}
				catch (Throwable ex) {
					throw new ThrowableWrapper(ex);
				}
			}
		};


		try {
			return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
		}
		catch (CacheOperationInvoker.ThrowableWrapper th) {
			throw th.getOriginal();
		}
	}

这个增强逻辑的核心功能是在CacheAspectSupport中实现的,其中首先调用AnnotationCacheOperationSource.getCacheOperations(method, targetClass)方法得到被调用方法的Collection<CacheOperation>,然后将这些CacheOperation以及被调用方法、调用参数、目标类、相应的Cache信息统统封装到CacheOperation上下文里,随后调用真正的核心方法:

 

private Object execute(final CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts)

该方法封装了SpringCache核心的处理逻辑,也就是使用Cache配合来完成用户的方法调用,并返回结果。该核心处理逻辑将在下一篇详细分析。

 

2.3 BeanFactoryCacheOperationSourceAdvisor

最后,BeanFactoryCacheOperationSourceAdvisor就是一个上面Spring AOP介绍中的Advisor,具体来讲是一个PointcutAdvisor。它负责将CacheInterceptor与CacheOperationSourcePointcut结合起来。其内部注入了AnnotationCacheOperationSource,并创建了CacheOperationSourcePointcut:

 

	private CacheOperationSource cacheOperationSource;


	private final CacheOperationSourcePointcut pointcut = new CacheOperationSourcePointcut() {
		@Override
		protected CacheOperationSource getCacheOperationSource() {
			return cacheOperationSource;
		}
	};


BeanFactoryCacheOperationSourceAdvisor利用Spring AOP的面向切面机制,将配置了SpringCache相关注释的类进行代理增强,并加入到其advisors处理链中。在bean创建的时候,如果需要Spring AOP代理增强,会首先取出beanfactory中所有的advisors,然后过滤出适合该Bean的advisors,加入到代理类中。其中对于PointcutAdvisor类型的advisor是通过

 

//逻辑抽取,非实际源码
advisor.getPointcut().getMethodMatcher().matches(method, targetClass)

来判断该advisor是否适合用于被创建的Bean。因此最终会调用到CacheOperationSourcePointcut的matches方法,代码如下:

 

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

 

结合上面的代码,最终会调用AnnotationCacheOperationSource.getCacheOperations(method, targetClass)方法。因此matches方法的意思就是:如果bean目标类的任何一个方法存在SpringCache相关的标注从而可以获得List<CacheOperation>,那么该bean需要由BeanFactoryCacheOperationSourceAdvisor来做切面增强,参见上面配置类ProxyCachingConfiguration中的定义:

@Bean(name = CacheManagementConfigUtils.CACHE_ADVISOR_BEAN_NAME)

		

	@Role(BeanDefinition.ROLE_INFRASTRUCTURE)

		

	public BeanFactoryCacheOperationSourceAdvisor cacheAdvisor() {

		

		BeanFactoryCacheOperationSourceAdvisor advisor =

		

				new BeanFactoryCacheOperationSourceAdvisor();

		

		advisor.setCacheOperationSource(cacheOperationSource());

		

		advisor.setAdvice(cacheInterceptor());

		

		advisor.setOrder(this.enableCaching.<Integer>getNumber("order"));

		

		return advisor;

		

	}

BeanFactoryCacheOperationSourceAdvisor切入后执行的业务逻辑就是CacheInterceptor中的invoke(MethodInvocation invocation)方法。在该方法中,调用其父类CacheAspectSupport中的方法来完成缓存的核心处理逻辑,并返回结果。

 

 

至此,Spring Cache的实现原理已经介绍完毕,关于缓存核心处理逻辑将在下一篇详细分析。

 

3 SpringBoot自动配置机制

当我们在SpringBoot中使用默认实现时,由于其自动配置机制,我们甚至都不需要自己配置CacheManager。在spring-boot-autoconfigure模块里有专门配置SpringCache的配置类:

 

 

@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
.....................
@Import({ CacheManagerCustomizers.class, CacheConfigurationImportSelector.class })
public class CacheAutoConfiguration {


	..................


	/**
	 * {@link ImportSelector} to add {@link CacheType} configuration classes.
	 */
	static class CacheConfigurationImportSelector implements ImportSelector {


		@Override
		public String[] selectImports(AnnotationMetadata importingClassMetadata) {
			CacheType[] types = CacheType.values();
			String[] imports = new String[types.length];
			for (int i = 0; i < types.length; i++) {
				imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
			}
			return imports;
		}


	}


}

意思是当满足这些条件:

 

  • CacheManager.class存在;
  • CacheAspectSupport对应的bean存在;
  • CacheManager对应的bean不存在;
  • 等等......;

 

时CacheAutoConfiguration配置类就生效。其中CacheAspectSupport我们在上面已经介绍过了,它是CacheInterceptor的父类,SpringCache真正的核心业务逻辑是由它实现的。当打上@CacheEnable注释时,自动配置了CacheInterceptor的bean,也就是CacheAspectSupport的bean,因此肯定存在。

 

CacheAutoConfiguration通过@Import机制引入CacheManagerCustomizers.class和CacheConfigurationImportSelector.class。其中CacheConfigurationImportSelector使用CacheType.values()作为Key,遍历并创建将要加载的配置类全名的字符串数组。枚举CacheType的代码为:

public enum CacheType {


	/**
	 * Generic caching using 'Cache' beans from the context.
	 */
	GENERIC,


	/**
	 * JCache (JSR-107) backed caching.
	 */
	JCACHE,


	/**
	 * EhCache backed caching.
	 */
	EHCACHE,


	/**
	 * Hazelcast backed caching.
	 */
	HAZELCAST,


	/**
	 * Infinispan backed caching.
	 */
	INFINISPAN,


	/**
	 * Couchbase backed caching.
	 */
	COUCHBASE,


	/**
	 * Redis backed caching.
	 */
	REDIS,


	/**
	 * Caffeine backed caching.
	 */
	CAFFEINE,


	/**
	 * Guava backed caching.
	 */
	GUAVA,


	/**
	 * Simple in-memory caching.
	 */
	SIMPLE,


	/**
	 * No caching.
	 */
	NONE;


}

我们可以看到,其中预定义了11种不同的cache类型。同时在CacheConfigurations类中定义了各不同类型的cache对应的配置类,代码如下:

 

final class CacheConfigurations {


	private static final Map<CacheType, Class<?>> MAPPINGS;


	static {
		Map<CacheType, Class<?>> mappings = new HashMap<CacheType, Class<?>>();
		mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
		mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
		mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
		mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
		mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
		mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
		mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
		mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
		mappings.put(CacheType.GUAVA, GuavaCacheConfiguration.class);
		mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
		mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
		MAPPINGS = Collections.unmodifiableMap(mappings);
	}


	private CacheConfigurations() {
	}


	public static String getConfigurationClass(CacheType cacheType) {
		Class<?> configurationClass = MAPPINGS.get(cacheType);
		Assert.state(configurationClass != null, "Unknown cache type " + cacheType);
		return configurationClass.getName();
	}


	public static CacheType getType(String configurationClassName) {
		for (Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
			if (entry.getValue().getName().equals(configurationClassName)) {
				return entry.getKey();
			}
		}
		throw new IllegalStateException(
				"Unknown configuration class " + configurationClassName);
	}


}

这些配置类各自生效的条件并不相同:

 

  • GenericCacheConfiguration的生效条件是存在Cache的bean但是不存在CacheManager的bean,并且没有定义spring.cache.type或者spring.cache.type的值为generic。
  • SimpleCacheConfiguration的生效条件是不存在CacheManager的bean,并且没有定义spring.cache.type或者spring.cache.type的值为simple。
  • NoOpCacheConfiguration的生效条件是不存在CacheManager的bean,并且没有定义spring.cache.type或者spring.cache.type的值为none。
  • 其他的配置类都有各自额外的要求,例如需要引入相应的类库支持。

因此当我们没有引入任何其他类库,没有配置Cache bean并且没有指定spring.cache.type时,从上到下判断,GenericCacheConfiguration不起作用(未定义Cache bean)、后续的一系列与第三方存储实现方案集成的配置类也不起作用(未引入相应类库),最后轮到SimpleCacheConfiguration符合条件起作用了。因此此时使用SimpleCacheConfiguration来进行SpringCache的配置:

 

 

@Configuration
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class SimpleCacheConfiguration {


	private final CacheProperties cacheProperties;


	private final CacheManagerCustomizers customizerInvoker;


	SimpleCacheConfiguration(CacheProperties cacheProperties,
			CacheManagerCustomizers customizerInvoker) {
		this.cacheProperties = cacheProperties;
		this.customizerInvoker = customizerInvoker;
	}


	@Bean
	public ConcurrentMapCacheManager cacheManager() {
		ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager();
		List<String> cacheNames = this.cacheProperties.getCacheNames();
		if (!cacheNames.isEmpty()) {
			cacheManager.setCacheNames(cacheNames);
		}
		return this.customizerInvoker.customize(cacheManager);
	}


}

该配置类指明使用ConcurrentMapCacheManager作为CacheManager bean,在其内部使用缓存方法的注释中指明的cache名称来创建ConcurrentMapCache类型的cache,创建cache的代码如下:

 

public void setCacheNames(Collection<String> cacheNames) {
		if (cacheNames != null) {
			for (String name : cacheNames) {
				this.cacheMap.put(name, createConcurrentMapCache(name));
			}
			this.dynamic = false;
		}
		else {
			this.dynamic = true;
		}
	}

如此一来,由于SpringBoot的自动配置机制, 我们只需要打上@EnableCaching标注就可以启动SpringCache机制,使用其开箱即用的缓存实现方案。

 

 

小结: 本篇首先介绍了Spring AOP的实现原理,然后介绍了SpringCache如何通过Spring AOP的面向切面编程模式来实现通过打注释就能使用缓存机制的实现原理,最后介绍了一下SpringBoot对于SpringCache的约定大于配置的自动配置实现原理。希望通过本篇介绍,可以让大家对于SpringCache、Spring AOP、SpringBoot autoconfiguration约定大于配置等机制有更深入的了解。

 

 

 

 

 

 

 

相关文章:SpringCache实现原理及核心业务逻辑(一)

相关文章:SpringCache实现原理及核心业务逻辑(三)

 

 

  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值