SpringBoot自动配置原理解析(二)

关联博文:
SpringBoot自动配置原理解析(一)
SpringBoot自动配置原理解析(二)
SpringBoot自动配置原理解析(三)
SpringBoot自动配置原理解析(四)
SpringBoot自动配置原理解析(五)

SpringBoot自动配置原理解析(一)后,我们继续分析SpringBoot如何进行自动配置的。

前面我们从表现上跟踪了一下SpringBoot自动配置的大概过程,但是并没有说明处理器、环节等信息,本文从源码角度我们看一下。

首先说明一下,BeanDefinition的扫描注册发生在refresh过程中的invokeBeanFactoryPostProcessors方法中,具体处理器是ConfigurationClassPostProcessor,bean实例化则是发生在refresh方法中的finishBeanFactoryInitialization环节。

Spring中refresh分析之invokeBeanFactoryPostProcessors方法详解一文中我们简要分析了PostProcessorRegistrationDelegateinvokeBeanFactoryPostProcessors方法。其对AbstractApplicationContext的成员beanFactoryPostProcessors处理完后,就会从BeanFactory这个容器中找寻BeanDefinitionRegistryPostProcessor,并优先对那些实现了PriorityOrdered接口的类遍历挨个触发其postProcessBeanDefinitionRegistry方法。

在这里插入图片描述

private static void invokeBeanDefinitionRegistryPostProcessors(
		Collection<? extends BeanDefinitionRegistryPostProcessor> postProcessors, BeanDefinitionRegistry registry) {

	for (BeanDefinitionRegistryPostProcessor postProcessor : postProcessors) {
		postProcessor.postProcessBeanDefinitionRegistry(registry);
	}
}

本文这里从容器BeanFactory中得到的(注意哦,不是从ApplicationContext的成员中得到)同时实现接口BeanDefinitionRegistryPostProcessor&PriorityOrdered,只有后置处理器ConfigurationClassPostProcessor。其是在AnnotationConfigUtilsregisterAnnotationConfigProcessors方法中被注册到BeanFactory中的。

【1】ConfigurationClassPostProcessor

如下图所示,其首先判断当前BeanDefinitionRegistry(本文这里是指DefaultListableBeanFactory)是否触发过ConfigurationClassPostProcessor,如果触发过则抛出异常,否则触发processConfigBeanDefinitions方法。
在这里插入图片描述
processConfigBeanDefinitions方法源码

// ConfigurationClassPostProcessor
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) {
	List<BeanDefinitionHolder> configCandidates = new ArrayList<>();

// 候选配置类名称,首先从registry获取 这里registry是DefaultListableBeanFactory
	String[] candidateNames = registry.getBeanDefinitionNames();

// 遍历循环,判断其是否作为一个configuration 类被处理过,如果没有则放入configCandidates 
	for (String beanName : candidateNames) {
		BeanDefinition beanDef = registry.getBeanDefinition(beanName);
		if (beanDef.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE) != null) {
			if (logger.isDebugEnabled()) {
				logger.debug("Bean definition has already been processed as a configuration class: " + beanDef);
			}
		}
		else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
			configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
		}
	}

	// Return immediately if no @Configuration classes were found
	//如果为空则直接返回
	if (configCandidates.isEmpty()) {
		return;
	}

	// Sort by previously determined @Order value, if applicable
	//根据@Order排序
	configCandidates.sort((bd1, bd2) -> {
		int i1 = ConfigurationClassUtils.getOrder(bd1.getBeanDefinition());
		int i2 = ConfigurationClassUtils.getOrder(bd2.getBeanDefinition());
		return Integer.compare(i1, i2);
	});

	// Detect any custom bean name generation strategy supplied through the enclosing application context
	// 尝试获取自定义的internalConfigurationBeanNameGenerator
	SingletonBeanRegistry sbr = null;
	if (registry instanceof SingletonBeanRegistry) {
		sbr = (SingletonBeanRegistry) registry;
		if (!this.localBeanNameGeneratorSet) {
		// 本文这里获取的为null
			BeanNameGenerator generator = (BeanNameGenerator) sbr.getSingleton(
					AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR);
			if (generator != null) {
				this.componentScanBeanNameGenerator = generator;
				this.importBeanNameGenerator = generator;
			}
		}
	}
