SpringBoot之自动装配和启动流程分析 文章目录1 注解启动分析1.1 SpringBootConfiguration1.2 ComponentScan1.3 自动装配EnableAutoConfiguration1.3.1 AutoConfigurationPackage1.3.2 Import({AutoConfigurationImportSelector.class})1.3.2.1 AutoConfigurationImportSelector.class1.3.2.2 getAutoConfigurationEntry方法1.3.2.3 loadFactoryNames方法1.3.2.4 loadSpringFactories方法1.3.2.5 cache探秘1.3.2.6 getAutoConfigurationEntry再探1.3.2.7 自动装配本质1.3.2.8 自动装配总结1.3.3 自动装配用到类总结2 SpringApplication引导启动2.1 SpringApplication—创建引导启动的实例2.2 run()—开始引导启动2.2.1 new StopWatch()—创建计时器2.2.2 configureHeadlessProperty()—配置Headless模式2.2.3 SpringApplicationRunListener.start()—获取监听器,启动监听2.2.4 prepareEnvironment()—准备环境创建ConfigurableEnvironment对象2.2.5 printBanner()—打印横幅2.2.6 createApplicationContext()—创建应用程序上下文并加载Bean2.2.7 prepareContext()—准备ApplicationContext2.2.8 refreshContext()—刷新上下文2.2.8.1 prepareRefresh()—准备刷新2.2.8.2 prepareBeanFactory()—准备BeanFactory2.2.8.3 postProcessBeanFactory()—后置处理BeanFactory2.2.8.4 invokeBeanFactoryPostProcessors()—实例化并调用BeanFactoryPostProcessor2.2.8.5 registerBeanPostProcessors()—注册Bean后置处理器2.2.8.6 initMessageSource()—初始化MessageSource2.2.8.7 initApplicationEventMulticaster()—初始化ApplicationEventMulticaster2.2.8.8 onRefresh()—刷新应用程序2.2.8.9 registerListeners()—注册监听器2.2.8.10 finishBeanFactoryInitialzation()—初始化容器中的Bean2.2.8.11 finishRefresh()—完成刷新2.2.9 afterRefresh()—留给子类的钩子函数2.3 启动完成2.4 准备运行3 总结Spring Boot为我们提供了一种极简的项目搭建方式看一下Spring Boot项目的启动类SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[]args){SpringApplication.run(Application.class,args);}}1 注解启动分析注解启动分析就是说的SpringBootApplication首先看一下SpringBootApplication这个组合注解除去元注解外它还引入了其他三个重要的注解SpringBootConfigurationEnableAutoConfigurationComponentScan1.1 SpringBootConfigurationTarget(ElementType.TYPE)Retention(RetentionPolicy.RUNTIME)DocumentedConfigurationpublicinterfaceSpringBootConfiguration{}从源码可以看到其实SpringBootConfiguration并没有额外功能它只是Spring中Configuration的派生注解用于标注配置类完成Bean的配置与管理SpringBootConfiguration只是Spring标准Configuration批注的替代方法。 两者之间的唯一区别是SpringBootConfiguration允许自动找到配置应用程序应该只包含一个SpringBootConfiguration并且大多数惯用的SpringBoot应用程序将从SpringBootApplication继承它1.2 ComponentScanSpring中的注解用于包的扫描并把声明了特定注解的类交给spring的ioc容器1.3 自动装配EnableAutoConfigurationSpringBoot根据应用所声明的依赖来对Spring框架进行自动配置其中包括两个重要注解AutoConfigurationPackage和Import({AutoConfigurationImportSelector.class})1.3.1 AutoConfigurationPackageAutoConfigurationPackage该注解上有一个Import({Registrar.class})注解其中Registrar类的作用是将启动类所在的包下的所有子包组件扫描注入到spring容器中因此这就是为什么将controller、service等包放在启动类的同级目录下的原因1.3.2 Import({AutoConfigurationImportSelector.class})1.3.2.1 AutoConfigurationImportSelector.classSpring Boot有中一个非常重要的理念就是约定大于配置。而自动配置这一机制的核心实现就是靠EnableAutoConfiguration注解完成的可以看出在EnableAutoConfiguration注解中使用Import导入了AutoConfigurationImportSelector这个类实现了ImportSelector接口的selectImports()方法。spring中会把selectImports()方法返回的String数组中的类的全限定名实例化为bean并交给spring容器管理1.3.2.2 getAutoConfigurationEntry方法查看其中的getAutoConfigurationEntry方法在执行完getCandidateConfigurations后把众多类的全限定名存储到了一个List中SpringFactoriesLoader这个类非常重要属于Spring框架的一种扩展方案提供一种了配置查找的功能支持。其主要功能就是读取配置文件META-INF/spring.factories决定要加载哪些类当然并不是所有spring.factories中的类都会被加载到spring容器中很多情况下需要按照需求所需的情况引入依赖条件注解Conditional进行判断。例如ServletWebServerFactoryAutoConfiguration类只有在classpath下存在ServletRequest这一类时才将ServletWebServerFactoryAutoConfiguration作为配置类导入spring容器中1.3.2.3 loadFactoryNames方法进入loadFactoryNames方法这个方法非常简短因为他调用了真正实现的方法loadSpringFactories这一行return代码复制在下面loadSpringFactories(classLoader).getOrDefault(factoryTypeName,Collections.emptyList());可以分析得出loadSpringFactories方法的返回值又调用了一个getOrDefault方法这明显是一个容器类的方法目的是从容器中拿点东西出来就此推测loadSpringFactories返回了一个包含我们需要的Config全类名字符串的集合容器然后从这个集合容器中拿出来的东西就是我们的configurations1.3.2.4 loadSpringFactories方法看这个loadSpringFactories方法它确实返回了一个容器MapString, List这个容器的类型是MultiValueMapString, String这个数据结构就非常牛逼了多值集合映射简单来说一个key可以对应多个value根据他的返回值我们可以看到在这个方法中一个String对应了一个List接下来我们继续思考我们来的目的是获取configurations所以无论你做什么必须得读取配置文件拿到configurations于是我们在try方法体中果然发现了这个操作获取了一个路径urls那么这个路径是否就是我们前面验证的META-INF/spring.factories呢查看静态常量FACTORIES_RESOURCE_LOCATION的值继续往下看果然他遍历了urls中的内容从这个路径加载了配置文件终于看到了我们熟悉的loadProperties方法那我们大概就知道了他确实是通过找到路径然后根据路径读取了配置文件然后返回了读取的result这就是loadFactoryNames方法的内部实现。1.3.2.5 cache探秘细心地朋友已经发现了玄机隐藏在loadFactoryNames方法的开头和结尾它是从cache缓存中取出来的根据下面的if判断如果从缓存中读取出来了result并且result的结果不为空就直接返回不需要再进行下面的读写操作了这样减少了磁盘频繁的读写I/O同理更新完所有的配置文件资源之后退出时也要更新缓存。1.3.2.6 getAutoConfigurationEntry再探关键部分已经过去重新审视一下遗漏的内容还记得getAutoConfigurationEntry方法吗我们最后来研究一下这个类除了getCandidateConfigurations还干了哪些事情removeDuplicatesconfigurations.removeAll(exclusions)可以看到这里对加载进来的配置进行了去重、排除的操作这是为了使得用户自定义的排除包生效同时避免包冲突异常在SpringBoot的入口函数中我们可以通过注解指定需要排除哪些不用的包例如我不使用RabbitMQ的配置包就把它的配置类的class传给excludeSpringBootApplication(exclude{RabbitAutoConfiguration.class})1.3.2.7 自动装配本质SpringBoot自动装配的本质就是通过Spring去读取META-INF/spring.factories中保存的配置类文件然后加载bean定义的过程。如果是标了Configuration注解就是批量加载了里面的bean定义如何实现自动通过配置文件获取对应的批量配置类然后通过配置类批量加载bean定义只要有写好的配置文件spring.factories就实现了自动。1.3.2.8 自动装配总结Spring Boot的自动装配特性可以说是Spring Boot最重要、最核心的一环正是因为这个特性使得我们的生产复杂性大大降低极大地简化了开发流程Spring Boot自动装配详细流程图自动装配部分转载于https://mp.weixin.qq.com/s/1u_Pyu1CQbYrgELRbo8Qyw1.3.3 自动装配用到类总结最后总结一下整个自动装配的过程引入META-INF/spring.factories配置文件在EnableAutoConfiguration对应的value中配置需要引入的配置类。启动类增加EnableAutoConfiguration注解SpringBootApplication已经自带。EnableAutoConfiguration注解中通过Import标注了AutoConfigurationImportSelector类。AutoConfigurationImportSelector继承了DeferredImportSelector接口在Spring生命周期处理BeanFactoryPostProcessors的时候会对配置信息进行后置处理这是会调用到AutoConfigurationImportSelector.process方法。process方法中会读取META-INF/spring.factories配置文件中的内容为Key-Value形式读取完后值返回key EnableAutoConfiguration对应的配置类信息保存到autoConfigurationEntries中。AutoConfigurationGroup#selectImports方法返回排序、筛选后的配置类信息然后依次遍历递归调用processImports 根据这些配置类的全路径名读取并注册在 Spring 容器中。2 SpringApplication引导启动SpringApplication类是用来执行Spring框架启动的引导类。有两种方式可以进行启动引导通过静态方法SpringApplication.run启动。先创建SpringApplication实例在调用的实例方法run进行启动。无论是以上哪种方式最终都是通过创建SpringApplication实例在调用run()启动2.1 SpringApplication—创建引导启动的实例SpringApplication提供了一个简单的方式以启动Spring boot程序查看SpringApplication.run方法调用在此创建了一个SpringApplication的实例并调用了它的run方法在创建实例的过程中会根据用户输入和工程环境做一些基础配置供之后引导启动中使用:设置资源加载器ResourceLoader和PrimarySources用于将资源加载到加载器中从类中加载initializer和listener放在集合判断当前项目类型是什么 提供了NONESERVLETREACTIVE 三种类型备选使用SpringFactoriesLoader查找并加载所有可用的ApplicationContextInitializer使用SpringFactoriesLoader查找并加载所有可用的监听器ApplicationListener推断并设置main方法的定义类设置是否为Web环境(先确认用户是否指定未指定则根据工程目录下是否有servlet相关环境)从工程环境中决定主入口的类2.2 run()—开始引导启动SpringApplication完成初始化后调用run方法下面对run方法中核心代码进行分析按照图中标注序号进行分析spring监听器的使用要获取这些监听器的对象就要知道其全路径。通过SpringFactoriesLoader查找spring.factories获得之后再调用它们的started()方法。创建并配置当前Spring Boot应用将要使用的Environment根据监听器和默认应用参数来准备所需要的环境。打印Banner创建spring应用上下文。根据之前推断的项目类型决定该为当前SpringBoot应用创建什么类型的ApplicationContext并创建完成。准备应用上下文首先将之前准备好的Environment设置给创建好的ApplicationContext使用。然后遍历调用所有ApplicationContextInitializer的initialize方法来对已经创建好的ApplicationContext进行进一步的处理。最后遍历调用所有SpringApplicationRunListener的contextPrepared()方法。这里最终调用了Spring中AbstractApplicationContext的refresh方法可以说这个refresh方法是Spring中最重要的方法之一完成了Bean工厂创建后置管理器注册Bean实例化等最重要的工作。这一步工作完成后spring的ioc容器就完成了。如果有Bean实现了CommandLineRunner接口并重写了run方法则遍历执行CommandLineRunner中的方法2.2.1 new StopWatch()—创建计时器StopWatch是springframework.util中提供的一个工具类在启动过程中使用StopWatch是为了记录启动花费的时间。2.2.2 configureHeadlessProperty()—配置Headless模式Headless模式是在环境缺少显示器等设备情况下的一种配置和我们启动流程并无太多关系2.2.3 SpringApplicationRunListener.start()—获取监听器,启动监听监听器可以用来监听SpringApplication启动过程中的各个阶段。默认的监听器是EventPublishRunListener用户也可以通过实现SpringApplicationRunListener接口实现应用程序对SpringApplication启动过程的监听。在resources/META-INF下建立spring.factories文件文件中添加keyvalue形式其中key为SpringApplicationRunListener的全路径名value为应用程序对该接口的实现类(类需要一个参数类型为SpringApplication和String数组的构造函数用于通过反射创建实例)。2.2.4 prepareEnvironment()—准备环境创建ConfigurableEnvironment对象在这一步SpringApplication会创建Spring启动所需的环境这个环境主要由ConfigurableEnviroment对象表示。首先该对象确认了程序是否需要设置Web环境其次该对象还确定了程序所需要的参数和读取的配置文件等信息。此步骤会回调SpringApplicationRunListener的environmentPrepared()方法通知监听器环境已经准备好。2.2.5 printBanner()—打印横幅这一步骤其实和启动并没有太大关系只是会向控制台或是日志中输出Spring的Logo和版本信息。2.2.6 createApplicationContext()—创建应用程序上下文并加载Bean在准备好环境之后接下来要做的就是创建应用程序上下文ApplicationContext对象。ApplicationContext是Spring IoC的核心组件它不仅是程序所需Bean的容器还提供了国际化事件发布等功能。在创建应用程序上下文的时候首先会根据之前配置决定上下文的具体类型AnnotationConfigApplicationContext或是AnnotationConfigServletWebServerApplicationContext。再通过反射实例化到对象。protectedConfigurableApplicationContextcreateApplicationContext(){Class?contextClassthis.applicationContextClass;if(contextClassnull){try{switch(this.webApplicationType){caseSERVLET:contextClassClass.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);break;caseREACTIVE:contextClassClass.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);break;default:contextClassClass.forName(DEFAULT_CONTEXT_CLASS);}}catch(ClassNotFoundExceptionex){thrownewIllegalStateException(Unable to create a default ApplicationContext, please specify an ApplicationContextClass,ex);}}return(ConfigurableApplicationContext)BeanUtils.instantiateClass(contextClass);}在createApplicationContext()方法中Spring Boot首先会判断应用程序的类型如果是Web应用程序则会创建一个WebApplicationContext否则会创建一个普通的ApplicationContext。调用BeanUtils.instantiateClass(contextClass)方法创建应用程序的上下文。这个方法会根据上面的逻辑创建一个相应的ApplicationContext。调用load()方法加载应用程序的配置。这个方法会扫描classpath中的各种配置文件例如application.properties、application.yml、META-INF/spring.factories等自动配置各种组件和Bean。调用postProcessApplicationContext()方法对应用程序的上下文进行后处理。这个方法会调用各种初始化器和监听器执行各种初始化任务。2.2.7 prepareContext()—准备ApplicationContext虽然已经得到了ApplicationContext对象但此时的对象还只是一个空白对象需要准备和处理后ApplicationContext才能被使用。在准备过程中主要做了做了几件事为ApplicationContext设置之前准备好的Environment对象。对ApplicationContext进行后置处理设置beanNameGenerator、resourceLoader、conversionService等基础设施随后通过BeanDefinitionLoader等方式往容器中添加一些初始的Bean遍历调用所有ApplicationContextInitializer的initialize()方法让自定义初始化器有机会对ApplicationContext做进一步定制。触发所有SpringApplicationRunListener的contextPrepared()事件通知监听器上下文已准备就绪。往BeanFactory中注册一些特殊的单例Bean如 springApplicationArguments、springBootBanner。通过BeanDefinitionLoader加载主类将其注册为BeanDefinition。触发所有SpringApplicationRunListener的contextLoaded()事件通知监听器上下文已加载完成。2.2.8 refreshContext()—刷新上下文在应用程序上下文准备好后可以通过刷新应用程序上下文发现Bean并加载到容器中。refreshContext()会调用ApplicationContext.refresh()方法。AbstractApplicationContext中定义了refresh()方法的基本框架(模板模式的应用)。在刷新上下文阶段中Spring Boot会执行各种启动任务包括创建Web服务器、加载应用程序的配置、初始化各种组件等。这个阶段的核心源码是Spring Boot的刷新机制它会调用各种初始化器和监听器执行各种启动任务。protectedvoidrefreshContext(ConfigurableApplicationContextapplicationContext){refresh(applicationContext);if(this.registerShutdownHook){try{applicationContext.registerShutdownHook();}catch(AccessControlExceptionex){// Not allowed in some environments.}}}在refreshContext()方法中调用refresh(applicationContext)方法刷新上下文。这个方法是ApplicationContext接口的核心方法会启动上下文执行各种启动任务。调用registerShutdownHook()方法注册应用程序的关闭钩子。这个方法会在应用程序关闭时自动执行清理资源、关闭线程等,所以我们利用此特性在服务关闭的时候清理一些资源。并向外部发送告警通知。在refresh(applicationContext)方法中Spring Boot会执行上下文的各种启动任务包括创建 Web 服务器、加载应用程序的配置、初始化各种组件等。具体的启动任务会调用各种初始化器和监听器例如for(ApplicationContextInitializer?initializer:getInitializers()){initializer.initialize(applicationContext);}另外Spring Boot 还会调用各种监听器我们不做赘述例如for(ApplicationListener?listener:getApplicationListeners()){if(listenerinstanceofSmartApplicationListener){SmartApplicationListenersmartListener(SmartApplicationListener)listener;if(smartListener.supportsEventType(eventType)smartListener.supportsSourceType(sourceType)){invokeListener(smartListener,event);}}elseif(supportsEvent(listener,eventType)){invokeListener(listener,event);}}2.2.8.1 prepareRefresh()—准备刷新准备刷新的阶段做了初始化和校验的工作。比如初始化启动时间初始化PropertySources(在AbstractApplicationContext中只是一个空方法留给子类根据需要实现)以及校验环境中是否已经有必要的参数。2.2.8.2 prepareBeanFactory()—准备BeanFactoryBeanFactory是Spring框架中容器的底层实现所有的Bean都存放在BeanFactory中虽然ApplicationContext也实现了BeanFactory接口但是在其内部还是将获取Bean的相关操作委托给内部的DefaultListableBeanFactory变量只是ApplicationContext帮用户屏蔽了底层的操作同时提供出一些更符合外部用户使用的接口。对BeanFactory的准备主要是添加一些必要组件比如类加载器表达式解析器属性编辑器注册表等。以及一些后置处理器比如ApplicationContextAwareProcessor(xxxAware的接口就是通过后置处理器在Bean创建的时候通过后置处理器设置的)。此外还有一些特殊的Beanenvironment,systemProperties和systemEnvirnoment。点击查看Spring核心之bean生命周期和三级缓存2.2.8.3 postProcessBeanFactory()—后置处理BeanFactory对于非WebServlet环境的ApplicationContext而言这个方法是个空方法但是Web环境下的ApplicationContext会通过这个方法定制一些后处理动作比如添加WebApplicationContextServletAwareProcessor后置处理器添加在web环境中可能使用的Scopesession和request。2.2.8.4 invokeBeanFactoryPostProcessors()—实例化并调用BeanFactoryPostProcessorBeanFactoryPostProcessor是一种特殊的后置处理器其操作的对象是针对BeanFactory。此时主要有三个后置处理器分别是:SharedMetadataReaderFactoryContextInitializer$CachingMetadataReaderFactoryPostProcessor,ConfigurationWarningsApplicationContextInitializer$ConfigurationWarningsPostProcessor和ConfigFileApplicationListener$PropertySourceOrderingPostProcessor。这三个类名字虽然很长但是其实是因为内部类的关系而且我们看名字也能发现类是怎么来的(外部类是xxxInitializer的就说明是初始化器设置的)。其中第一个类和启动流程有关因为它会向容器注册ConfigurationClassPostProcessor。如果BeanFactoryPostProcessor同时又是BeanDefinitionRegistryPostProcessor则先进行针对BeanDefinition注册表的后置处理目的是为了发现Bean。在最初的三个BeanFactoryProcessor后置处理完成后会从容器中获取BeanDefinitionRegistryPostProcessor类型的后置处理器这里主要会得到刚才加载的ConfigurationClassPostProcessor实例。再调用这些BeanDefinitionRegistry的后置处理器继续向发现并向容器中注册新的Bean。这里主要是通过Configuration注解作为入口发现Bean如果发现的Bean中又存在新的ConfigurationBean则以此Bean为入口再进行发现直到所有的Bean都被发现。在针对BeanDefinition注册表的后置处理完成发现Bean的过程中如果找到了BeanFactoryPostProcessor(包括最初的三个BeanFatoryProcessor)会进行针对BeanFactory的后置处理过程之前只是进行针对注册表的后置处理二者的目的还是有区别的。注意Bean的发现过程只是向BeanDefinition注册表注册BeanDefinition的过程并没有针对发现的Bean进行实例化少部分需要用到的Bean会进行实例化比如这部分会对BeanDefinitionRegistryPostProcessor类型的Bean实例化。2.2.8.5 registerBeanPostProcessors()—注册Bean后置处理器从BeanFactory中找出所有BeanPostProcessor类型的BeanDefinition实例化并排序后注册到容器的beanPostProcessors列表中。这些处理器将在后续Bean实例化阶段执行postProcessBeforeInitialization()初始化前调用如 PostConstructpostProcessAfterInitialization()初始化后调用如 AOP 代理生成常见BeanPostProcessorAutowiredAnnotationBeanPostProcessor处理 Autowired、ValueCommonAnnotationBeanPostProcessor处理 PostConstruct、PreDestroyAsyncAnnotationBeanPostProcessor处理 AsyncAnnotationAwareAspectJAutoProxyCreatorAOP 代理创建2.2.8.6 initMessageSource()—初始化MessageSourceMessageSource是拥有特殊功能的Bean用来处理国际化相关内容。2.2.8.7 initApplicationEventMulticaster()—初始化ApplicationEventMulticasterApplicationEventMulticaster是ApplicationEvent广播器可以通过这个对象向容器中添加移除Listener也可以通过这个对象发布事件观察者模式的应用。2.2.8.8 onRefresh()—刷新应用程序发现了所有的Bean并且需要实例化的Bean也都被创建好了之后Spring接下去要做的是创建ThemeSource和主题相关的组件以及创建Webserver(如果是Web环境的话)。2.2.8.9 registerListeners()—注册监听器这一步会将初始化得到的ApplicationListener方法和容器中获得ApplicationListener一起注册到ApplicationEventMulticaster中并且如果存在需要早起发布的事件则发布事件。2.2.8.10 finishBeanFactoryInitialzation()—初始化容器中的Bean经过之前的步骤现在容器中必要的组件都已经准备好了并且所有需要容器管理的Bean也都已经被发现注册成BeanDefinition注册表中。对于Scope是Singleton的Bean而言此时已经具备了实例化Bean的条件因此在这一步中Spring会对所有Singleton且非lazy-init的Bean进行实例化。主要做法就是获取容器中所有为singletion且非lazyInit的BeanDefinition然后通过getBean创建出Bean的实例保存在容器内部。有一种特殊的情况是针对FactoryBeanFactoryBean是一种用来创建Bean的特殊Bean在得到FactoryBean的Bean之后还需要判断是否要创建FactoryBean负责创建的Bean2.2.8.11 finishRefresh()—完成刷新在这步主要是一些资源清理以及注册LifeCycleProcessor。LifeCycleProcessor可以用来在Spring生命周期的refresh和close时触发回调。并且发布Refresh的消息。2.2.9 afterRefresh()—留给子类的钩子函数在Application完成刷新后SpringApplication给子类留了afterRefresh()的方法作为回调。2.3 启动完成启动完成后stopWatch会记录下本次启动消费的时间。然后向ApplicationRunListener发布started事件,说明已经启动就绪。2.4 准备运行启动完成后正式运行前SpringApplication还会执行用户定义的ApplicationRunner和CommandLineRunner两个接口中定义的run()方法。在执行完成后向ApplicationRunListener发布runing的消息。至此启动流程结束。CommandLineRunner和ApplicationRunner区别如下方法签名不同CommandLineRunner接口有一个run(String... args)方法它接收命令行参数作为可变长度字符串数组。ApplicationRunner接口则提供了一个run(ApplicationArguments args)方法它接收一个ApplicationArguments对象作为参数这个对象提供了对传入的所有命令行参数包括选项和非选项参数的访问。参数解析方式不同CommandLineRunner接口更简单直接适合处理简单的命令行参数。ApplicationRunner接口提供了一种更强大的参数解析能力可以通过ApplicationArguments获取详细的参数信息比如获取选项参数及其值、非选项参数列表以及查询是否存在特定参数等。使用场景不同当只需要处理一组简单的命令行参数时可以使用CommandLineRunner。对于需要精细控制和解析命令行参数的复杂场景推荐使用ApplicationRunner3 总结本文旨在对SpringBoot启动流程各个步骤做一次梳理本文的段落标题就是启动的各个步骤不同等级的标题也含有方法前后调用的关系并没有对每行代码做深入分析另外在贴一份整理的不错的流程图帮助大家加深印象。转载于 : https://www.cnblogs.com/insaneXs/p/12721306.html