Spring 源码分析零:Springboot的启动流程

文章目录

  • 一、前言
  • 二、SpringApplication 的初始化
  • 三、流程简介
  • 四、流程步骤详解
    • 1. 获取监听器
    • 2. 环境变量的构造
      • 2.1. application.yml 的加载
    • 3. 创建上下文
    • 4. 上下文准备工作
    • 5. SpringApplication#refreshContext
  • 附录:spring.factories

一、前言

本文是笔者阅读Spring源码的记录文章,由于本人技术水平有限,在文章中难免出现错误,如有发现,感谢各位指正。在阅读过程中也创建了一些衍生文章,衍生文章的意义是因为自己在看源码的过程中,部分知识点并不了解或者对某些知识点产生了兴趣,所以为了更好的阅读源码,所以开设了衍生篇的文章来更好的对这些知识点进行进一步的学习。


本文是针对 Springboot run方法的启动流程的分析。

下面是一个简单的项目,我们直接开始看run方法

@SpringBootApplication
public class SpringbootDemoApplication {
	
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(SpringbootDemoApplication.class, args);
        Object demoService = run.getBean("demoService");
        System.out.println("demoService = " + demoService);
   }
}

二、SpringApplication 的初始化

我们这里直接看 SpringApplication#run(java.lang.Class[], java.lang.String[]) 方法

	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		return new SpringApplication(primarySources).run(args);
	}

我们这里先看一下 SpringApplication 的构造函数流程

	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 保存启动类信息
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 初始化环境。环境分为三种 非web环境、web环境、reactive环境三种。其判断逻辑就是判断是否存在指定的类,默认是Servlet 环境,我们这也是Servlet
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// getSpringFactoriesInstances 方法加载了 spring.factories文件。在这里进行了首次加载spring.factoies文件。设置 ApplicationContextInitializer
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 获取监听器,也加载了spring.factories文件
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 设置启动类信息
		this.mainApplicationClass = deduceMainApplicationClass();
	}

三、流程简介

我们下面直接来看 SpringApplication#run(java.lang.String...) 方法的执行流程

	public ConfigurableApplicationContext run(String... args) {
		// 开启关于启动时间的信息监控
		StopWatch stopWatch = new StopWatch();
		stopWatch.start();
		// 准备 ApplicationContext
		ConfigurableApplicationContext context = null;
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
				//java.awt.headless是J2SE的一种模式用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole 需要将该值设置为true,系统变量默认为true
		configureHeadlessProperty();
		// 1. 获取Spring的监听器类,这里是从 spring.factories 中去获取,默认的是以 org.springframework.boot.SpringApplicationRunListener 为key,获取到的监听器类型为 EventPublishingRunListener。
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 1.1 监听器发送启动事件
		listeners.starting();
		try {
			// 封装参数
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 2. 构造容器环境。将容器的一些配置内容加载到 environment  中
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			// 配置BeanInfo的忽略 :“spring.beaninfo.ignore”,值为“true”表示跳过对BeanInfo类的搜索
			configureIgnoreBeanInfo(environment);
			// 打印信息对象
			Banner printedBanner = printBanner(environment);
			// 3. 创建上下文对象
			context = createApplicationContext();
			// 从 spring.factries 中获取错误报告的类。出错的时候会调用其方法通知
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 4. 准备刷新上下文
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 5. 刷新上下文
			refreshContext(context);
			// 结束刷新,留待扩展功能,并未实现什么
			afterRefresh(context, applicationArguments);
			// 停止监听
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 监听器发送启动结束时间
			listeners.started(context);
			// 调用 ApplicationRunner 和 CommandLineRunner 对应的方法
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			// 发送容器运行事件
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}

四、流程步骤详解

下面我们重点分析几个步骤

1. 获取监听器

这一步是从 spring.factories 文件中获取监听器集合,当有事件发生时调用监听器对应事件的方法。

默认的是以 org.springframework.boot.SpringApplicationRunListener 为key,获取到的监听器类型为 EventPublishingRunListener

SpringApplicationRunListeners listeners = getRunListeners(args);

其详细代码如下:

	private SpringApplicationRunListeners getRunListeners(String[] args) {
		Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
		return new SpringApplicationRunListeners(logger,
				getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
	}

这里需要注意的是 getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args) 返回的是一个Collection 类型。也就是说明在 SpringApplicationRunListeners并非代表一个监听器,而是保存了监听器集合,在默认情况下,仅有一个 EventPublishingRunListener。在 SpringApplicationRunListeners 类中也能看到,如下:

class SpringApplicationRunListeners {

	private final Log log;

	private final List<SpringApplicationRunListener> listeners;

	SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
		this.log = log;
		this.listeners = new ArrayList<>(listeners);
	}
	...
}

