springboot启动类原理?

Spring boot Batch 的启动原理- Configuration

Spring boot 整合了web 和batch ,但是他们肯定不是同一条路, 在spring boot 中,会推断当前的运行环境。 this.webApplicationType = WebApplicationType.deduceFromClasspath();

从上文可以看出,Spring 尝试从classpath 里找到特征类,来判断当前app 是什么类型。当然这种判断是有局限性的,有可能是transitive 带进来一个带有servlet 的类被当成了 WebApplicationType.SERVLET, 实际上是个WebApplicationType.NONE;。如果不想以web 运行就是想运行batch 可以在application.properties 强行指定WebApplicationType

具体发生作用的请看下面的stack trace

当一个batch application需要启动,需要 配置JobRepository, Datasource 等等,所有的开始都来自一个annotation @EnableBatchProcessing

当加入@EnableBatchProcessing 时, BatchConfigurationSelector 开始启动,怎么启动的大家可以参考下面的stack trace。

import 类主要是由ConfigurationClassPostProcessor 来实现的。 当BatchConfigurationSelector 被调用的时候,我们可以看到他有两条支路。

那么这两条路有啥不同呢。 主要是job 定义的方式不同。

modular = true 的情况下,下面是一个例子

可以有多个子ApplicationContextFactory ,这样好处是在除了job 大家不可以重复,因为是在不同的context 里,其他的step,reader, writer,processor,mapper ,以及所有的bean等等都可以重名。

那为什么Job 不可以重复,是因为虽然可以重复,但是如果job 也重复,对用户来讲太不友好了。用户可能不知道自己配的是哪个context 的job。具体为什么可以重名要看看GenericApplicationContextFactory 的实现。

当GenericApplicationContextFactory::createApplicationContext, 会触发 ApplicationContextHelper 的构造函数 从而调用 loadConfiguration(config) 把定义的bean 加入到context 里。那么有个问题,parent 在哪里设置,createApplicationContext 是什么时候调用的。

我们继续看ModularConfiguration

从Modular 的角度来看 首先他可以外部注入一个Configurer,如果没有就选择DefaultBatchConfigurer, 如果有多个选择则会抛出。

当然Datasource也可以选择外部注入,或者由 DefaultBatchConfigurer::initialize 方法 SimpleJobLauncher , JobRepository 和 JobExplorer 都是 由FactoryBean 的方式实现的。

在这个DefaultBatchConfigurer 中可以看到JobLauncher 的类型是

initialize 里MapJobRepositoryFactoryBean 这个可以重点读一下。 首先用FactoryBean的模式实现了一个ProxyBean,如果想了解FactoryBean 的用法,这是个典型的例子。但是这个FactoryBean 是以api 行为直接调用的,并没有注册到Spring 的context 中。

配置好job 需要的 jobRepository ,jobLauncher等 那么重点来了,下面的AutomaticJobRegistrar 就是来处理ApplicationContextFactory

这个逻辑是把所有的ApplicationContextFactory 的bean instance 放入到AutomaticJobRegistrar 里去。 这就回到了第一个问题,parent context 是什么时候放进去的。就是在

context.getBeansOfType(ApplicationContextFactory.class) 请看下面的调用栈,在ApplicationContextAwareProcessor 里会自动把parent context 注入。

但是放进去啥时候用呢?我们看一下AutomaticJobRegistrar

原来AutomaticJobRegistrar 是个Smartlifecycle, 从Smartlifecycle的细节可以从 Springboot Smartlifecycle 来得知。它就是在所有bean都初始化结束后开始进行的一个阶段。在这个start 方法中,开始遍历所有的ApplicationContextFactory, 来进行加载。 从上文这个jobLoader 是DefaultJobLoader。

那么可以看看DefaultJobLoader::doLoad 方法

这里有几个关键调用 第一个是createApplicationContext, 把context 里定义的全部加载到Spring context里去,这就满足了GenericApplicationContextFactory 工作的两个条件。第二个是doRegister(context, job) 里 jobRegistry.register(jobFactory);

