-- 以下内容均基于2.1.8.RELEASE版本

通过粗粒度的分析SpringBoot启动过程中执行的主要操作,可以很容易划分它的大流程,每个流程只关注重要操作为后续深入学习建立一个大纲。


官方示例-使用SpringBoot快速构建一个Web服务

@RestController
@SpringBootApplication
public class Example { @RequestMapping("/")
String home() {
return "Hello World!";
} public static void main(String[] args) {
SpringApplication.run(Example.class, args);
} }

由代码可知SpringBoot应用程序入口为SpringApplication.java,由其run()方法开始。

SpringApplication.java

构造方法


public class 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));
this.webApplicationType = WebApplicationType.deduceFromClasspath();
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
}

由代码可得知,它未实现接口、未继承其它类。由构造方法可以看出它主要做了如下几件事:

  • 获取当前应用类型(NONE, SERVLET, REACTIVE其中之一)。
  • 通过SPI获取ApplicationContextInitializer接口的实现类,其配置在MATE-INF/spring.factories文件中。
  • 通过SPI获取ApplicationListener接口的实现类,同上。
  • 获取启动入口(main函数)

run(...args) 方法

整个应用的启动将会在run方法内部完成。去除那些枝叶,只取最主要的内容来看。

	/**
* Run the Spring application, creating and refreshing a new
* {@link ApplicationContext}.
* @param args the application arguments (usually passed from a Java main method)
* @return a running {@link ApplicationContext}
*/
public ConfigurableApplicationContext run(String... args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
configureHeadlessProperty();
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
configureIgnoreBeanInfo(environment);
Banner printedBanner = printBanner(environment);
context = createApplicationContext();
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
refreshContext(context);
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
listeners.started(context);
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;
}

通过方法上面的注释描述可以看出它就是用于启动并刷新容器。在run方法内部通过SPI获取SpringApplicationRunListener接口的实现类,它用于触发所有的监听器。EventPublishingRunListener作为一个实现类,从名称上来看其主要用于运行时的事件发布。在SpringBoot的各个生命周期来触发相对的事件,调用处理事件的监听器来完成每个阶段的操作。

SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();

获取所有的SpringApplicationRunListener接口实例,此处的SpringApplicationRunListeners它包装了SpringApplicationRunListener对象。如下:

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);
}
// 省略部分代码
}

接着调用了starting()方法,实际上还是调用SpringApplicationRunListener的starting方法。如下:

public void starting() {
for (SpringApplicationRunListener listener : this.listeners) {
listener.starting();
}
}

实际上SpringApplicationRunListener并不止这一个方法:

void starting();

运行一开始触发,属于最早期的事件,处理ApplicationStartingEvent事件

void environmentPrepared(ConfigurableEnvironment environment);

当环境准备好的时候调用,但是在ApplicationContext创建之前。

void contextPrepared(ConfigurableApplicationContext context);

当ApplicationContext准备好的时候调用,但是在sources加载之前。

void contextLoaded(ConfigurableApplicationContext context);

当ApplicationContext准备好的时候调用,但是在它refresh之前。

void started(ConfigurableApplicationContext context);

上下文已被刷新,应用程序已启动,但CommandLineRunners 和ApplicationRunner还没被调用。

void running(ConfigurableApplicationContext context);

在run方法结束之前,并且所有的CommandLineRunners 和ApplicationRunner都被调用的时候调用。

根据方法的注释可以得知他们执行的时机,并得知他们所处理的事件类型。接下来看看EventPublishingRunListener的staring方法内部做了什么操作:

    public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}

这个initialMulticaster是干啥的?看看它的构造方法吧

public EventPublishingRunListener(SpringApplication application, String[] args) {
this.application = application;
this.args = args;
this.initialMulticaster = new SimpleApplicationEventMulticaster();
for (ApplicationListener<?> listener : application.getListeners()) {
this.initialMulticaster.addApplicationListener(listener);
}
}

此处它并没有直接自己来操作这些监听器,而是在初始化的时候将所有监听器给了SimpleApplicationEventMulticaster,由它来执行触发,此处先不做深入探讨,只需要知道它会触发事件就行。现在把关注点放在具体的事件触发上this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));这行代码在处理事件的时候new了一个ApplicationStartingEvent,由此得知它的每一个类型的处理方法都会传入一个指定的事件。