总结一下:
Spring启动时,通过 spring.factories 文件中获取监听器集合。默认类型为 EventPublishingRunListener。在事件发生时,EventPublishingRunListener 会寻找容器中 ApplicationListener 的bean,并进行事件通知。

具体内容请参考 :Spring源码分析衍生篇六:Spring监听事件

2. 环境变量的构造

这一步的作用就是加载一些配置文件的内容

ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

其具体实现如下:

	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments) {
		// Create and configure the environment
		// 获取或者创建 environment。这里获取类型是 StandardServletEnvironment 
		ConfigurableEnvironment environment = getOrCreateEnvironment();
		// 将入参配置到环境配置中
		configureEnvironment(environment, applicationArguments.getSourceArgs());
		ConfigurationPropertySources.attach(environment);
		// 发布环境准备事件。
		listeners.environmentPrepared(environment);
		bindToSpringApplication(environment);
		if (!this.isCustomEnvironment) {
			environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
					deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}

	private Class<? extends StandardEnvironment> deduceEnvironmentClass() {
		switch (this.webApplicationType) {
		case SERVLET:
			return StandardServletEnvironment.class;
		case REACTIVE:
			return StandardReactiveWebEnvironment.class;
		default:
			return StandardEnvironment.class;
		}
	}

关于 webApplicationType 的值,在 org.springframework.boot.SpringApplication#SpringApplication(org.springframework.core.io.ResourceLoader, java.lang.Class...) 构造函数中进行了赋值为Servlet。所以我们这里可以知道 Environment 类型为 StandardServletEnvironment

2.1. application.yml 的加载

listeners.environmentPrepared(environment); 时会发送环境准备事件,环境准备事件要通知七个监听器如下图。对于 Springboot 的配置文件application.yml或者application.properties文件的加载实际上是通过发布环境准备事件完成的,完成这项功能的就是 ConfigFileApplicationListener
Spring 源码分析零:Springboot的启动流程_第1张图片

我们这里看看 ConfigFileApplicationListeneronApplicationEvent 方法如下

	@Override
	public void onApplicationEvent(ApplicationEvent event) {
		if (event instanceof ApplicationEnvironmentPreparedEvent) {
			onApplicationEnvironmentPreparedEvent((ApplicationEnvironmentPreparedEvent) event);
		}
		if (event instanceof ApplicationPreparedEvent) {
			onApplicationPreparedEvent(event);
		}
	}

由于在 EventPublishingRunListener#environmentPrepared 发布事件的时候,指明了该事件为 ApplicationEnvironmentPreparedEvent
Spring 源码分析零:Springboot的启动流程_第2张图片
所以我们这里会调用
ConfigFileApplicationListener#onApplicationEnvironmentPreparedEvent 方法。如下

	private void onApplicationEnvironmentPreparedEvent(ApplicationEnvironmentPreparedEvent event) {
		// 获取 EnvironmentPostProcessor。这里也是从spring.factories 文件中获取
		List<EnvironmentPostProcessor> postProcessors = loadPostProcessors();
		// 把自身添加进入
		postProcessors.add(this);
		AnnotationAwareOrderComparator.sort(postProcessors);
		// 排序后调用 EnvironmentPostProcessor#postProcessEnvironment 方法
		for (EnvironmentPostProcessor postProcessor : postProcessors) {
			postProcessor.postProcessEnvironment(event.getEnvironment(), event.getSpringApplication());
		}
	}

实际上,也就是在 ConfigFileApplicationListener#postProcessEnvironment 方法中加载了Springboot 配置文件

	@Override
	public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
		addPropertySources(environment, application.getResourceLoader());
	}
	...
	protected void addPropertySources(ConfigurableEnvironment environment, ResourceLoader resourceLoader) {
		RandomValuePropertySource.addToEnvironment(environment);
		// 在这里面的load方法中加载了配置文件内容。具体就不再解析
		new Loader(environment, resourceLoader).load();
	}

3. 创建上下文

这一步是创建上下文了

context = createApplicationContext();

