SpringBoot(四)SpringApplication启动类运行阶段 - SpringApplicationRunListener
前言
最近在学习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