《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)

网友投稿 372 2022-08-23

《SpringBoot启动流程五》:你真的知道SpringBoot自动装配原理吗(两万字图文源码分析)

文章目录

​​一、前言​​​​二、入口​​​​三、处理自动装配​​

​​1、处理自动装配的入口​​​​2、处理自动装配内容​​

​​1)找启动类,构建ConfigurationClassParser解析器准备解析启动类​​​​2)解析启动类​​​​3)解析启动类中的@ComponentScan注解​​​​4)解析启动类中的@Import注解​​​​5)处理所有的自动装配类​​

​​**phase1> getCandidateConfigurations() --> 获取自动装配类:**​​​​**phase5> getConfigurationClassFilter().filter(configurations) --> 过滤候选自动装配Class集合中不符合条件装配的Class成员:**​​

​​第一步:获取所有的Filter!​​​​第二步:执行条件装配​​

一、前言

我们前四篇博文,详细讨论了SpringBoot整个启动流程。博文如下:

1> ​​《SpringBoot启动流程一》:万字debug梳理SpringBoot如何加载并处理META-INF/spring.factories文件中的信息​​​; 2> ​​​《SpringBoot启动流程二》:七千字源码分析SpringApplication构造阶段​​​; 3> ​​​《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)​​​; 4> ​​​《SpringBoot启动流程四》:图文带你debug源码分析SpringApplication运行阶段和运行后阶段​​。

在启动流程中涉及SpringBoot的自动装配,虽然在之前我们聊过<​​SpringBoot自动装配机制原理​​​>,但其中没有聊到​​@EnableAutoConfiguration​​​,​​@Import​​注解是在何时被扫描的,本文就这一部分展开讨论。

注:Spring Boot版本:2.3.7.RELEASE。

二、入口

在Spring应用上下文准备阶段prepareContext()方法将应用的启动类加到Context中。

在Spring应用上下文启动阶段,会进入到refreshContext()方法,具体代码执行流程如下:

看​​ServletWebServerApplicationContext​​的类图:

ServletWebServerApplicationContext间接继承自​​AbstractApplicationContext​​​,所以最终会进入到​​AbstractApplicationContext#refresh()​​方法。

走到​​AbstractApplicationContext#refresh()​​方法便意味着Spring应用上下文进入Spring生命周期,Spring Boot核心特性随之启动,比如:自动装配。

三、处理自动装配

1、处理自动装配的入口

最终进入到​​PostProcessorRegistrationDelegate#invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory,List)​​方法中,自动装配在其中实现;

​​invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory,List)​​​方法中,传入接收到的入参beanFactory类型为​​DefaultListableBeanFactory​​:

再看DefaultListableBeanFactory的类图:

其实现了​​BeanDefinitionRegistry​​接口,所以会进入到if代码块:

在进入if代码块之后,会做两个操作:

1> 首先,遍历传入的三个BeanFactoryPostProcessor对其做分类;

分为常规后置处理器集合regularPostProcessors 和 注册处理器集合registryProcessors;

分类之后,regularPostProcessors有一个成员,registryProcessors中有两个成员。

final class PostProcessorRegistrationDelegate { public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List beanFactoryPostProcessors) { .... // Do not initialize FactoryBeans here: We need to leave all regular beans // uninitialized to let the bean factory post-processors apply to them! // Separate between BeanDefinitionRegistryPostProcessors that implement // PriorityOrdered, Ordered, and the rest. List currentRegistryProcessors = new ArrayList<>(); // First, invoke the BeanDefinitionRegistryPostProcessors that implement PriorityOrdered. // 这里只有一个值:org.springframework.context.annotation.internalConfigurationAnnotationProcessor String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanDefinitionRegistryPostProcessor.class, true, false); for (String ppName : postProcessorNames) { // internalConfigurationAnnotationProcessor实现了PriorityOrdered接口 if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) { // 将ConfigurationClassPostProcessor添加到currentRegistryProcessors中 currentRegistryProcessors.add(beanFactory.getBean(ppName, BeanDefinitionRegistryPostProcessor.class)); processedBeans.add(ppName); } } // 对currentRegistryProcessors做一个排序 sortPostProcessors(currentRegistryProcessors, beanFactory); registryProcessors.addAll(currentRegistryProcessors); // 走到这里registryProcessors中有三个对象了 // todo 核心所在 invokeBeanDefinitionRegistryPostProcessors(currentRegistryProcessors, registry, beanFactory.getApplicationStartup()); .... }}

接着从BeanFactory中获取到​​BeanDefinitionRegistryPostProcessor​​​的实现类​​ConfigurationClassPostProcessor​​,并将其添加到registryProcessors中;此时registryProcessors中有三个成员:

SharedMetadataReaderFactoryContextInitializer的静态内部类​​CachingMetadataReaderFactoryPostProcessor​​;ConfigurationWarningsApplicationContextInitializer的静态内部类​​ConfigurationWarningsPostProcessor​​;​​ConfigurationClassPostProcessor​​;

2> 其次,执行当前注册处理器ConfigurationClassPostProcessor;

代码执行流程如下:

由于postProcessors中只有一个成员ConfigurationClassPostProcessor,进入到​​ConfigurationClassPostProcessor​​​的​​postProcessBeanDefinitionRegistry(BeanDefinitionRegistry)​​方法。

从​​ConfigurationClassPostProcessor​​​#​​processConfigBeanDefinitions(BeanDefinitionRegistry)​​方法开始真正进入到处理自动装配的核心逻辑。

2、处理自动装配内容

1)找启动类,构建ConfigurationClassParser解析器准备解析启动类