// 如果环境为null,实例化一个环境
	if (this.environment == null) {
		this.environment = new StandardEnvironment();
	}

	// Parse each @Configuration class
	// 实例化ConfigurationClassParser ,这个玩意将会用来解析配置类
	ConfigurationClassParser parser = new ConfigurationClassParser(
			this.metadataReaderFactory, this.problemReporter, this.environment,
			this.resourceLoader, this.componentScanBeanNameGenerator, registry);

	//候选配置类--当前要被解析的配置类
	Set<BeanDefinitionHolder> candidates = new LinkedHashSet<>(configCandidates);
	// 已经执行过parse的
	Set<ConfigurationClass> alreadyParsed = new HashSet<>(configCandidates.size());

// 注意,这里是do  while循环 这个方法执行之后就拥有了当前应用的所有配置类
	do {
	// 解析配置类,
		parser.parse(candidates);
		
	// 校验类是否为final,beanMethod是否覆盖
		parser.validate();

// 获取当前解析器拥有的配置类,移除掉已经解析过的
		Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
		//第一次没有意义,后续循环有意义
		configClasses.removeAll(alreadyParsed);

		// Read the model and create bean definitions based on its content
		if (this.reader == null) {
			this.reader = new ConfigurationClassBeanDefinitionReader(
					registry, this.sourceExtractor, this.resourceLoader, this.environment,
					this.importBeanNameGenerator, parser.getImportRegistry());
		}
// 实例化一个reader然后加载配置类涉及到的BeanDefinition,这个很重要!
		this.reader.loadBeanDefinitions(configClasses);

		//放入已解析集合
		alreadyParsed.addAll(configClasses);
		//清空candidates
		candidates.clear();
		// 判断registry中的BeanDefinition数量与candidateNames是否一致
		// 如果不一致就检索哪些新引入且未解析的候选配置类放到candidates中
		if (registry.getBeanDefinitionCount() > candidateNames.length) {
			String[] newCandidateNames = registry.getBeanDefinitionNames();
			Set<String> oldCandidateNames = new HashSet<>(Arrays.asList(candidateNames));
			Set<String> alreadyParsedClasses = new HashSet<>();
			for (ConfigurationClass configurationClass : alreadyParsed) {
				alreadyParsedClasses.add(configurationClass.getMetadata().getClassName());
			}
			//找到oldCandidateNames不包含且属于候选配置类,且未被解析的
			for (String candidateName : newCandidateNames) {
				if (!oldCandidateNames.contains(candidateName)) {
					BeanDefinition bd = registry.getBeanDefinition(candidateName);
					if (ConfigurationClassUtils.checkConfigurationClassCandidate(bd, this.metadataReaderFactory) &&
							!alreadyParsedClasses.contains(bd.getBeanClassName())) {
						candidates.add(new BeanDefinitionHolder(bd, candidateName));
					}
				}
			}
			candidateNames = newCandidateNames;
		}
	}
	// 如果candidates不为空,则再次循环解析
	while (!candidates.isEmpty());

	// Register the ImportRegistry as a bean in order to support ImportAware @Configuration classes
	//注册单例bean ImportRegistry 
	if (sbr != null && !sbr.containsSingleton(IMPORT_REGISTRY_BEAN_NAME)) {
		sbr.registerSingleton(IMPORT_REGISTRY_BEAN_NAME, parser.getImportRegistry());
	}

// 清空缓存
	if (this.metadataReaderFactory instanceof CachingMetadataReaderFactory) {
		// Clear cache in externally provided MetadataReaderFactory; this is a no-op
		// for a shared cache since it'll be cleared by the ApplicationContext.
		((CachingMetadataReaderFactory) this.metadataReaderFactory).clearCache();
	}
}