public void multicastEvent(ApplicationEvent event,  ResolvableType eventType) {
ResolvableType type = eventType != null ? eventType : this.resolveDefaultEventType(event);
Executor executor = this.getTaskExecutor();
Iterator var5 = this.getApplicationListeners(event, type).iterator();
while(var5.hasNext()) {
ApplicationListener<?> listener = (ApplicationListener)var5.next();
if (executor != null) {
executor.execute(() -> {
this.invokeListener(listener, event);
});
} else {
this.invokeListener(listener, event);
}
}
}

可以看出在处理事件的方法内部做了两件事(multicastEvent方法内部标红的方法调用):

获取所有的监听器

调用监听器

获取所有的监听器

在获取监听器的过程中,会循环判断监听器声明的事件类型是否和本次处理的事件类型相同,本次处理的类型为ApplicationStartingEvent,不符合事件类型的事件会被排除,只调用声明了此类型的监听器。

代码片段:

AbstractApplicationEventMulticaster.retrieveApplicationListeners(ResolvableType eventType, Class<?> sourceType, ListenerRetriever retriever)

for (ApplicationListener<?> listener : listeners) {
if (supportsEvent(listener, eventType, sourceType)) {
if (retriever != null) {
retriever.applicationListeners.add(listener);
}
allListeners.add(listener);
}
}

循环判断所有的监听器(ApplicationListener)判断其是否支持当前所处理的事件(ApplicationStartingEvent)。

protected boolean supportsEvent(
ApplicationListener<?> listener, ResolvableType eventType, @Nullable Class<?> sourceType) { GenericApplicationListener smartListener = (listener instanceof GenericApplicationListener ?
(GenericApplicationListener) listener : new GenericApplicationListenerAdapter(listener));
return (smartListener.supportsEventType(eventType) && smartListener.supportsSourceType(sourceType));
}

在判断当前监听器是否支持指定事件之前,将当前监听器转换为了GenericApplicationListener

然后在进行判断,看看它转换为泛型监听器的时候作了什么。

public GenericApplicationListenerAdapter(ApplicationListener<?> delegate) {
Assert.notNull(delegate, "Delegate listener must not be null");
this.delegate = delegate;
this.declaredEventType = resolveDeclaredEventType(this.delegate);
}

通过GenericApplicationListener的构造方法可以看出它获取了监听器声明的事件类型.

private static ResolvableType resolveDeclaredEventType(ApplicationListener<ApplicationEvent> listener) {
ResolvableType declaredEventType = resolveDeclaredEventType(listener.getClass());
if (declaredEventType == null || declaredEventType.isAssignableFrom(ApplicationEvent.class)) {
Class<?> targetClass = AopUtils.getTargetClass(listener);
if (targetClass != listener.getClass()) {
declaredEventType = resolveDeclaredEventType(targetClass);
}
}
return declaredEventType;
}

根据代码可以看出它比较了当前处理的事件和监听器处理的事件是否相符

public boolean supportsEventType(ResolvableType eventType) {
if (this.delegate instanceof SmartApplicationListener) {
Class<? extends ApplicationEvent> eventClass = (Class<? extends ApplicationEvent>) eventType.resolve();
return (eventClass != null && ((SmartApplicationListener) this.delegate).supportsEventType(eventClass));
}
else {
return (this.declaredEventType == null || this.declaredEventType.isAssignableFrom(eventType));
}
}

通过笔者分析代码时的应用程序执行情况来看,笔者的应用程序在处理一共捕获了如下监听器,他们都监听了ApplicationStartingEvent事件

笔者的处理ApplicationStartingEvent监听器列表:

LoggingApplicationListener

BackgroundPreinitializer

DelegatingApplicationListener

LiquibaseServiceLocatorApplicationListener

可以简单来看看这些监听器都有什么特点:

LoggingApplicationListener监听器,可以看出它内部声明了需要关注的事件类型数组包含ApplicationStartingEvent。