其详细内容如下:


	/**
	 * The class name of application context that will be used by default for non-web
	 * environments.
	 */
	public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
			+ "annotation.AnnotationConfigApplicationContext";

	/**
	 * The class name of application context that will be used by default for web
	 * environments.
	 */
	public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
			+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

	/**
	 * The class name of application context that will be used by default for reactive web
	 * environments.
	 */
	public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework."
			+ "boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";


	protected ConfigurableApplicationContext createApplicationContext() {
		Class<?> contextClass = this.applicationContextClass;
		if (contextClass == null) {
			try {
				switch (this.webApplicationType) {
				case SERVLET:
					contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
					break;
				case REACTIVE:
					contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
					break;
				default:
					contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
				}
			}
			catch (ClassNotFoundException ex) {
				throw new IllegalStateException(
						"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
			}
		}
		return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
	}

很明显,因为我们知道 webApplicationType 值是 servlet,所以这里创建的是 AnnotationConfigServletWebServerApplicationContext 类型的上下文

这里需要注意:AnnotationConfigServletWebServerApplicationContext 构造函数中会创建 AnnotatedBeanDefinitionReader。而在 AnnotatedBeanDefinitionReader 构造函数中会调用 AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);,该方法将一些必要Bean(如ConfigurationClassPostProcessor、AutowiredAnnotationBeanPostProcessor、CommonAnnotationBeanPostProcessor 等)注入到了容器中。

4. 上下文准备工作

上面一步,仅仅是将上下文创建出来了,并没有对上下文进行操作。这一步开始对上下文的准备操作。

prepareContext(context, environment, listeners, applicationArguments, printedBanner);

其详细内容如下:

	private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
			SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
		// 设置上下文的环境变量
		context.setEnvironment(environment);
		// 执行容器后置处理 : 可以注册beanName策略生成器、设置资源加载器,设置转换服务等。但这里默认是没有做任何处理。目的是留给后续可以扩展
		postProcessApplicationContext(context);
		// 处理所有的初始化类的初始化方法。即 spring.factories 中key 为 org.springframework.context.ApplicationContextInitializer 指向的类,调用其 initialize 方法
		applyInitializers(context);
		// 向监听器发送容器准备事件
		listeners.contextPrepared(context);
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}
		// Add boot specific singleton beans
		// 获取上下文中的 BeanFactory。这里的BeanFactory 实际类型是  DefaultListableBeanFactory。BeanFactory 在初始化的时候,直接在构造函数里创建为 DefaultListableBeanFactory
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		// 注册 springApplicationArguments等一系列bean
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}
		if (beanFactory instanceof DefaultListableBeanFactory) {
			// 设置是否允许bean定义覆盖
			((DefaultListableBeanFactory) beanFactory)
					.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
		}
		// 如果允许懒加载,则添加对应的BeanFactory后置处理器
		if (this.lazyInitialization) {
			context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
		}
		// Load the sources
		// 这里加载的实际上是启动类
		Set<Object> sources = getAllSources();
		Assert.notEmpty(sources, "Sources must not be empty");
		// 这里将启动类加入到 beanDefinitionMap 中,为后续的自动化配置做好了基础
		load(context, sources.toArray(new Object[0]));
		// 发送容器加载完成事件
		listeners.contextLoaded(context);
	}

	....
	
	// 需要注意这里的 sources参数实际上是 启动类的 Class
	protected void load(ApplicationContext context, Object[] sources) {
		if (logger.isDebugEnabled()) {
			logger.debug("Loading source " + StringUtils.arrayToCommaDelimitedString(sources));
		}
		// 从上下文中获取 BeanDefinitionRegistry并依次创建出 BeanDefinitionLoader 。这里将sources作为参数保存到了 loader  中。也就是 loader  中保存了 启动类的Class信息
		BeanDefinitionLoader loader = createBeanDefinitionLoader(getBeanDefinitionRegistry(context), sources);
		if (this.beanNameGenerator != null) {
			loader.setBeanNameGenerator(this.beanNameGenerator);
		}
		if (this.resourceLoader != null) {
			loader.setResourceLoader(this.resourceLoader);
		}
		if (this.environment != null) {
			loader.setEnvironment(this.environment);
		}
		loader.load();
	}

我们这里比较关键的方法是 loader.load(); 方法
其中 loader.load(); 不管怎么跳转,最后都会跳转到 BeanDefinitionLoader#load(java.lang.Class) 方法中。如下

	private int load(Class<?> source) {
		// 判断是否存在 groovy 加载方式
		if (isGroovyPresent() && GroovyBeanDefinitionSource.class.isAssignableFrom(source)) {
			// Any GroovyLoaders added in beans{} DSL can contribute beans here
			GroovyBeanDefinitionSource loader = BeanUtils.instantiateClass(source, GroovyBeanDefinitionSource.class);
			load(loader);
		}
		// 判断 source 是否 需要被加载到Spring容器中。实际上是根据判断是否存在 @Component 
		if (isComponent(source)) {
			// 将source 就是启动类的 class,注册到 annotatedReader 中。annotatedReader  类型是AnnotatedBeanDefinitionReader。
			this.annotatedReader.register(source);
			return 1;
		}
		return 0;
	}