① 遍历循环检测是否为候选配置类

原始的candidateNames也就是registry.getBeanDefinitionNames()获取的如下所示:

// ConfigurationClassPostProcessor
0 = "org.springframework.context.annotation.internalConfigurationAnnotationProcessor"
// AutowiredAnnotationBeanPostProcessor
1 = "org.springframework.context.annotation.internalAutowiredAnnotationProcessor"
// CommonAnnotationBeanPostProcessor
2 = "org.springframework.context.annotation.internalCommonAnnotationProcessor"
// EventListenerMethodProcessor
3 = "org.springframework.context.event.internalEventListenerProcessor"
// DefaultEventListenerFactory
4 = "org.springframework.context.event.internalEventListenerFactory"
5 = "recommendApplication"
// SharedMetadataReaderFactoryContextInitializer$SharedMetadataReaderFactoryBean
6 = "org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory"

然后会从上面得到的candidateNames进行遍历循环,判断其是否作为一个configuration 类被处理过,如果没有则放入configCandidates 。

else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this.metadataReaderFactory)) {
	configCandidates.add(new BeanDefinitionHolder(beanDef, beanName));
}

如何判断呢?如下几种情况直接排除:

  • 有FactoryMethodName;
  • BeanFactoryPostProcessor、BeanPostProcessor、AopInfrastructureBean、EventListenerFactory类型均不行;
  • 其是一个接口
  • 没有@Configuration注解标注也没有@Component、@ComponentScan、@Import、@ImportResource标注;
  • 没有包含@Bean标注的方法。

本文这里第一次遍历筛选后只剩下主启动类RecommendApplication。

② 解析配置类

首先实例化ConfigurationClassParser,然后对前面筛选后的候选配置类进行解析。如下图所示,这里会触发ConfigurationClassParser的parse方法。

public void parse(Set<BeanDefinitionHolder> configCandidates) {
	for (BeanDefinitionHolder holder : configCandidates) {
		BeanDefinition bd = holder.getBeanDefinition();
		try {
		// 本文从这里进入
			if (bd instanceof AnnotatedBeanDefinition) {
				parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
			}
			else if (bd instanceof AbstractBeanDefinition && ((AbstractBeanDefinition) bd).hasBeanClass()) {
				parse(((AbstractBeanDefinition) bd).getBeanClass(), holder.getBeanName());
			}
			else {
				parse(bd.getBeanClassName(), holder.getBeanName());
			}
		}
		catch (BeanDefinitionStoreException ex) {
			throw ex;
		}
		catch (Throwable ex) {
			throw new BeanDefinitionStoreException(
					"Failed to parse configuration class [" + bd.getBeanClassName() + "]", ex);
		}
	}
// 这个方法也很重要 !!! 从spring.factories中检索配置类就是在这里触发的
	this.deferredImportSelectorHandler.process();
}

在这里插入图片描述

protected final void parse(AnnotationMetadata metadata, String beanName) throws IOException {
// 这里为当前需要解析的beanName实例化了一个ConfigurationClasss实例交给下游
	processConfigurationClass(new ConfigurationClass(metadata, beanName));
}

protected void processConfigurationClass(ConfigurationClass configClass) throws IOException {
// 判断是否可以跳过
	if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) {
		return;
	}
// Map<ConfigurationClass, ConfigurationClass> configurationClasses
//这里判断配置类是否已经存在,如果存在则合并或者替换
	ConfigurationClass existingClass = this.configurationClasses.get(configClass);
	if (existingClass != null) {
		if (configClass.isImported()) {
			if (existingClass.isImported()) {
				existingClass.mergeImportedBy(configClass);
			}
			// Otherwise ignore new imported config class; existing non-imported class overrides it.
			return;
		}
		else {
			// Explicit bean definition found, probably replacing an import.
			// Let's remove the old one and go with the new one.
			this.configurationClasses.remove(configClass);
			this.knownSuperclasses.values().removeIf(configClass::equals);
		}
	}

	// Recursively process the configuration class and its superclass hierarchy.
	//递归处理配置类及其父类,这里会根据configClass获取到SourceClass 
	SourceClass sourceClass = asSourceClass(configClass);
	do {
	// 核心解析方法
		sourceClass = doProcessConfigurationClass(configClass, sourceClass);
	}
	while (sourceClass != null);

	// 放入configurationClasses这个LinkedHashMap中 ,后面会使用
	this.configurationClasses.put(configClass, configClass);
}