public class LoggingApplicationListener implements GenericApplicationListener
public boolean supportsEventType(ResolvableType resolvableType) {
return this.isAssignableFrom(resolvableType.getRawClass(), EVENT_TYPES);
} static {
// 省略部分无关代码……
EVENT_TYPES = new Class[]{ApplicationStartingEvent.class, ApplicationEnvironmentPreparedEvent.class, ApplicationPreparedEvent.class, ContextClosedEvent.class, ApplicationFailedEvent.class};
SOURCE_TYPES = new Class[]{SpringApplication.class, ApplicationContext.class};
shutdownHookRegistered = new AtomicBoolean(false);
}

BackgroundPreinitializer监听器:

public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent>

public void onApplicationEvent(SpringApplicationEvent event) {
if (!Boolean.getBoolean("spring.backgroundpreinitializer.ignore") && event instanceof ApplicationStartingEvent && preinitializationStarted.compareAndSet(false, true)) {
this.performPreinitialization();
} }

通过上面两个被筛选出来的处理ApplicationStartingEvent事件的监听器案例会发现他们一个实现了ApplicationListener接口,一个实现了GenericApplicationListener接口,后者继承了前者,多了两个判断事件类型的方法和默认的排序优先级(默认最低)

小结

由一个最先执行的Starting事件我们可以得知SpringBoot是如何处理事件,以及事件的匹配是如何实现。后续的其他事件处理都是同样的方式。

在run方法的try catch代码块内部,开始处理有关上下文的一些流程。SpringBoot也设计了精简的流程来处理不同的任务,具体来说就是如下几个方法共同完成每个阶段的任务。

prepareEnvironment(listeners, applicationArguments);

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

refreshContext(context);

afterRefresh(context, applicationArguments);

在如上几个阶段中可以发现在准备环境和准备上下文的过程中都传入了监听器,意味着它们会被调用。

环境准备阶段

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
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;
}

在方法内部可以看到它创建了环境对象,并在创建完毕的时候调用了listeners.environmentPrepared(environment)方法,触发了ApplicationEnvironmentPreparedEvent事件。通知其他监听器环境信息准备完毕。

上下文准备阶段

创建ApplicationContext

根据当前应用类型创建指定的上下文容器,供后续准备上下文使用

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);
}

准备上下文

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
context.setEnvironment(environment);
postProcessApplicationContext(context);
applyInitializers(context);
listeners.contextPrepared(context);
if (this.logStartupInfo) {
logStartupInfo(context.getParent() == null);
logStartupProfileInfo(context);
}
// Add boot specific singleton beans
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
if (printedBanner != null) {
beanFactory.registerSingleton("springBootBanner", printedBanner);
}
if (beanFactory instanceof DefaultListableBeanFactory) {
((DefaultListableBeanFactory) beanFactory)
.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
}
// Load the sources
Set<Object> sources = getAllSources();
Assert.notEmpty(sources, "Sources must not be empty");
load(context, sources.toArray(new Object[0]));
listeners.contextLoaded(context);
}

在此环节下,将环境对象放入上下文中,初始化BeanNameGenerator、ResourceLoader、ConversionService。执行ApplicationContextInitializer接口的实现类中的initialize()方法。接着调用了listeners.contextPrepared(context)方法,此方法对应处理ApplicationContextInitializedEvent事件,通知其他注册了此事件的监听器。

protected void applyInitializers(ConfigurableApplicationContext context) {
for (ApplicationContextInitializer initializer : getInitializers()) {
Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(),
ApplicationContextInitializer.class);
Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
initializer.initialize(context);
}
}

ApplicationContextInitializer接口也是Spring的重要扩展接口之一,著名的配置中心:携程Apollo中就有很棒的应用。可以展示一下代码片段:

public class ApolloApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {

	private static final Logger logger = LoggerFactory.getLogger(ApolloApplicationContextInitializer.class);
private static final Splitter NAMESPACE_SPLITTER = Splitter.on(",").omitEmptyStrings().trimResults(); private final ConfigPropertySourceFactory configPropertySourceFactory = ApolloInjector
.getInstance(ConfigPropertySourceFactory.class); @Override
public void initialize(ConfigurableApplicationContext context) {
ConfigurableEnvironment environment = context.getEnvironment();
String enabled = environment.getProperty(PropertySourcesConstants.SURK_BOOTSTRAP_ENABLED, "false");
if (!Boolean.valueOf(enabled)) {
logger.debug("Apollo bootstrap config is not enabled for context {}, see property: ${{}}", context, PropertySourcesConstants.SURK_BOOTSTRAP_ENABLED);
return;
}
logger.debug("Apollo bootstrap config is enabled for context {}", context); if (environment.getPropertySources().contains(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
//already initialized
return;
} String namespaces = environment.getProperty(PropertySourcesConstants.APOLLO_BOOTSTRAP_NAMESPACES, ConfigConsts.NAMESPACE_APPLICATION);
logger.debug("Apollo bootstrap namespaces: {}", namespaces);
List<String> namespaceList = NAMESPACE_SPLITTER.splitToList(namespaces); CompositePropertySource composite = new CompositePropertySource(PropertySourcesConstants.APOLLO_BOOTSTRAP_PROPERTY_SOURCE_NAME);
for (String namespace : namespaceList) {
Config config = ConfigService.getConfig(namespace); composite.addPropertySource(configPropertySourceFactory.getConfigPropertySource(namespace, config));
} environment.getPropertySources().addFirst(composite);
}
}

Apollo实现此接口的目的是为了实现在应用还未启动,容器还未刷新,Bean实例还未装载的时候就将配置获取到放入环境信息中,待使用这些配置的Bean真正创建的时候就可以直接使用,实现了优先加载配置的能力。

回到当前阶段的处理,完成了ApplicationContextInitializedEvent事件通知之后,开始加载BeanDefinition,此处不作分析,紧接着调用了listeners.contextLoaded(context)方法处理ApplicationPreparedEvent事件。完成了上下文准备工作。

刷新容器

private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
} protected void refresh(ApplicationContext applicationContext) {
Assert.isInstanceOf(AbstractApplicationContext.class, applicationContext);
((AbstractApplicationContext) applicationContext).refresh();
}

底层还是调用ApplicationContext的.refresh()方法,此处不作解读。刷新完毕之后触发ApplicationStartedEvent事件,通知其他监听器作相应处理。

最后调用Runner

private void callRunners(ApplicationContext context, ApplicationArguments args) {
List<Object> runners = new ArrayList<>();
runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
AnnotationAwareOrderComparator.sort(runners);
for (Object runner : new LinkedHashSet<>(runners)) {
if (runner instanceof ApplicationRunner) {
callRunner((ApplicationRunner) runner, args);
}
if (runner instanceof CommandLineRunner) {
callRunner((CommandLineRunner) runner, args);
}
}
}

由此可见,Runner优先级最低。在容器刷新完毕之后才调用,可以实现一些容器加载完毕之后的逻辑。例如spring-batch就有一个JobLauncherCommandLineRunner用于批处理。至此run方法执行完毕。

总结:

通过简要的分析SpringBoot启动过程,可以发现,在应用启动过程中涉及到多个事件,通过EventPublishingRunListener来触发他们,同时又调用了ApplicationContextInitializer接口完成一些特定操作。

大体步骤可以总结为:开始启动-> 准备环境 -> 准备上下文 -> 刷新上下文 -> 后置处理。通过监听容器启动相关的事件可以在容器启动的各个阶段进行功能扩展,同时也展示了Apollo是如何使用本文涉及到的扩展接口。