我们看一下JobRepository的实现MapJobRegistry::register 方法,在这里就把jobname 和jobFactory 的键值对存储起来了。

这样就可以通过JobRepository 这个bean 拿到所有注册的job 了。

咱们再回来看@EnableBatchProcessing 这个annotation,当没有设定modular 的时候是比较简单的,只是实现了一个proxy based 的Job的bean。

同样也只有BatchConfigurer 来配置。 这个逻辑和Modular 是一样的。从这两个配置知道, Modular 注册了在子context的配置,并且加载。 但是当以正常bean 的方式存在的,是怎么读进来的呢。这时候就要看 JobRegistryBeanPostProcessor

这个 postProcessAfterInitialization 方法里, 对每个job 类型的bean, jobRegistry 加入了ReferenceJobFactory。 这样所有的以bean 的方式定义的都可以通过jobRegistry获得。

SpringBoot Stater原理

一.SpringBoot的好处

    1.依赖管理:可插拔式的组件管理,当需要某个组件时,只需要引入相关stater即可,不需要再手动引入各个jar包,避免了包遗漏、包冲突等不必要的问题。开发人员可以专注于业务开发,

    2.自动配置:遵从"约定优于配置"的原则,开发人员可以在少量配置或者不配置的情况下,使用某组件。

    大大降低项目搭建及组件引入的成本,开发人员可以专注于业务开发,避免繁杂的配置和大量的jar包管理。

二.实现原理

    要引入某组件,无非要做两件事。一是引入jar包即pom文件引入stater;二就是编写配置文件,使用Java配置的情况下就是编写一系列@Configuration注解标注的类。那么SpringBoot是怎么来引入这些配置类的呢?就需要我们深入SpringBoot启动类一探究竟。

    SpringBoot启动类上面会有@SpringBootApplication注解,这是SpringBoot中最重要的一个注解,是实现自动配置的关键。@SpringBootApplication是一个租合注解,主要由@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan三部分组成。

    @SpringBootConfiguration表明该类是一个配置类。

    @EnableAutoConfiguration由@AutoConfigurationPackage和@Import(AutoConfigurationImportSelector.class)组成。@AutoConfigurationPackage由@Import(AutoconfigurationPackages.Registrar.class)组成,向Bean容器中注册一个AutoConfigurationPackages类,该类持有basePackage,目前我发现的作用是在MyBatis扫描注册Mapper时作为包扫描路径。

    @AutoConfigurationPackage的执行流程如下图:

    @Import(AutoConfigurationImportSelector.class)是启动自动配置的核心。这里还有一个小插曲,一直在看AutoConfigurationImportSelector的selectImports方法,却发现没有被调用,以为是demo项目问题,去真实项目中debug断点,发现也没有进入,瞬间懵逼。。。继续跟踪发现执行流程如下:

springboot启动类原理?  第1张

SpringBoot启动原理分析

自动配置核心类SpringFactoriesLoader

上面在说@EnableAutoConfiguration的时候有说META-INF下的spring.factories文件,那么这个文件是怎么被spring加载到的呢,其实就是SpringFactoriesLoader类。

SpringFactoriesLoader是一个供Spring内部使用的通用工厂装载器,SpringFactoriesLoader里有两个方法,

在这个SpringBoot应用启动过程中,SpringFactoriesLoader做了以下几件事:

加载所有META-INF/spring.factories中的Initializer

加载所有META-INF/spring.factories中的Listener

加载EnvironmentPostProcessor(允许在Spring应用构建之前定制环境配置)

接下来加载Properties和YAML的PropertySourceLoader(针对SpringBoot的两种配置文件的加载器)

各种异常情况的FailureAnalyzer(异常解释器)

加载SpringBoot内部实现的各种AutoConfiguration

模板引擎TemplateAvailabilityProvider(如Freemarker、Thymeleaf、Jsp、Velocity等)

总得来说,SpringFactoriesLoader和@EnableAutoConfiguration配合起来,整体功能就是查找spring.factories文件,加载自动配置类。

整体启动流程