首先从DefaultLisableBeanFactory中获取所有已经注册的BeanDefinition名称;

candidateNames中包含了我们的启动类,此外还有6个internalXxx类;然后遍历找到启动类,将其加到configCandidates集合中。

找到启动类(saintSpringBootApplicatioin)之后,构建一个配置类解析器ConfigurationClassParser,其中包括ComponentScanAnnotationParser、ConditionEvaluator,分别用于包扫描和条件装配;

接着调用ConfigurationClassParser#parse()方法开始解析启动类进行应用程序的启动。

2)解析启动类

以ConfigurationClassParser#parse()方法为入口,部分代码执行流程如下:

其中在做条件装配时,有个点需要注意一下:​​ConfigurationCondition​​​接口内部的枚举类​​ConfigurationPhase​​​中有两个值​​PARSE_CONFIGURATION​​​、​​REGISTER_BEAN​​,分别表示:在类解析阶段做条件装配、在类注册阶段做条件装配。

if (this.conditionEvaluator.shouldSkip(configClass.getMetadata(), ConfigurationPhase.PARSE_CONFIGURATION)) { return;}

1> 继续看获取SourceClass的代码逻辑:

其中会校验启动类上@SpringBootApplication注解的合法性,然后将启动类和其注解元数据AnnotationMetadata封装到SourceClass中返回。

如果获取不到SourceClass,则不会执行配置类(启动类)的处理。

2> 获取到SourceClass之后,处理配置类和SourceClass:

​​ConfigurationClassParser#doProcessConfigurationClass(ConfigurationClass,SourceClassPredicate)​​方法中会处理如下内容:

如果启动类configClass被@Component的衍生注解(递归注解的父注解可以找到@Component)标注,则首先递归处理所有成员(嵌套)类:即 configClass类内部如果找到成员类,会递归调用doProcessConfigurationClass()方法处理所有成员类。解析启动类中所有的@PropertySource、@ComponentScan、@Import、@ImportResource、@Bean注解。

具体代码如下:

class ConfigurationClassParser { .... @Nullable protected final SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate filter) throws IOException { // 启动类configClass被@Component的衍生注解(递归注解的父注解可以找到@Component)标注 if (configClass.getMetadata().isAnnotated(Component.class.getName())) { // 首先递归处理所有成员(嵌套)类:configClass类内部如果找到成员类,会递归调用doProcessConfigurationClass()方法处理所有成员类。 processMemberClasses(configClass, sourceClass, filter); } // Process any @PropertySource annotations 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 Set componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class); if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { // componentScan中包含11个成员,对应于@ComponentScan中11个属性 for (AnnotationAttributes componentScan : componentScans) { // 处理@ComponentScan 中的属性,返回所有派生的@Component注解标注的类,然后立即进行扫描 // 此处会找到basePackages,其默认为启动类所在的目录 Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed // TODO 实际业务中这里定义了多个派生@Component注解标注的类,这里就会循环多少次 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 // getImports()方法从启动类中获取所有的@Import注解的内容 processImports(configClass, sourceClass, getImports(sourceClass), filter, true); // Process any @ImportResource annotations AnnotationAttributes importResource = AnnotationConfigUtils.attributesFor(sourceClass.getMetadata(), ImportResource.class); if (importResource != null) { String[] resources = importResource.getStringArray("locations"); Class readerClass = importResource.getClass("reader"); for (String resource : resources) { String resolvedResource = this.environment.resolveRequiredPlaceholders(resource); configClass.addImportedResource(resolvedResource, readerClass); } } // Process individual @Bean methods Set beanMethods = retrieveBeanMethodMetadata(sourceClass); for (MethodMetadata methodMetadata : beanMethods) { configClass.addBeanMethod(new BeanMethod(methodMetadata, configClass)); } // Process default methods on interfaces 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; } ....}

就一个最简单、最干净的SpringBoot程序来看,其中没有@PropertySource、@ImportResource注解,平时工程中也很少使用。所以本文我们着重看@ComponentScan、@Import两个注解的处理流程(也就是我们自动装配的核心所在)。

3)解析启动类中的@ComponentScan注解

