前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >你的springboot starter是如何生效的?

你的springboot starter是如何生效的?

作者头像
叔牙
发布2023-08-09 15:07:27
3290
发布2023-08-09 15:07:27
举报

内容目录

一、背景描述二、自定义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集成了一组相关的依赖项,开发人员无需手动添加和管理这些依赖项。
  • 避免版本冲突:spring-boot-starter中的依赖项已经过测试和验证,它们之间的版本兼容性已经得到保证,避免了版本冲突的问题。
  • 自动配置:spring-boot-starter提供了默认的配置,自动装配和一些合理的默认值,使得开发人员可以快速启动并运行应用程序,无需手动编写大量的配置代码。
  • 提高生产力:借助spring-boot-starter,开发人员可以更专注于业务逻辑的开发,而不用花费太多精力在依赖管理、配置和初始化方面。

总的来说,spring-boot-starter一套完整的功能集,简化了Springboot项目的构建和配置过程,提供了一种方便的方式来引入特定功能或技术所需的依赖项,并提供了默认的配置和自动装配,帮助开发人员更高效地开发和交付应用程序。

当然我们也可以自己实现starter,在一些通用或者定制化场景,我们可以通过自定义starter方式封装依赖和工具,然后在业务项目中引入,通过简单配置就能使用相关能力,旨在提供某种场景通用的解决方案,最大限度降低功能和工具接入成本,提高开发效率。

二、自定义starter实现方式

从官方文档中我们了解到,实现自定义starter需要包含autoconfigure和starter模块,autoconfigure包含所有自动配置代码,starter提供所有对autoconfigure的依赖管理,通常情况下引用starter模块就能开启相关能力。如果不需要将自动配置代码和依赖关系管理分离,则可以将它们组合在一个模块中。大部分场景我们编写自定义starter,使用一个模块即可。编写自定义starter步骤如下:

1.引入spring-boot-autoconfigure依赖

编写自定义starter需要引入spring-boot-autoconfigure依赖开启相关配置能力。

代码语言:javascript
复制
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
</dependency>
2.编写自动配置

在自定义的Springboot starter中,spring.factories文件的作用是将自定义的自动配置类注册到Springboot应用程序中。这个文件是一个标准的Java属性文件,它的内容包含了要注册的自动配置类的全限定类名。

spring.factories文件位于META-INF目录下,它是Springboot自动装配机制的关键之一。当Spring Boot应用程序启动时,Spring框架会扫描所有的spring.factories文件,然后根据其中配置的自动配置类,自动应用相应的配置。

在自定义的starter中,通过在spring.factories文件中添加以下内容,可以将自定义的自动配置类注册到Spring Boot应用程序中:

代码语言:javascript
复制
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.example.YourAutoConfiguration

其中,EnableAutoConfiguration是Springboot提供的注解,用于开启自动配置功能。com.example.YourAutoConfiguration是你自定义的自动配置类的全限定类名。多个用逗号隔开。

通过这样的配置,Springboot应用程序在启动时会自动识别并应用YourAutoConfiguration类中的配置。这些配置可能包括自定义Bean的创建、属性设置、条件判断等操作,以满足特定场景下的自动化配置需求。

3.编写spring-configuration-metadata.json

该项非必须。通过spring-configuration-metadata.json文件,Spring Boot可以根据其中的配置元数据提供更好的IDE支持、自动补全和验证功能。开发人员可以更方便地了解每个配置属性的含义、类型和默认值,从而减少配置错误和提高开发效率。

配置方式如下:

代码语言:javascript
复制
{
  "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以及其相关能力了。

三、自定义starter依赖加载原理

对于springboot项目中所有依赖的二方或者三方jar,在使用之前必须要先加载成类描述文件,而springboot使用到的自定义starter也是以jar的形式体现在项目的依赖集中。当然springboot加载jar文件是一个通用能力,不止针对自定义starter,我们也是本着弄清本质的原则,将自定义starter依赖加载也分析一下。

1.可运行jar的目录结构

SrpingBoot运行的时候是直接运行的一个jar文件,那么java -jar做了什么事情呢,我们先从项目目录结构分析一下。通过unzip命令解压jar文件,基本可以得到大致如下的目录结构:

代码语言:javascript
复制
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:

代码语言:javascript
复制
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
2.加载starter

自定义starter的jar也会放到springboot的/BOOT-INF/lib目录下,在执行java -jar时就会调用JarLauncher中执行main方法,所有应用程序类文件均可通过/BOOT-INF/classes加载,所有依赖的第三方jar均可通过/BOOT-INF/lib加载。

代码语言:javascript
复制
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方法来获取:

代码语言:javascript
复制
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包中的类都被加载后,就可以执行后续的启动流程了。

四、spring.factories加载与解析

从之前的文章《springboot自动装配原理》中可以看出,

AbstractApplictionContext的refresh方法在执行到ConfigurationClassParser的parse会加载并解析spring.factories。加载和解析spring.factories流程如下:

解析之后会注册成BeanDefination等后续实例化备用,具体触发点是在ConfigurationClassParser的parse方法之后:

代码语言:javascript
复制
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方法,然后调用另外一个内部方法:

代码语言:javascript
复制
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中:

代码语言:javascript
复制
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/

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2023-07-08,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景描述
  • 二、自定义starter实现方式
    • 1.引入spring-boot-autoconfigure依赖
      • 2.编写自动配置
        • 3.编写spring-configuration-metadata.json
        • 三、自定义starter依赖加载原理
          • 1.可运行jar的目录结构
            • 2.加载starter
            • 四、spring.factories加载与解析
            • 五、参考
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档