这里doProcessConfigurationClass方法是真正解析配置的方法,是本文讲述的核心的方法。

【2】核心方法doProcessConfigurationClass

这个方法将会解析当前配置类对应的SourceClass 那些注解、导入以及@Bean Method。

protected final SourceClass doProcessConfigurationClass(ConfigurationClass configClass, SourceClass sourceClass)
		throws IOException {
// 如果标注了@Component注解,则首先尝试处理成员类(内部类)
	if (configClass.getMetadata().isAnnotated(Component.class.getName())) {
		// Recursively process any member (nested) classes first
		processMemberClasses(configClass, sourceClass);
	}

	// Process any @PropertySource annotations
	// 解析@PropertySource注解,将会添加配置信息到环境中
	for (AnnotationAttributes propertySource : AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), PropertySources.class,
			org.springframework.context.annotation.PropertySource.class)) {
		if (this.environment instanceof ConfigurableEnvironment) {
			processPropertySource(propertySource);
		}
		else {
			logger.info("Ignoring @PropertySource annotation on [" + sourceClass.getMetadata().getClassName() +
					"]. Reason: Environment must implement ConfigurableEnvironment");
		}
	}

	// Process any @ComponentScan annotations
	// 解析@ComponentScan注解,使用ComponentScanAnnotationParser解析器
	// 扫描@Configuration、@Service、@Controller、@Repository和@Component注解并注册BeanDefinition
	Set<AnnotationAttributes> componentScans = AnnotationConfigUtils.attributesForRepeatable(
			sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);
	if (!componentScans.isEmpty() &&
			!this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) {
		for (AnnotationAttributes componentScan : componentScans) {
			// The config class is annotated with @ComponentScan -> perform the scan immediately
			//获取扫描解析到的BeanDefinition
			Set<BeanDefinitionHolder> scannedBeanDefinitions =
					this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName());
			// Check the set of scanned definitions for any further config classes and parse recursively if needed
			// 对scannedBeanDefinitions遍历,检测是否为候选配置类,触发parse
			//也就是对扫描到的类再次触发解析过程,递归解析
			for (BeanDefinitionHolder holder : scannedBeanDefinitions) {
				BeanDefinition bdCand = holder.getBeanDefinition().getOriginatingBeanDefinition();
				if (bdCand == null) {
					bdCand = holder.getBeanDefinition();
				}
				if (ConfigurationClassUtils.checkConfigurationClassCandidate(bdCand, this.metadataReaderFactory)) {
					parse(bdCand.getBeanClassName(), holder.getBeanName());
				}
			}
		}
	}

// Process any @Import annotations
// 解析@Import注解,然后进行实例化,并执行ImportBeanDefinitionRegistrar的
//registerBeanDefinitions逻辑,或者ImportSelector的selectImports逻辑 或者再次触发解析
	processImports(configClass, sourceClass, getImports(sourceClass), true);

	// Process any @ImportResource annotations
	// 解析@ImportResource注解,并加载相关配置信息
	AnnotationAttributes importResource =
			AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class);
	if (importResource != null) {
		String[] resources = importResource.getStringArray("locations");
		Class<? extends BeanDefinitionReader> readerClass = importResource.getClass("reader");
		for (String resource : resources) {
			String resolvedResource = this.environment.resolveRequiredPlaceholders(resource);
			configClass.addImportedResource(resolvedResource, readerClass);
		}
	}