// Process any @ComponentScan annotationsSet componentScans = AnnotationConfigUtils.attributesForRepeatable( sourceClass.getMetadata(), ComponentScans.class, ComponentScan.class);if (!componentScans.isEmpty() && !this.conditionEvaluator.shouldSkip(sourceClass.getMetadata(), ConfigurationPhase.REGISTER_BEAN)) { // componentScan中包含11个成员,对应于@ComponentScan中11个属性 for (AnnotationAttributes componentScan : componentScans) { // 处理@ComponentScan 中的属性,返回所有派生的@Component注解标注的类,然后立即进行扫描 // 此处会找到basePackages,其默认为启动类所在的目录 Set scannedBeanDefinitions = this.componentScanParser.parse(componentScan, sourceClass.getMetadata().getClassName()); // Check the set of scanned definitions for any further config classes and parse recursively if needed // TODO 实际业务中这里定义了多个派生@Component注解标注的类,这里就会循环多少次 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()); } } }}

首先获取启动类@SpringBootApplication注解中的11个属性,然后调用​​ComponentScanAnnotationParser#parse()​​​方法处理@SpringBootApplication注解中的注解 并 设置到类路径BeanDefinition扫描器​​ClassPathBeanDefinitionScanner​​的相应属性中。

class ComponentScanAnnotationParser { .... public Set parse(AnnotationAttributes componentScan, String declaringClass) { ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(this.registry, componentScan.getBoolean("useDefaultFilters"), this.environment, this.resourceLoader); Class generatorClass = componentScan.getClass("nameGenerator"); boolean useInheritedGenerator = (BeanNameGenerator.class == generatorClass); scanner.setBeanNameGenerator(useInheritedGenerator ? this.beanNameGenerator : BeanUtils.instantiateClass(generatorClass)); ScopedProxyMode scopedProxyMode = componentScan.getEnum("scopedProxy"); if (scopedProxyMode != ScopedProxyMode.DEFAULT) { scanner.setScopedProxyMode(scopedProxyMode); } else { Class resolverClass = componentScan.getClass("scopeResolver"); scanner.setScopeMetadataResolver(BeanUtils.instantiateClass(resolverClass)); } scanner.setResourcePattern(componentScan.getString("resourcePattern")); for (AnnotationAttributes includeFilterAttributes : componentScan.getAnnotationArray("includeFilters")) { List typeFilters = TypeFilterUtils.createTypeFiltersFor(includeFilterAttributes, this.environment, this.resourceLoader, this.registry); for (TypeFilter typeFilter : typeFilters) { scanner.addIncludeFilter(typeFilter); } } for (AnnotationAttributes excludeFilterAttributes : componentScan.getAnnotationArray("excludeFilters")) { List typeFilters = TypeFilterUtils.createTypeFiltersFor(excludeFilterAttributes, this.environment, this.resourceLoader, this.registry); for (TypeFilter typeFilter : typeFilters) { scanner.addExcludeFilter(typeFilter); } } boolean lazyInit = componentScan.getBoolean("lazyInit"); if (lazyInit) { scanner.getBeanDefinitionDefaults().setLazyInit(true); } Set basePackages = new LinkedHashSet<>(); String[] basePackagesArray = componentScan.getStringArray("basePackages"); for (String pkg : basePackagesArray) { String[] tokenized = StringUtils.tokenizeToStringArray(this.environment.resolvePlaceholders(pkg), ConfigurableApplicationContext.CONFIG_LOCATION_DELIMITERS); Collections.addAll(basePackages, tokenized); } for (Class clazz : componentScan.getClassArray("basePackageClasses")) { basePackages.add(ClassUtils.getPackageName(clazz)); } // 默认会走到这里 if (basePackages.isEmpty()) { // 默认,basePackages为启动类所在的目录。eg:启动类为com.saint.SaintSpringBootApplication,basePackages为:com.saint basePackages.add(ClassUtils.getPackageName(declaringClass)); } scanner.addExcludeFilter(new AbstractTypeHierarchyTraversingFilter(false, false) { @Override protected boolean matchClassName(String className) { return declaringClass.equals(className); } }); return scanner.doScan(StringUtils.toStringArray(basePackages)); }}

​​basePackages​​​属性为@ComponentScan注解的默认扫描包路径,如果没指定该属性,则会将启动类所在的包作为默认值 赋值​​basePackages​​属性上(以启动类SaintSpringBootApplication为例,其默认扫包路径为:com.saint)。

给ClassPathBeanDefinitionScanner制定完所有属性之后,会调用其​​doScan(String...)​​​方法扫描​​basePackages​​​目录下的所有标注了​​@Component​​衍生注解(比如:@Controller、@Service、@Repository)的类。

具体代码执行流程如下:

获取并注册完所有的@Component衍生类之后,在递归对这些类做解析。

4)解析启动类中的@Import注解

在此之前,我聊SpringBoot自动装配都是说,@EnableAutoConfiguration注解中通过@Import注解导入了AutoConfigurationImportSelector.class,@EnableAutoConfiguration注解中的@AutoConfigurationPackage中通过@Import注解导入了AutoConfigurationPackages.Registrar.class类,但我并不知道这里的@Import是在何时处理的!!这里我们就看一下针对@Import注解是怎么处理的。

processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

这里分两步,首先通过​​getImports()​​方法获取启动类中的@Import注解,然后再通过processImports()方法处理所有的@Import注解。

1> getImports()方法获取启动类中所有的@Import注解:

具体递归流程如下:

最终获取的@import注解有两个:

2> processImports()方法处理获取到的启动类中所有的@Import注解:

先处理AutoConfigurationPackages.Registrar.class类,再处理AutoConfigurationImportSelector类;

对AutoConfigurationPackages.Registrar.class类的处理比较简单,利用反射将其实例化之后,添加到启动类的​​importBeanDefinitionRegistrars​​属性中。

由于AutoConfigurationImportSelector实现了DeferredImportSelector接口,所以会对AutoConfigurationImportSelector进行一个处理:将​​AutoConfigurationImportSelector​​​封装为​​DeferredImportSelectorHolder​​​对象,然后添加到​​ConfigurationClassParser​​​类的​​deferredImportSelectors​​属性中(供后面处理@Import内容)。

5)处理所有的自动装配类