在我们执行入口类的main方法之后,运行SpringApplication.run,后面new了一个SpringApplication对象,然后执行它的run方法。

初始化SpringApplication类

创建一个SpringApplication对象时,会调用它自己的initialize方法

执行核心run方法

初始化initialize方法执行完之后,会调用run方法,开始启动SpringBoot。

首先遍历执行所有通过SpringFactoriesLoader,在当前classpath下的META-INF/spring.factories中查找所有可用的SpringApplicationRunListeners并实例化。调用它们的starting()方法,通知这些监听器SpringBoot应用启动。

创建并配置当前SpringBoot应用将要使用的Environment,包括当前有效的PropertySource以及Profile。

遍历调用所有的SpringApplicationRunListeners的environmentPrepared()的方法,通知这些监听器SpringBoot应用的Environment已经完成初始化。

打印SpringBoot应用的banner,SpringApplication的showBanner属性为true时,如果classpath下存在banner.txt文件,则打印其内容,否则打印默认banner。

根据启动时设置的applicationContextClass和在initialize方法设置的webEnvironment,创建对应的applicationContext。

创建异常解析器,用在启动中发生异常的时候进行异常处理(包括记录日志、释放资源等)。

设置SpringBoot的Environment,注册Spring Bean名称的序列化器BeanNameGenerator,并设置资源加载器ResourceLoader,通过SpringFactoriesLoader加载ApplicationContextInitializer初始化器,调用initialize方法,对创建的ApplicationContext进一步初始化。

调用所有的SpringApplicationRunListeners的contextPrepared方法,通知这些Listener当前ApplicationContext已经创建完毕。

最核心的一步,将之前通过@EnableAutoConfiguration获取的所有配置以及其他形式的IoC容器配置加载到已经准备完毕的ApplicationContext。

调用所有的SpringApplicationRunListener的contextLoaded方法,加载准备完毕的ApplicationContext。

调用refreshContext,注册一个关闭Spring容器的钩子ShutdownHook,当程序在停止的时候释放资源(包括:销毁Bean,关闭SpringBean的创建工厂等)

注: 钩子可以在以下几种场景中被调用:

1)程序正常退出

2)使用System.exit()

3)终端使用Ctrl+C触发的中断

4)系统关闭

5)使用Kill pid命令杀死进程

获取当前所有ApplicationRunner和CommandLineRunner接口的实现类,执行其run方法

遍历所有的SpringApplicationRunListener的finished()方法,完成SpringBoot的启动。

SpringBoot应用启动原理(二) 扩展URLClassLoader实现嵌套jar加载

在上篇文章《SpringBoot应用启动原理(一) 将启动脚本嵌入jar》中介绍了SpringBoot如何将启动脚本与Runnable Jar整合为Executable Jar的原理,使得生成的jar/war文件可以直接启动

本篇将介绍SpringBoot如何扩展URLClassLoader实现嵌套jar的类(资源)加载,以启动我们的应用。

首先,从一个简单的示例开始

build.gradle

WebApp.java

执行 gradle build 构建jar包,里面包含 应用程序 、 第三方依赖 以及SpringBoot 启动程序 ,其目录结构如下

查看MANIFEST.MF的内容(MANIFEST.MF文件的作用请自行GOOGLE)

可以看到,jar的启动类为 org.springframework.boot.loader.JarLauncher ,而并不是我们的 com.manerfan.SpringBoot.theory.WebApp ,应用程序入口类被标记为了Start-Class

jar启动并不是通过应用程序入口类,而是通过JarLauncher代理启动。其实SpringBoot拥有3中不同的Launcher: JarLauncher 、 WarLauncher 、 PropertiesLauncher

SpringBoot使用Launcher代理启动,其最重要的一点便是可以自定义ClassLoader,以实现对jar文件内(jar in jar)或其他路径下jar、class或资源文件的加载

关于ClassLoader的更多介绍可参考 《深入理解JVM之ClassLoader》

SpringBoot抽象了Archive的概念,一个Archive可以是jar(JarFileArchive),可以是一个文件目录(ExplodedArchive),可以抽象为统一访问资源的逻辑层。