// Process individual @Bean methods
//解析标注了@Bean注解的方法并注册到configclass中Set<BeanMethod> beanMethods里
	Set<MethodMetadata> beanMethods = retrieveBeanMethodMetadata(sourceClass);
	for (MethodMetadata methodMetadata : beanMethods) {
		configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass));
	}

	// Process default methods on interfaces
	// 解析其实现接口中标注了@Bean注解的方法并注册到configclass中Set<BeanMethod> beanMethods里
	processInterfaces(configClass, sourceClass);

	// Process superclass, if any
	// 尝试处理其父类
	if (sourceClass.getMetadata().hasSuperClass()) {
		String superclass = sourceClass.getMetadata().getSuperClassName();
		if (superclass != null && !superclass.startsWith("java") &&
				!this.knownSuperclasses.containsKey(superclass)) {
			this.knownSuperclasses.put(superclass, configClass);
			// Superclass found, return its annotation metadata and recurse
			return sourceClass.getSuperClass();
		}
	}

	// No superclass -> processing is complete
	return null;
}

核心流程梳理如下:

  • 如果标注了@Component注解,则首先尝试处理成员类(内部类)
  • 解析@PropertySource注解,将会添加配置信息到环境中
  • 解析@ComponentScan注解,使用ComponentScanAnnotationParser解析器扫描@Configuration(复合注解标注了@Component)、@Service、@Controller、@Repository和@Component注解并注册BeanDefinition
  • 解析@Import注解,然后进行实例化,并执行ImportBeanDefinitionRegistrar的registerBeanDefinitions逻辑,或者ImportSelector的selectImports逻辑,或者再次触发配置类解析方法processConfigurationClass
  • 解析@ImportResource注解,并加载相关配置信息
  • 解析标注了@Bean注解的方法并注册到configclass的Set<BeanMethod> beanMethods
  • 解析其实现接口中标注了@Bean注解的方法并注册到configclass中Set<BeanMethod> beanMethods

① 处理导入processImports

这个方法是用来处理导入的类的,分为三个分支:

  • 如果导入类是ImportSelectorl类型,则进行实例化:
    • 如果是DeferredImportSelector,则触发其handle方法,我们@EnableAutoConfiguration注解引入的AutoConfigurationImportSelector就是DeferredImportSelector类型。
    • 否则触发其selectImports方法,递归调用processImports
  • ImportBeanDefinitionRegistrar如果是该类型,实例化后添加到当前configClass的成员importBeanDefinitionRegistrars中。AutoConfigurationPackages.Registrar就被这样处理了
  • 放入importStack,作为配置类触发processConfigurationClass方法再次进行解析
// ConfigurationClassParser
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
		Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
// 如果导入候选为空,直接返回
	if (importCandidates.isEmpty()) {
		return;
	}
// 检测是否循环导入
	if (checkForCircularImports && isChainedImportOnStack(configClass)) {
		this.problemReporter.error(new CircularImportProblem(configClass, this.importStack));
	}
	else {
	// 放入当前configClass
		this.importStack.push(configClass);
		try {
			for (SourceClass candidate : importCandidates) {
			// 判断是否为ImportSelector类型
				if (candidate.isAssignable(ImportSelector.class)) {
					// Candidate class is an ImportSelector -> delegate to it to determine imports
					Class<?> candidateClass = candidate.loadClass();
					ImportSelector selector = ParserStrategyUtils.instantiateClass(candidateClass, ImportSelector.class,
							this.environment, this.resourceLoader, this.registry);
					if (selector instanceof DeferredImportSelector) {
						this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);
					}
					else {
//触发selector的selectImports方法,得到importSourceClasses ,递归调用processImports
						String[] importClassNames = selector.selectImports(currentSourceClass.getMetadata());
						Collection<SourceClass> importSourceClasses = asSourceClasses(importClassNames);
						processImports(configClass, currentSourceClass, importSourceClasses, false);
					}
				}
				else if 
				// 判断是否为ImportBeanDefinitionRegistrar类型
				(candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) {
					// Candidate class is an ImportBeanDefinitionRegistrar ->
					// delegate to it to register additional bean definitions
					Class<?> candidateClass = candidate.loadClass();
					ImportBeanDefinitionRegistrar registrar =
							ParserStrategyUtils.instantiateClass(candidateClass, ImportBeanDefinitionRegistrar.class,
									this.environment, this.resourceLoader, this.registry);
					configClass.addImportBeanDefinitionRegistrar(registrar, currentSourceClass.getMetadata());
				}
				else {
					// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
					// process it as an @Configuration class
					// 作为 @Configuration class进行处理
					this.importStack.registerImport(
							currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
//candidate作为配置类再次触发解析,importedBy 中将放入configClass
					processConfigurationClass(candidate.asConfigClass(configClass));
				}
			}
		}
		//...两个异常
		finally {
		// 从importStack移除顶层结点
			this.importStack.pop();
		}
	}
}