最后回到​​ConfigurationClassParser#parse()​​方法中:

代码执行流程如下:

最终进入到​​AutoConfigurationImportSelector​​​#​​getAutoConfigurationEntry(AnnotationMetadata)​​​方法,这里的代码逻辑相信大家嘎嘎眼熟,在​​SpringBoot自动装配机制原理​​一文中我们聊过。

​​AutoConfigurationImportSelector​​​#​​getAutoConfigurationEntry(AnnotationMetadata)​​方法源代码如下:

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return EMPTY_ENTRY; } // 1. 获取@EnableAutoConfiguration标注类的元信息 AnnotationAttributes attributes = getAttributes(annotationMetadata); // 2. 返回自动装配类的候选类名集合 List configurations = getCandidateConfigurations(annotationMetadata, attributes); // 3. 移除重复对象,因为 自动装配组件存在重复定义的情况 configurations = removeDuplicates(configurations); // 4. 自动装配组件的排除名单 Set exclusions = getExclusions(annotationMetadata, attributes); // 5.1. 检查自动装配Class排除集合的合法性 checkExcludedClasses(configurations, exclusions); // 5.2 排除掉不需要自动装配的Class configurations.removeAll(exclusions); // 6. 进一步过滤 configurations = getConfigurationClassFilter().filter(configurations); // 7. 触发自动装配的导入事件,事件包括候选的装配组件类名单和排除名单。 fireAutoConfigurationImportEvents(configurations, exclusions); return new AutoConfigurationEntry(configurations, exclusions);}

其负责拿到所有自动配置的节点,大致分为六步;

