前言
最近在學習Spring Boot相關的課程,過程中以筆記的形式記錄下來,方便以后回憶,同時也在這里和大家探討探討,文章中有漏的或者有補充的、錯誤的都希望大家能夠及時提出來,本人在此先謝謝了!
開始之前呢,希望大家帶着幾個問題去學習:
1、Spring Boot SpringApplication 是什么?
2、整體流程或結構是怎樣的?
3、重點內容或者核心部分是什么?
4、怎么實現的?
5、是怎么和 Spring 關聯起來的?
這是對自我的提問,我認為帶着問題去學習,是一種更好的學習方式,有利於加深理解。好了,接下來進入主題。
1、起源
上篇文章我們講了 SpringApplication
的准備階段,在這個階段,完成了運行時所需要准備的資源,如:initializers
、listeners
等。而這篇文章我們就來講講 SpringApplication
的運行階段,在這個階段,它是如何啟動 Spring
應用上下文的,且如何與 Spring
事件結合起來,形成完整的 SpringApplication
生命周期的。
注:本篇文章所用到的
Spring Boot
版本是2.1.6.BUILD-SNAPSHOT
2、SpringApplication 運行階段
上篇文章我們講了 SpringApplication
的構造方法,這里我們就來講講 SpringApplication
的核心,也就是run方法,代碼如下:
public class SpringApplication {
...
public ConfigurableApplicationContext run(String... args) {
// 這是 Spring 的一個計時器,計算代碼的執行時間(ms級別)
StopWatch stopWatch = new StopWatch();
stopWatch.start();
// 這倆變量在后面賦值處進行說明
ConfigurableApplicationContext context = null;
Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
// 用來設置java.awt.headless屬性值
configureHeadlessProperty();
// 該對象屬於組合模式的實現,核心是內部關聯的 SpringApplicationRunListener 集合,SpringApplicationRunListener 是 Spring Boot 的運行時監聽器
SpringApplicationRunListeners listeners = getRunListeners(args);
// 會在不同的階段調用對應的方法,這里表示啟動run方法被調用
listeners.starting();
try {
// 用來獲取 SpringApplication.run(args)傳入的參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 獲取 properties 配置文件
ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
// 設置 spring.beaninfo.ignore 的屬性值,判斷是否跳過搜索BeanInfo類
configureIgnoreBeanInfo(environment);
// 這里是項目啟動時,控制台打印的 Banner
Banner printedBanner = printBanner(environment);
// 這里就是創建 Spring 應用上下文
context = createApplicationContext();
// 獲取 spring.factories 中key為 SpringBootExceptionReporter 的類名集合
exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
new Class[] { ConfigurableApplicationContext.class }, context);
// 這里是准備 Spring 應用上下文
prepareContext(context, environment, listeners, applicationArguments, printedBanner);
// 這里是啟動 Spring 應用上下文,底層調用的是 ApplicationContext 的 refresh() 方法,到這里就正式進入了 Spring 的生命周期,同時,SpringBoot的自動裝配特性也隨之啟動
refreshContext(context);
// 里面是空的,猜測應該是交由開發人員自行擴展
afterRefresh(context, applicationArguments);
stopWatch.stop();
// 這里打印啟動信息
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// ApplicationContext 啟動時,調用該方法
listeners.started(context);
// 項目啟動后,做的一些操作,開發人員可自行擴展
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, listeners);
throw new IllegalStateException(ex);
}
try {
// ApplicationContext 啟動完成時,調用該方法
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, exceptionReporters, null);
throw new IllegalStateException(ex);
}
return context;
}
...
}
上面就是整個過程的概覽,可以看到,在運行階段執行的操作比較多,雖然看起來雜亂無章,但其實還是有規律可循的。比如,執行的 SpringApplicationRunListeners
中的階段方法,剛啟動階段的 starting
、已啟動階段的 started
、啟動完成階段的 running
等。還有對應的 Spring
應用上下文的創建、准備、啟動操作等。接下來,就對里面的幾個核心對象進行討論。
2.1 SpringApplicationRunListeners 結構
我們先來看看 SpringApplicationRunListeners
對象,從代碼可以看出該對象是由 getRunListeners
方法創建的:
private SpringApplicationRunListeners getRunListeners(String[] args) {
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}
可以看到,通過傳入的 getSpringFactoriesInstances
方法的返回值,執行 SpringApplicationRunListeners
的構造方法,進行對象的創建。接着看 getSpringFactoriesInstances
方法:
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = getClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
看到這大家應該比較熟悉了,通過前面幾篇文章的討論我們知道,該方法通過 SpringFactoriesLoader.loadFactoryNames
返回所有 classpass 下的 spring.factories
文件中 key 為 SpringApplicationRunListener
的實現類集合。如 Spring Boot 的內建實現:
# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener
最后,就是將該集合傳入 SpringApplicationRunListeners
的構造方法:
class SpringApplicationRunListeners {
...
private final List<SpringApplicationRunListener> listeners;
SpringApplicationRunListeners(Log log, Collection<? extends SpringApplicationRunListener> listeners) {
this.log = log;
this.listeners = new ArrayList<>(listeners);
}
public void starting() {
for (SpringApplicationRunListener listener : this.listeners) {
listener.starting();
}
}
...
}
里面是將集合賦值到 listeners
屬性,可以看到 SpringApplicationRunListeners
屬於組合模式的實現,核心其實是內部關聯的 SpringApplicationRunListener
對象集合,當外部調用該階段方法時,就會迭代執行集合中 SpringApplicationRunListener
對應的方法。所以接下來我們就來討論 SpringApplicationRunListener
。
2.1.1 SpringApplicationRunListener 事件和監聽機制
SpringApplicationRunListener
負責在 SpringBoot
的不同階段廣播相應的事件,然后調用實際的 ApplicationListener
類,在該類的 onApplicationEvent
方法中,根據不同的 Spring Boot
事件執行相應操作。整個過程大概如此,接下來進行詳細討論,先來看看 SpringApplicationRunListener
定義:
public interface SpringApplicationRunListener {
// 在run()方法開始執行時被調用,表示應用剛剛啟動,對應的 Spring Boot 事件為 ApplicationStartingEvent
void starting();
// ConfigurableEnvironment 構建完成時調用,對應的 Spring Boot 事件為 ApplicationEnvironmentPreparedEvent
void environmentPrepared(ConfigurableEnvironment environment);
// ApplicationContext 構建完成時調用,對應的 Spring Boot 事件為 ApplicationContextInitializedEvent
void contextPrepared(ConfigurableApplicationContext context);
// ApplicationContext 完成加載但還未啟動時調用,對應的 Spring Boot 事件為 ApplicationPreparedEvent
void contextLoaded(ConfigurableApplicationContext context);
// ApplicationContext 已啟動,但 callRunners 還未執行時調用,對應的 Spring Boot 事件為 ApplicationStartedEvent
void started(ConfigurableApplicationContext context);
// ApplicationContext 啟動完畢被調用,對應的 Spring Boot 事件為 ApplicationReadyEvent
void running(ConfigurableApplicationContext context);
// 應用出錯時被調用,對應的 Spring Boot 事件為 ApplicationFailedEvent
void failed(ConfigurableApplicationContext context, Throwable exception);
}
我們來看看它的實現類,也就是上面加載的 spring.factories
文件中的 EventPublishingRunListener
類,該類也是 Spring Boot
內建的唯一實現類,具體廣播事件的操作在該類中進行,代碼如下:
public class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
private final SpringApplication application;
private final String[] args;
private final SimpleApplicationEventMulticaster 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);
}
}
@Override
public void starting() {
this.initialMulticaster.multicastEvent(new ApplicationStartingEvent(this.application, this.args));
}
...
}
可以看到,通過構造方法創建 EventPublishingRunListener
實例的過程中,調用了 getListeners
方法,將 SpringApplication
中所有 ApplicationListener
監聽器關聯到了 initialMulticaster
屬性中。沒錯,這里的 ApplicationListener
監聽器就是上篇文章中在 SpringApplication
准備階段從 spring.factories
文件加載的 key 為 ApplicationListener
的實現類集合,該實現類集合全部重寫了 onApplicationEvent
方法。
2.1.2 SimpleApplicationEventMulticaster 廣播器
這里又引出了另一個類, 也就是 SimpleApplicationEventMulticaster
,該類是 Spring
的事件廣播器,也就是通過它來廣播各種事件。接着,當外部迭代的執行到 EventPublishingRunListener
的 starting
方法時,會通過 SimpleApplicationEventMulticaster
的 multicastEvent
方法進行事件的廣播,這里廣播的是 ApplicationStartingEvent
事件,我們進入 multicastEvent
方法:
public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster {
...
@Override
public void multicastEvent(ApplicationEvent event) {
multicastEvent(event, resolveDefaultEventType(event));
}
@Override
public void multicastEvent(final ApplicationEvent event, @Nullable ResolvableType eventType) {
ResolvableType type = (eventType != null ? eventType : resolveDefaultEventType(event));
Executor executor = getTaskExecutor();
for (ApplicationListener<?> listener : getApplicationListeners(event, type)) {
if (executor != null) {
executor.execute(() -> invokeListener(listener, event));
}
else {
invokeListener(listener, event);
}
}
}
}
通過 getApplicationListeners
方法,根據事件類型返回從上面關聯的 ApplicationListener
集合中篩選出匹配的 ApplicationListener
集合,根據 Spring Boot
版本的不同,在這個階段獲取到的監聽器也有可能不同,如 2.1.6.BUILD-SNAPSHOT
版本返回的是:
然后依次遍歷這些監聽器,同步或異步的調用 invokeListener
方法:
protected void invokeListener(ApplicationListener<?> listener, ApplicationEvent event) {
ErrorHandler errorHandler = getErrorHandler();
if (errorHandler != null) {
try {
doInvokeListener(listener, event);
}
catch (Throwable err) {
errorHandler.handleError(err);
}
}
else {
doInvokeListener(listener, event);
}
}
...
private void doInvokeListener(ApplicationListener listener, ApplicationEvent event) {
try {
listener.onApplicationEvent(event);
}
catch (ClassCastException ex) {
String msg = ex.getMessage();
if (msg == null || matchesClassCastMessage(msg, event.getClass())) {
// Possibly a lambda-defined listener which we could not resolve the generic event type for
// -> let's suppress the exception and just log a debug message.
Log logger = LogFactory.getLog(getClass());
if (logger.isTraceEnabled()) {
logger.trace("Non-matching event type for listener: " + listener, ex);
}
}
else {
throw ex;
}
}
}
可以看到,最終調用的是 doInvokeListener
方法,在該方法中執行了 ApplicationListener
的 onApplicationEvent
方法,入參為廣播的事件對象。我們就拿其中一個的監聽器來看看 onApplicationEvent
中的實現,如 BackgroundPreinitializer
類:
public class BackgroundPreinitializer implements ApplicationListener<SpringApplicationEvent> {
...
@Override
public void onApplicationEvent(SpringApplicationEvent event) {
if (!Boolean.getBoolean(IGNORE_BACKGROUNDPREINITIALIZER_PROPERTY_NAME)
&& event instanceof ApplicationStartingEvent && preinitializationStarted.compareAndSet(false, true)) {
performPreinitialization();
}
if ((event instanceof ApplicationReadyEvent || event instanceof ApplicationFailedEvent)
&& preinitializationStarted.get()) {
try {
preinitializationComplete.await();
}
catch (InterruptedException ex) {
Thread.currentThread().interrupt();
}
}
}
...
}
在該方法中,通過 instanceof
判斷事件的類型,從而進行相應的操作。該監聽器主要的操作是新建一個后台線程去執行那些耗時的初始化工作,包括驗證器、消息轉換器等。LoggingApplicationListener
監聽器則是對 Spring Boot
的日志系統做一些初始化的前置操作。另外兩個監聽器在該階段無任何操作。
至此,SpringBoot
事件機制的整體流程大概如此,我們簡要回顧一下幾個核心組件:
-
SpringApplicationRunListeners:首先,在
run
方法的執行過程中,通過該類在SpringBoot
不同的階段調用不同的階段方法,如在剛啟動階段調用的starting
方法。 -
SpringApplicationRunListener:而
SpringApplicationRunListeners
屬於組合模式的實現,它里面關聯了SpringApplicationRunListener
實現類集合,當外部調用階段方法時,會迭代執行該集合中的階段方法。實現類集合是spring.factories
文件中定義好的類。這里是一個擴展點,詳細的后面述說。 -
EventPublishingRunListener:該類是
Spring Boot
內置的SpringApplicationRunListener
唯一實現類,所以,當外部調用各階段的方法時,真正執行的是該類中的方法。 -
SimpleApplicationEventMulticaster:在階段方法中,會通過
Spring
的SimpleApplicationEventMulticaster
事件廣播器,廣播各個階段對應的事件,如這里的starting
方法廣播的事件是ApplicationStartingEvent
。 -
ApplicationListener:最后
ApplicationListener
的實現類也就是Spring Boot
監聽器會監聽到廣播的事件,根據不同的事件,進行相應的操作。這里的Spring Boot
監聽器是也是在spring.factories
中定義好的,這里我們也可自行擴展。
到這里 Spring Boot
事件監聽機制差不多就結束了,值得注意的是 Spring Boot
監聽器實現的是 Spring
的 ApplicationListener
類,事件類最終繼承的也是 Spring
的 ApplicationEvent
類,所以,Spring Boot
的事件和監聽機制都基於 Spring
而實現的。
2.2 ApplicationArguments 加載啟動參數
當執行完 listeners.starting
方法后,接着進入構造 ApplicationArguments
階段:
public class SpringApplication {
...
public ConfigurableApplicationContext run(String... args) {
...
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting();
try {
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
...
}
}
...
}
該類是用於簡化 Spring Boot
應用啟動參數的封裝接口,我們啟動項目時輸入的命令參數會封裝在該類中。一種是通過 IDEA 輸入的參數,如下:
另一種是 springboot
jar包運行時傳遞的參數:cmd中運行java -jar xxx.jar name=張三 pwa=123
。
然后,可以通過 @Autowired
注入 ApplicationArguments
的方式進行使用:
public class Test {
@Autowired
private ApplicationArguments applicationArguments;
public void getArgs() {
// 獲取 args 中的所有 non option 參數
applicationArguments.getNonOptionArgs();
// 獲取 args 中所有的 option 參數的 name
applicationArguments.getOptionNames();
// 獲取傳遞給應用程序的原始未處理參數
applicationArguments.getSourceArgs();
// 獲取 args 中指定 name 的 option 參數的值
applicationArguments.getOptionValues("nmae");
// 判斷從參數中解析的 option 參數是否包含指定名稱的選項
applicationArguments.containsOption("name");
}
}
2.3 ConfigurableEnvironment 加載外部化配置
接着進入構造 ConfigurableEnvironment
的階段,該類是用來處理我們外部化配置的,如 properties
、YAML
等,提供對配置文件的基礎操作。當然,它能處理的外部配置可不僅僅如此,詳細的在下篇文章討論,這里我們進行簡要了解即可,進入創建該類的 prepareEnvironment
方法:
private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
ApplicationArguments applicationArguments) {
// Create and configure the environment
ConfigurableEnvironment environment = getOrCreateEnvironment();
configureEnvironment(environment, applicationArguments.getSourceArgs());
listeners.environmentPrepared(environment);
bindToSpringApplication(environment);
if (!this.isCustomEnvironment) {
environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
deduceEnvironmentClass());
}
ConfigurationPropertySources.attach(environment);
return environment;
}
這里通過 getOrCreateEnvironment
方法返回具體的 Environment
:
private ConfigurableEnvironment getOrCreateEnvironment() {
if (this.environment != null) {
return this.environment;
}
switch (this.webApplicationType) {
case SERVLET:
return new StandardServletEnvironment();
case REACTIVE:
return new StandardReactiveWebEnvironment();
default:
return new StandardEnvironment();
}
}
可以看到,這里通過 webApplicationType
屬性來判斷當前應用的類型,有 Servlet
、 Reactive
、 非Web 3種類型,該屬性也是在上篇文章中 SpringApplication
准備階段確定的,這里我們通常都是 Servlet
類型,返回的是 StandardServletEnvironment
實例。
之后,還調用了 SpringApplicationRunListeners
的 environmentPrepared
階段方法,表示 ConfigurableEnvironment
構建完成,同時向 Spring Boot
監聽器發布 ApplicationEnvironmentPreparedEvent
事件。監聽該事件的監聽器有:
2.4 ConfigurableApplicationContext 創建 Spring 應用上下文
這里通過 createApplicationContext
方法創建 Spring
應用上下文,實際上 Spring
的應用上下文才是驅動 Spring Boot
的核心引擎:
public class SpringApplication {
...
public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context."
+ "annotation.AnnotationConfigApplicationContext";
public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot."
+ "web.servlet.context.AnnotationConfigServletWebServerApplicationContext";
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
屬性來確定應用類型從而創建 String
上下文,上篇文章說到該屬性值是在 Spring Boot
准備階段推導出來的。這里我們的應用類型是 Servlet
,所以創建的是 AnnotationConfigServletWebServerApplicationContext
對象。創建完 Spring
應用上下文之后,執行 prepareContext
方法進入准備上下文階段:
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);
}
我們來看看主要做了哪些操作:
-
設置了
Spring
應用上下文的ApplicationArguments
,上面說過是處理外部化配置的,具體類型為StandardServletEnvironment
。 -
Spring
應用上下文后置處理,主要是覆蓋當前Spring
應用上下文默認所關聯的ResourceLoader
和ClassLoader
。 -
執行
Spring
的初始化器,上篇文章說過在Spring Boot
准備階段初始化了一批在spring.factories
文件中定義好的ApplicationContextInitializer
,這里就是執行它們的initialize
方法,同時這里也是一個擴展點,后面詳細討論。 -
執行
SpringApplicationRunListeners
的contextPrepared
階段方法,表示ApplicationContext
准備完成,同時向Spring Boot
監聽器發布ApplicationContextInitializedEvent
事件 。 -
將
springApplicationArguments
和springBootBanner
注冊為Bean
。 -
加載
Spring
應用上下文的配置源,也是在上篇文章Spring Boot
准備階段獲取的primarySources
和sources
,primarySources
來源於SpringApplication
構造器參數,sources
則來源於自定義配置的setSources
方法。 -
最后執行
SpringApplicationRunListeners
的contextLoaded
階段方法,表示ApplicationContext
完成加載但還未啟動,同時向Spring Boot
監聽器發布ApplicationPreparedEvent
事件 。
接下來就是真正啟動階段,執行的是 refreshContext
方法:
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();
}
可以看到,底層調用的是 AbstractApplicationContext
的 refresh
方法,到這里 Spring
應用正式啟動,Spring Boot
核心特性也隨之啟動,如自動裝配。隨后執行 SpringApplicationRunListeners
的 started
階段方法,表示 ApplicationContext
已啟動,同時向 Spring Boot
監聽器發布 ApplicationStartedEvent
事件 。但還未啟動完成,后面還有一個 callRunners
方法,一般來講,里面執行一些我們自定義的操作。之后 Spring
應用才算啟動完成,隨后調用 running
方法,發布 ApplicationReadyEvent
事件。至此,SpringApplication
運行階段結束。
3、總結
最后來對 SpringApplication
運行階段做一個總結。這個階段核心還是以啟動 Spring
應用上下文為主,同時根據應用類型來初始化不同的上下文對象,但這些對象的基類都是 Spring
的 ConfigurableApplicationContext
類。且在啟動的各個階段中,使用 SpringApplicationRunListeners
進行事件廣播,回調 Spring Boot
的監聽器。同時還初始化了 ApplicationArguments
、ConfigurableEnvironment
等幾個組件。下篇文章我們就來討論 Spring Boot
的外部化配置部分,來看看為什么外部的各個組件,如 Redis
、Dubbo
等在 properties
文件中進行相應配置后,就可以正常使用。
以上就是本章的內容,如過文章中有錯誤或者需要補充的請及時提出,本人感激不盡。
參考:
《Spring Boot 編程思想》
https://www.cnblogs.com/youzhibing/p/9603119.html
https://www.jianshu.com/p/b86a7c8b3442
https://www.cnblogs.com/duanxz/p/11243271.html
https://www.jianshu.com/p/7a674c59d76e