this.annotatedReader.register(source); 后续会跳转到 AnnotatedBeanDefinitionReader#doRegisterBean 方法中,看名字就知道是这个方法的工作是 注册 Bean。实际上,在这个方法中完成了对@Qualifier 以及一些其他注解的处理。具体如下:

	// 这里的 beanClass 其实就是启动类的 beanClass 
	private <T> void doRegisterBean(Class<T> beanClass, @Nullable String name,
			@Nullable Class<? extends Annotation>[] qualifiers, @Nullable Supplier<T> supplier,
			@Nullable BeanDefinitionCustomizer[] customizers) {
		// 将Class 转换成一个 BeanDefinition 类
		AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass);
		// 判断是否应该跳过
		if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) {
			return;
		}

		abd.setInstanceSupplier(supplier);
		// 保存其作用域信息。这里默认是 singleton
		ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(abd);
		abd.setScope(scopeMetadata.getScopeName());
		// 获取 beanName
		String beanName = (name != null ? name : this.beanNameGenerator.generateBeanName(abd, this.registry));
		// 处理一些通用的注解信息,包括Lazy、Primary、DependsOn、Role、Description 注解。获取其value值并保存到 abd 中
		AnnotationConfigUtils.processCommonDefinitionAnnotations(abd);
		// 处理  @Qualifier 
		if (qualifiers != null) {
			for (Class<? extends Annotation> qualifier : qualifiers) {
				if (Primary.class == qualifier) {
					abd.setPrimary(true);
				}
				else if (Lazy.class == qualifier) {
					abd.setLazyInit(true);
				}
				else {
					abd.addQualifier(new AutowireCandidateQualifier(qualifier));
				}
			}
		}
		if (customizers != null) {
			for (BeanDefinitionCustomizer customizer : customizers) {
				customizer.customize(abd);
			}
		}

		BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(abd, beanName);
		// 判断是否需要创建代理,需要则创建
		definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
		// 将 BeanDefinitionHolder  注册到 容器中。此时的 registry 就是 AnnotationConfigServletWebServerApplicationContext。在BeanDefinitionLoader 初始化的时候保存的
		BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, this.registry);
	}

5. SpringApplication#refreshContext

对容器进行一个刷新工作。在此进行了大量的工作。这里的处理工作就由Springboot交给 Spring来处理了

refreshContext(context);

详细如下:

private void refreshContext(ConfigurableApplicationContext context) {
		refresh(context);
		if (this.registerShutdownHook) {
			try {
				context.registerShutdownHook();
			}
			catch (AccessControlException ex) {
				// Not allowed in some environments.
			}
		}
	}

最终会跳转到 AbstractApplicationContext#refresh 中。而关于 AbstractApplicationContext#refresh 方法在之前的文章中有过介绍,具体请参考:Spring源码分析一:容器的刷新 - refresh()


附录:spring.factories

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.cloud.CloudFoundryVcapEnvironmentPostProcessor,\
org.springframework.boot.env.SpringApplicationJsonEnvironmentPostProcessor,\
org.springframework.boot.env.SystemEnvironmentPropertySourceEnvironmentPostProcessor,\
org.springframework.boot.reactor.DebugAgentEnvironmentPostProcessor

# Failure Analyzers
org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.boot.diagnostics.analyzer.BeanCurrentlyInCreationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanDefinitionOverrideFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BeanNotOfRequiredTypeFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.BindValidationFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.UnboundConfigurationPropertyFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ConnectorStartFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoSuchMethodFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.NoUniqueBeanDefinitionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.PortInUseFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.ValidationExceptionFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyNameFailureAnalyzer,\
org.springframework.boot.diagnostics.analyzer.InvalidConfigurationPropertyValueFailureAnalyzer

# FailureAnalysisReporters
org.springframework.boot.diagnostics.FailureAnalysisReporter=\
org.springframework.boot.diagnostics.LoggingFailureAnalysisReporter


以上:内容部分参考
https://blog.csdn.net/woshilijiuyi/article/details/82219585
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

你可能感兴趣的:(#,源码分析篇)