上例中,spring-boot-theory-1.0.0.jar既为一个JarFileArchive,spring-boot-theory-1.0.0.jar!/BOOT-INF/lib下的每一个jar包也是一个JarFileArchive

将spring-boot-theory-1.0.0.jar解压到目录spring-boot-theory-1.0.0,则目录spring-boot-theory-1.0.0为一个ExplodedArchive

按照定义,JarLauncher可以加载内部 /BOOT-INF/lib 下的jar及 /BOOT-INF/classes 下的应用class

其实JarLauncher实现很简单

其主入口新建了JarLauncher并调用父类Launcher中的launch方法启动程序

再创建JarLauncher时,父类ExecutableArchiveLauncher找到自己所在的jar,并创建archive

在Launcher的launch方法中,通过以上archive的getNestedArchives方法找到/BOOT-INF/lib下所有jar及/BOOT-INF/classes目录所对应的archive,通过这些archives的url生成LaunchedURLClassLoader,并将其设置为线程上下文类加载器,启动应用

至此,才执行我们应用程序主入口类的main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载

在分析LaunchedURLClassLoader前,首先了解一下URLStreamHandler

java中定义了URL的概念,并实现多种URL协议(见 URL ) * http* * file* * ftp* * jar* 等,结合对应的URLConnection可以灵活地获取各种协议下的资源

对于jar,每个jar都会对应一个url,如

jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/

jar中的资源,也会对应一个url,并以'!/'分割,如

jar:file:/data/spring-boot-theory/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

对于原始的JarFile URL,只支持一个'!/',SpringBoot扩展了此协议,使其支持多个'!/',以实现jar in jar的资源,如

jar:file:/data/spring-boot-theory.jar!/BOOT-INF/lib/spring-aop-5.0.4.RELEASE.jar!/org/springframework/aop/SpringProxy.class

自定义URL的类格式为[pkgs].[protocol].Handler,在运行Launcher的launch方法时调用了 JarFile.registerUrlProtocolHandler() 以注册自定义的 Handler

在处理如下URL时,会循环处理'!/'分隔符,从最上层出发,先构造spring-boot-theory.jar的JarFile,再构造spring-aop-5.0.4.RELEASE.jar的JarFile,最后构造指向SpringProxy.class的

JarURLConnection ,通过JarURLConnection的getInputStream方法获取SpringProxy.class内容

从一个URL,到读取其中的内容,整个过程为

URLClassLoader可以通过原始的jar协议,加载jar中从class文件

LaunchedURLClassLoader 通过扩展的jar协议,以实现jar in jar这种情况下的class文件加载

构建war包很简单

构建出的war包,其目录机构为

MANIFEST.MF内容为

此时,启动类变为了 org.springframework.boot.loader.WarLauncher ,查看WarLauncher实现,其实与JarLauncher并无太大差别

差别仅在于,JarLauncher在构建LauncherURLClassLoader时,会搜索BOOT-INF/classes目录及BOOT-INF/lib目录下jar,WarLauncher在构建LauncherURLClassLoader时,则会搜索WEB-INFO/classes目录及WEB-INFO/lib和WEB-INFO/lib-provided两个目录下的jar

如此依赖,构建出的war便支持两种启动方式

PropretiesLauncher 的实现与 JarLauncher WarLauncher 的实现极为相似,通过PropretiesLauncher可以实现更为轻量的thin jar,其实现方式可自行查阅源码

以上内容为新媒号(sinv.com.cn)为大家提供!新媒号,坚持更新大家所需的互联网后端知识。希望您喜欢!

版权申明:新媒号所有作品(图文、音视频)均由用户自行上传分享,仅供网友学习交流,不声明或保证其内容的正确性,如发现本站有涉嫌抄袭侵权/违法违规的内容。请发送邮件至 k2#88.com(替换@) 举报,一经查实,本站将立刻删除。

(0)
上一篇 2023-09-23 13:37
下一篇 2023-09-23 13:37

相关推荐

发表回复

登录后才能评论