第一步,在​​getCandidateConfigurations()​​​方法中利用Spring Framework工厂机制的加载器​​SpringFactoriesLoader​​​,通过​​SpringFactoriesLoader#loadFactoryNames(Class, ClassLoader)​​​方法读取所有​​META-INF/spring.factories​​​资源中​​@EnableAutoConfiguration​​所关联的自动装配Class集合。第二步,利用Set不可重复性对​​自动装配Class集合​​进行去重,因为自动装配组件存在重复定义的情况;第三步,读取当前配置类所标注的@EnableAutoConfiguration注解的属性exclude和excludeName,并与​​spring.autoconfigure.exclude​​配置属性的值 合并为自动装配class排除集合。第四步,校验自动装配Class排除集合的合法性、并排除掉​​自动装配Class排除集合​​中的所有Class(不需要自动装配的Class)。第五步,再次过滤后候选自动装配Class集合中不符合条件装配的Class成员;最后一步,触发自动装配的导入事件。

phase1> getCandidateConfigurations() --> 获取自动装配类:

代码执行流程如下:

​​getSpringFactoriesLoaderFactoryClass()​​​方法返回我们熟悉的​​EnableAutoConfiguration​​注解类;

紧接着,​​SpringFactoriesLoader.loadFactoryNames(Class, ClassLoader)​​方法会获取所有META-INF/Spring.factories的配置文件,进而获取到所有的自动装配类;

loadFactoryNames()原理如下:

搜索指定ClassLoader下所有的META-INF/spring.fatories资源内容;将搜索到的资源内容作为Properties文件读取,合并为一个Key为​​接口的全类名​​​、Value为​​实现类全类名 列表​​的Map,作为方法的返回值;最后从上一步返回的Map中查找并返回方法指定类型 对应的​​实现类全类名列表​​。

loadFactoryNames()方法源码解释如下:

private static Map> loadSpringFactories(@Nullable ClassLoader classLoader) { // 先从缓存中获取 MultiValueMap result = cache.get(classLoader); if (result != null) { return result; } try { Enumeration urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); result = new LinkedMultiValueMap<>(); while (urls.hasMoreElements()) { /** * 查找所有我们依赖的jar包,并找到对应有META-INF/spring.factories⽂件,然后获取⽂件中的内容 * * 第一次循环:file:/.../org/springframework/spring-beans/5.2.12.RELEASE/spring-beans-5.2.12.RELEASE.jar!/META-INF/spring.factories * 第二次循环:file:/.../org/springframework/boot/spring-boot/2.3.7.RELEASE/spring-boot-2.3.7.RELEASE.jar!/META-INF/spring.factories * 第三次循环:file:/../org/springframework/boot/spring-boot-autoconfigure/2.3.7.RELEASE/spring-boot-autoconfigure-2.3.7.RELEASE.jar!/META-INF/spring.factories */ URL url = urls.nextElement(); // 获取资源 UrlResource resource = new UrlResource(url); // 获取资源的内容 Properties properties = PropertiesLoaderUtils.loadProperties(resource); for (Map.Entry entry : properties.entrySet()) { String factoryTypeName = ((String) entry.getKey()).trim(); for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) { result.add(factoryTypeName, factoryImplementationName.trim()); } } } cache.put(classLoader, result); return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); }}

phase5> getConfigurationClassFilter().filter(configurations) --> 过滤候选自动装配Class集合中不符合条件装配的Class成员:

这里分两步:

第一步:获取所有的Filter

首先通过​​getConfigurationClassFilter()​​​方法从所有​​META-INF/spring.factories​​​文件中获取所有的自动装配过滤器​​AutoConfigurationImportFilter​​​的实现类(一共有三个),然后实例化​​AutoConfigurationImportSelector​​​类的内部类​​ConfigurationClassFilter​​​,并将获取多的所有​​AutoConfigurationImportFilter​​​的实现类集合赋值到ConfigurationClassFilter的​​filters​​属性中(后面会用到它做条件装配)。

第二步:执行条件装配

然后对获取到的所有自动装配类(最干净、最简单的SpringBoot程序有127个)执行过滤操作(条件装配)后,还剩23个自动装配类。

关于此处为什么127个自动装配类经过​​AutoConfigurationImportFilter​​过滤后只剩23个了,且听下回分解《SpringBoot自动装配中的条件装配》。后面就一路返回返回返回!!!!

版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:《SpringBoot启动流程三》:两万+字图文带你debug源码分析SpringApplication准备阶段(含配置文件加载时机、日志系统初始化时机)
下一篇:如何把营销做到1+1远远大于2,如何运用这个“跨界营销”?(1+N营销)
相关文章

 发表评论

暂时没有评论,来抢沙发吧~