(一)SpringBoot启动过程的分析-启动流程概览的更多相关文章

  1. (五)SpringBoot启动过程的分析-刷新ApplicationContext

    -- 以下内容均基于2.1.8.RELEASE版本 紧接着上一篇[(四)SpringBoot启动过程的分析-预处理ApplicationContext] (https://www.cnblogs.co ...

  2. (四)SpringBoot启动过程的分析-预处理ApplicationContext

    -- 以下内容均基于2.1.8.RELEASE版本 紧接着上一篇(三)SpringBoot启动过程的分析-创建应用程序上下文,本文将分析上下文创建完毕之后的下一步操作:预处理上下文容器. 预处理上下文 ...

  3. (三)SpringBoot启动过程的分析-创建应用程序上下文

    -- 以下内容均基于2.1.8.RELEASE版本 紧接着上一篇(二)SpringBoot启动过程的分析-环境信息准备,本文将分析环境准备完毕之后的下一步操作:ApplicationContext的创 ...

  4. (二)SpringBoot启动过程的分析-环境信息准备

    -- 以下内容均基于2.1.8.RELEASE版本 由上一篇SpringBoot基本启动过程的分析可以发现在run方法内部启动SpringBoot应用时采用多个步骤来实现,本文记录启动的第二个环节:环 ...

  5. U-Boot启动过程完全分析

    U-Boot启动过程完全分析 1.1       U-Boot工作过程 U-Boot启动内核的过程可以分为两个阶段,两个阶段的功能如下: (1)第一阶段的功能 硬件设备初始化 加载U-Boot第二阶段 ...

  6. Android系统默认Home应用程序(Launcher)的启动过程源代码分析

    在前面一篇文章中,我们分析了Android系统在启动时安装应用程序的过程,这些应用程序安装好之后,还需要有一个 Home应用程序来负责把它们在桌面上展示出来,在Android系统中,这个默认的Home ...

  7. Android系统进程间通信(IPC)机制Binder中的Server启动过程源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6629298 在前面一篇文章浅谈Android系 ...

  8. Android应用程序组件Content Provider的启动过程源代码分析

    文章转载至CSDN社区罗升阳的安卓之旅,原文地址:http://blog.csdn.net/luoshengyang/article/details/6963418 通过前面的学习,我们知道在Andr ...

  9. (4.20)SQL Server数据库启动过程,以及启动不起来的各种问题的分析及解决技巧

    转自:指尖流淌 https://www.cnblogs.com/zhijianliutang/p/4085546.html SQL Server数据库启动过程,以及启动不起来的各种问题的分析及解决技巧 ...

随机推荐

  1. php 安装 yii framework notice-error 的解决方案!

    1 问题描述: 2 解决方案: error_reporting(0); //解决error_notice 的最简单最有效的方法在每一个php文件的头部都加上error_reporting(0); 3. ...

  2. zsh terminal set infinity scroll height

    zsh terminal set infinity scroll height zsh Terminal 开启无限滚动 https://stackoverflow.com/questions/2761 ...

  3. what's the print number means after called the setTimeout function in Chrome console?

    what's the print number means after called the setTimeout function in Chrome console? javascript fun ...

  4. HEVC Advance & H.265 专利费

    HEVC Advance & H.265 专利费 https://www.hevcadvance.com/pdfnew/HEVC_Advance_Program_Overview_cn.pdf

  5. React & redux-saga & effects & Generator function & React Hooks

    React & redux-saga & effects & Generator function & React Hooks demos https://github ...

  6. TS & error

    TS & error Function implementation is missing or not immediately following the declaration.ts ht ...

  7. 近期最值得关注的潜力币种——VAST

    近期币圈的热度又再次被掀起,很多新的币种也争相上线,还有一些币种虽然还未上线,但是在币圈的火热程度也非同一般.小编留意了一下,最近在币圈讨论的最火的便是VAST代币.许多生态建设者乃至机构都表示很看好 ...

  8. Masterboxan INC 下半年将聚焦超高净值和家族全权委托客户

    "投资是一个没有终点的奋斗.我们不能简单的预测市场,而是应对市场做出正确的反应.这需要我们不断反思.总结.提升,找到自己的投资哲学,然后用一生的时间去坚守."Masterboxan ...

  9. .NET探索模型路由约定实现伪静态

    概述 IPageRouteModelConvention接口用于自定义PageRouteModel,这个对象在Microsoft.AspNetCore.Mvc.ApplicationModels命名空 ...

  10. 大小厂必问Java后端面试题(含答案)

    你好,我是yes. 这个系列的文章不会是背诵版,不是那种贴上标准答案,到时候照着答就行的面试题汇总. 我会用大白话尽量用解释性.理解性的语言来回答,但是肯定没有比平时通过一篇文章来讲解清晰,不过我尽量 ...