这里我们要特别注意processConfigurationClass(candidate.asConfigClass(configClass));这行代码。首先这里candidate.asConfigClass(configClass)也就是导入的候选类作为配置类,这里会得到一个新的配置类ConfigurationClass 实例并向其成员集合importedBy中添加configClass表明其是被configClass导入的。然后再次触发配置类解析过程。

public ConfigurationClass asConfigClass(ConfigurationClass importedBy) {
	if (this.source instanceof Class) {
		return new ConfigurationClass((Class<?>) this.source, importedBy);
	}
	return new ConfigurationClass((MetadataReader) this.source, importedBy);
}

//像成员importedBy添加ConfigurationClass importedBy
public ConfigurationClass(MetadataReader metadataReader, @Nullable ConfigurationClass importedBy) {
	this.metadata = metadataReader.getAnnotationMetadata();
	this.resource = metadataReader.getResource();
	this.importedBy.add(importedBy);
}

为什么这里我们特别强调了importedBy这个LinkedHashSet呢?因为 在后面使用ConfigurationClassBeanDefinitionReader加载BeanDefinition时,importedBy不为空的配置类会被注册BeanDefinition到BeanFactory中。

② processImports过程中,我们的AutoConfigurationImportSelector做了什么?

如下所示,在processImports过程中如果当前selector 是DeferredImportSelector实例的话,那么将会触发下面这个方法。

this.deferredImportSelectorHandler.handle(configClass, (DeferredImportSelector) selector);

在这里插入图片描述
我们看下其handle方法,这里的逻辑是首先实例化DeferredImportSelectorHolder,然后将holder 放到this.deferredImportSelectors这个ArrayList中并没有触发if逻辑走的是else分支。

public void handle(ConfigurationClass configClass, DeferredImportSelector importSelector) {
	DeferredImportSelectorHolder holder = new DeferredImportSelectorHolder(
			configClass, importSelector);
			
// this.deferredImportSelectors默认是new ArrayList<>() 这里直接触发的是else逻辑
	if (this.deferredImportSelectors == null) {
	//如果为null,则实例化handler 将holder注册然后触发processGroupImports
		DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
		handler.register(holder);
		handler.processGroupImports();
	}
	else {
	//将DeferredImportSelectorHolder 放入集合
		this.deferredImportSelectors.add(holder);
	}
}

截止到目前我们还没有看到META-INF\spring.factories中配置的自动配置类加载与过滤,这个过程发生在this.deferredImportSelectorHandler.process();这个方法里面!接下来我们将会分析该方法。

这里可以先说明一下,parser.parse(candidates);方法执行后,ConfigurationClassParser的成员Map<ConfigurationClass, ConfigurationClass> configurationClasses就拥有了所有应该出现的配置类。

什么叫应该出现的呢?比如我们的@Controller、@Service、@Component、@Configuration以及哪些由于递归引入的配置类。需要注意的是META-INF\spring.factories中配置的自动配置类不会完全引入,将会根据当前应用具体情况相应引入。比如本文环境下没有引入RedisAutoConfiguration,但是存在DataSourceAutoConfiguration。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

流烟默

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值