内容目录
一、背景描述二、自定义starter实现方式三、自定义starter依赖加载原理四、spring.factories加载与解析五、参考
spring-boot-starter是SpringBoot框架中的一个比较重要的概念。它是一个可重用的、自包含的、可引入项目的Maven或Gradle依赖。spring-boot-starter为开发人员提供了一种方便的方式来引入和配置特定功能或技术栈所需的所有依赖项。它旨在简化Springboot应用程序的构建和配置过程。
通过使用spring-boot-starter,可以避免手动添加各种依赖项,并确保这些依赖项之间的版本兼容性。它将相关的依赖项打包在一起,并提供了一套默认的配置,以便开箱即用地启动和运行应用程序。
每个spring-boot-starter都专注于某个具体的功能或技术领域,例如spring-boot-starter-web用于构建Web应用程序,spring-boot-starter-data-redis用于访问redis等。这些starter在实现特定功能的同时,也处理了配置,自动装配和其他相关的扩展点。
使用spring-boot-starter可以带来以下好处:
总的来说,spring-boot-starter一套完整的功能集,简化了Springboot项目的构建和配置过程,提供了一种方便的方式来引入特定功能或技术所需的依赖项,并提供了默认的配置和自动装配,帮助开发人员更高效地开发和交付应用程序。
当然我们也可以自己实现starter,在一些通用或者定制化场景,我们可以通过自定义starter方式封装依赖和工具,然后在业务项目中引入,通过简单配置就能使用相关能力,旨在提供某种场景通用的解决方案,最大限度降低功能和工具接入成本,提高开发效率。
从官方文档中我们了解到,实现自定义starter需要包含autoconfigure和starter模块,autoconfigure包含所有自动配置代码,starter提供所有对autoconfigure的依赖管理,通常情况下引用starter模块就能开启相关能力。如果不需要将自动配置代码和依赖关系管理分离,则可以将它们组合在一个模块中。大部分场景我们编写自定义starter,使用一个模块即可。编写自定义starter步骤如下:
编写自定义starter需要引入spring-boot-autoconfigure依赖开启相关配置能力。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
在自定义的Springboot starter中,spring.factories文件的作用是将自定义的自动配置类注册到Springboot应用程序中。这个文件是一个标准的Java属性文件,它的内容包含了要注册的自动配置类的全限定类名。
spring.factories文件位于META-INF目录下,它是Springboot自动装配机制的关键之一。当Spring Boot应用程序启动时,Spring框架会扫描所有的spring.factories文件,然后根据其中配置的自动配置类,自动应用相应的配置。
在自定义的starter中,通过在spring.factories文件中添加以下内容,可以将自定义的自动配置类注册到Spring Boot应用程序中:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.YourAutoConfiguration
其中,EnableAutoConfiguration是Springboot提供的注解,用于开启自动配置功能。com.example.YourAutoConfiguration是你自定义的自动配置类的全限定类名。多个用逗号隔开。
通过这样的配置,Springboot应用程序在启动时会自动识别并应用YourAutoConfiguration类中的配置。这些配置可能包括自定义Bean的创建、属性设置、条件判断等操作,以满足特定场景下的自动化配置需求。
该项非必须。通过spring-configuration-metadata.json文件,Spring Boot可以根据其中的配置元数据提供更好的IDE支持、自动补全和验证功能。开发人员可以更方便地了解每个配置属性的含义、类型和默认值,从而减少配置错误和提高开发效率。
配置方式如下:
{
"groups": [
{
"name": "group1",
"type": "org.example.Group1",
"sourceType": "org.example.Group1Properties",
"description": "Group 1 configuration properties"
}
],
"properties": [
{
"name": "property1",
"type": "java.lang.String",
"description": "Description of property 1",
"defaultValue": "default value",
"deprecated": false,
"sourceType": "org.example.Group1Properties"
}
]
}
然后使用maven命令打包到仓库,springboot应用就可以使用自定义starter以及其相关能力了。
对于springboot项目中所有依赖的二方或者三方jar,在使用之前必须要先加载成类描述文件,而springboot使用到的自定义starter也是以jar的形式体现在项目的依赖集中。当然springboot加载jar文件是一个通用能力,不止针对自定义starter,我们也是本着弄清本质的原则,将自定义starter依赖加载也分析一下。
SrpingBoot运行的时候是直接运行的一个jar文件,那么java -jar做了什么事情呢,我们先从项目目录结构分析一下。通过unzip命令解压jar文件,基本可以得到大致如下的目录结构:
spring-boot-demo
├── META-INF
│ └── MANIFEST.MF
├── BOOT-INF
│ ├── classes
│ │ └── 应用程序类
│ └── lib
│ └── 三方依赖jar
└── org
└── springframework
└── boot
└── loader
└── springboot启动所需的class
在Springboot项目中会将所有所需的jar都打包放在BOOT-INF下的lib下面,对于java是无法加载jar中的jar,所以SpringBoot实现了自定义类加载器,这个类加载器通过org.springframework.boot.loader.JarLauncher创建了LaunchedURLClassLoader用以加载SpringBoot中的jar,在MANIFEST.MF中的Main-Class中有指定JarLauncher:
Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Archiver-Version: Plexus Archiver
Built-By: typhoon
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: xxx.ApplicationStarter
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Version: 2.5.6
Created-By: Apache Maven 3.8.6
Build-Jdk: 1.8.0_202
Main-Class: org.springframework.boot.loader.JarLauncher
自定义starter的jar也会放到springboot的/BOOT-INF/lib目录下,在执行java -jar时就会调用JarLauncher中执行main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。
public class JarLauncher extends ExecutableArchiveLauncher {
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
public JarLauncher() {
}
protected JarLauncher(Archive archive) {
super(archive);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
public static void main(String[] args) throws Exception {
new JarLauncher().launch(args);
}
protected void launch(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
ClassLoader classLoader = createClassLoader(getClassPathArchives());
launch(args, getMainClass(), classLoader);
}
protected ClassLoader createClassLoader(List<Archive> archives) throws Exception {
List<URL> urls = new ArrayList<>(archives.size());
for (Archive archive : archives) {
urls.add(archive.getUrl());
}
return createClassLoader(urls.toArray(new URL[0]));
}
protected ClassLoader createClassLoader(URL[] urls) throws Exception {
return new LaunchedURLClassLoader(urls, getClass().getClassLoader());
}
}
然后需要知道加载哪些类,通过调用getClassPathArchives方法来获取:
static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";
static final String BOOT_INF_LIB = "BOOT-INF/lib/";
@Override
protected List<Archive> getClassPathArchives() throws Exception {
List<Archive> archives = new ArrayList<>( this.archive.getNestedArchives(this::isNestedArchive));
postProcessClassPathArchives(archives); return archives;
}
@Override
public List<Archive> getNestedArchives(EntryFilter filter) throws IOException {
List<Archive> nestedArchives = new ArrayList<>();
for (Entry entry : this) {
if (filter.matches(entry)) {
nestedArchives.add(getNestedArchive(entry));
}
}
return Collections.unmodifiableList(nestedArchives);
}
@Override
protected boolean isNestedArchive(Archive.Entry entry) {
if (entry.isDirectory()) {
return entry.getName().equals(BOOT_INF_CLASSES);
}
return entry.getName().startsWith(BOOT_INF_LIB);
}
从方法实现可以看出来它是加载的BOOT-INF/classes/和BOOT-INF/lib/,然后把获取到的archives List传到LaunchedURLClassLoader去加载。
在springboot应用本身的类和依赖的jar包中的类都被加载后,就可以执行后续的启动流程了。
从之前的文章《springboot自动装配原理》中可以看出,
AbstractApplictionContext的refresh方法在执行到ConfigurationClassParser的parse会加载并解析spring.factories。加载和解析spring.factories流程如下:
解析之后会注册成BeanDefination等后续实例化备用,具体触发点是在ConfigurationClassParser的parse方法之后:
StartupStep processConfig = this.applicationStartup.start("spring.context.config-classes.parse");
parser.parse(candidates);
parser.validate();
Set<ConfigurationClass> configClasses = new LinkedHashSet<>(parser.getConfigurationClasses());
configClasses.removeAll(alreadyParsed);
// Read the model and create bean definitions based on its content
if (this.reader == null) {
this.reader = new ConfigurationClassBeanDefinitionReader(
registry, this.sourceExtractor, this.resourceLoader, this.environment,
this.importBeanNameGenerator, parser.getImportRegistry());
}
this.reader.loadBeanDefinitions(configClasses);
调用ConfigurationClassBeanDefinitionReader的loadBeanDefinitions方法,然后调用另外一个内部方法:
private void loadBeanDefinitionsForConfigurationClass(
ConfigurationClass configClass, TrackedConditionEvaluator trackedConditionEvaluator) {
if (trackedConditionEvaluator.shouldSkip(configClass)) {
String beanName = configClass.getBeanName();
if (StringUtils.hasLength(beanName) && this.registry.containsBeanDefinition(beanName)) {
this.registry.removeBeanDefinition(beanName);
}
this.importRegistry.removeImportingClass(configClass.getMetadata().getClassName());
return;
}
if (configClass.isImported()) {
registerBeanDefinitionForImportedConfigurationClass(configClass);
}
//省略...
}
然后调用registerBeanDefinition方法注册BeanDefination到BeanDefinitionRegistry中:
private void registerBeanDefinitionForImportedConfigurationClass(ConfigurationClass configClass) {
AnnotationMetadata metadata = configClass.getMetadata();
AnnotatedGenericBeanDefinition configBeanDef = new AnnotatedGenericBeanDefinition(metadata);
ScopeMetadata scopeMetadata = scopeMetadataResolver.resolveScopeMetadata(configBeanDef);
configBeanDef.setScope(scopeMetadata.getScopeName());
String configBeanName = this.importBeanNameGenerator.generateBeanName(configBeanDef, this.registry);
AnnotationConfigUtils.processCommonDefinitionAnnotations(configBeanDef, metadata);
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(configBeanDef, configBeanName);
definitionHolder = AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
this.registry.registerBeanDefinition(definitionHolder.getBeanName(), definitionHolder.getBeanDefinition());
configClass.setBeanName(configBeanName);
}
对于Springboot的自动配置类,它们的实例化过程与其他普通的bean一样。在Spring应用程序上下文初始化的过程中,当调用AbstractApplicationContext的refresh()方法时,会触发BeanFactory的实例化和初始化。refresh会调用finishBeanFactoryInitialization()方法,Spring在BeanFactory中获取所有的BeanDefinition,并根据定义创建相应的bean实例。这个过程会包括对自动配置类的实例化。
https://docs.spring.io/spring-boot/docs/1.5.11.RELEASE/reference/html/boot-features-developing-auto-configuration.html#boot-features-custom-starter
https://blog.csdn.net/qq_41737716/article/details/98028976
https://dmsupine.com/2021/04/27/springboot-qi-dong-yuan-li/
本文分享自 PersistentCoder 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!