jjzjj

实现自定义SpringBoot的Starter组件

令狐前生 2024-04-02 原文

一、前言

想要自定义starter组件,首先要了解springboot是如何加载starter的,也就是springboot的自动装配机制原理。

1.1、starter加载原理

springboot通过一个@SpringBootApplication注解启动项目,springboot在项目启动的时候,会将项目中所有声明为Bean对象(注解、xml)的实例信息全部加载到ioc容器当中。 除此之外也会将所有依赖到的starter里的bean信息加载到ioc容器中,从而做到所谓的零配置,开箱即用。

1.1.1、加载starter

首先通过通过注解@SpringBootApplication找到@EnableAutoConfiguration注解进行加载starter。

再通过注解@EnableAutoConfiguration下注解@import找到AutoConfigurationImportSelector类加载器实现。

这个AutoConfigurationImportSelector类会去其引用的依赖jar包下,找到一个”spring.factories”文件,一般spring.factories文件里都会声明该依赖所提供的核心功能bean配置信息。文件一般在依赖jar包的META-INF文件夹下面。

以spring-boot版本2.7.7为例,加载spring.factories的代码在:

AutoConfigurationImportSelector.java->selectImports(AnnotationMetadata annotationMetadata)->getAutoConfigurationEntry(annotationMetadata)->getCandidateConfigurations(annotationMetadata, attributes)->SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())->loadSpringFactories(classLoaderToUse):

private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
        Map<String, List<String>> result = (Map)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            HashMap result = new HashMap();

            try {
                Enumeration urls = classLoader.getResources("META-INF/spring.factories");

                while(urls.hasMoreElements()) {
                    URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();

                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryTypeName = ((String)entry.getKey()).trim();
                        String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        String[] var10 = factoryImplementationNames;
                        int var11 = factoryImplementationNames.length;

                        for(int var12 = 0; var12 < var11; ++var12) {
                            String factoryImplementationName = var10[var12];
                            ((List)result.computeIfAbsent(factoryTypeName, (key) -> {
                                return new ArrayList();
                            })).add(factoryImplementationName.trim());
                        }
                    }
                }

                result.replaceAll((factoryType, implementations) -> {
                    return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
                });
                cache.put(classLoader, result);
                return result;
            } catch (IOException var14) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var14);
            }
        }
    }

举例如:spring-boot-autoconfig的spring.factories.

二、自定义starter

上面了解了springboot加载starter原理,其实就是加载依赖jar包下的spring.factories文件。所以我们要自定义starter,就需要在项目中建立一个META-INF的文件夹,然后在该文件夹下面建一个spring.factories文件,文件里将你需要提供出去的bean实例信息配置好就行。

2.1、代码

2.1.1、新建springboot项目。简单演示所以需求配置任务依赖。如springboot构建很慢,或者打包的时候下载依赖很慢,可在pom文件中添加如下仓库配置,可以加快构建速度。

 <repositories>
        <repository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </pluginRepository>
    </pluginRepositories>

注意:spring官方规定自定义组件的命名:

SpringBoot官方命名方式
格式:spring-boot-starter-{模块名}
举例:spring-boot-starter-web
自定义命名方式
格式:{模块名}-spring-boot-starter
举例:mystarter-spring-boot-starter

2.1.2、项目构建完成后,在resources文件夹下面新建META-INF文件夹,并新建spring.factories文件。

2.1.3、因为我们是作为插件来使用,所以我们不需要启动类,删除启动类。并新建几个类:

一个接口AnimalService:

package com.example.demospringbootstarter.service;

/**
 * @Project: demo-spring-boot-starter
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  15:12
 */
public interface AnimalService {

    String say();
}

两个接口实现类CatService和DogService:

package com.example.demospringbootstarter.service;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * @Project: demo-spring-boot-starter
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  14:49
 */
@Service
public class CatService implements AnimalService{

    public static String name = "cat";

    @Override
    public String say() {
        return "喵喵";
    }
}
package com.example.demospringbootstarter.service;

import org.springframework.stereotype.Service;

/**
 * @Project: demo-spring-boot-starter
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  14:49
 */
@Service
public class DogService implements AnimalService{

    public static String name = "dog";

    @Override
    public String say() {
        return "汪汪";
    }
}

再建一个配置AnimalProperties类,方便注入属性值:

package com.example.demospringbootstarter.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @Project: demo-spring-boot-starter
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  15:37
 */
@Data
@ConfigurationProperties(prefix = "animal")
public class AnimalProperties {

    private String name;
}

最后新建一个核心自动装备配置类:

package com.example.demospringbootstarter.config;

import com.example.demospringbootstarter.service.AnimalService;
import com.example.demospringbootstarter.service.CatService;
import com.example.demospringbootstarter.service.DogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @Project: demo-spring-boot-starter
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  14:48
 */
@Configuration
@EnableConfigurationProperties(AnimalProperties.class)
public class AnimalAutoConfig {

    @Autowired
    private AnimalProperties animalProperties;

    @Bean
    public AnimalService demoService(){
        switch (animalProperties.getName()){
            case "cat":
                return new CatService();
            case "dog":
                return new DogService();
            default:
                return null;
        }
    }
}

META-INF/spring.factories的内容为:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.demospringbootstarter.config.AnimalAutoConfig

以上步骤都好后,使用maven命令打包:

mvn clean install -Dmaven.test.skip=true

或者使用idea的LIfecycle点击对应操作(注意不是plugin下的命令操作)。

pom.xml内容为:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.3.4.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo-spring-boot-starter</artifactId>
    <version>0.0.4-SNAPSHOT</version>
    <name>demo-spring-boot-starter</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

    </dependencies>

    <repositories>
        <repository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </repository>
    </repositories>

    <pluginRepositories>
        <pluginRepository>
            <id>alimaven</id>
            <url>https://maven.aliyun.com/repository/public</url>
        </pluginRepository>
    </pluginRepositories>

</project>

三、组件集成依赖测试

3.1、新启另一个项目中,引入刚刚打包的pom依赖

        <dependency>
            <groupId>com.example</groupId>
            <artifactId>demo-spring-boot-starter</artifactId>
            <version>0.0.4-SNAPSHOT</version>
        </dependency>

3.2、新建一个controller,里面注入上面提供的AnimalService类并调用其方法

package com.cjb.mavendemo.controllers;

import com.example.demospringbootstarter.service.AnimalService;
import com.example.inputoutputlogspringbootstarter.config.PrintResponseTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Project: maven-demo
 * @Description:
 * @Author: chengjiangbo
 * @Date: 2023/2/7  10:26
 */
@RestController
@RequestMapping(value = "/test")
public class TestController {

    @Autowired
    private AnimalService animalService;

    @PrintResponseTime
    @GetMapping("/call")
    public String call(){
        return animalService.say();
    }
}

3.3、application.properties内容配置参数"animal.name"值

3.4、最后通过项目启动类启动项目(项目启动类就一个@SpringBootApplicaiton注解)

package com.cjb.mavendemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MavenDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MavenDemoApplication.class, args);
    }

}

3.5、接口测试

调用http接口测试:

修改"animal.name"值为"cat",再次调用http接口访问:

四、源码地址

  1. 组件代码:https://download.csdn.net/download/u010132847/87426046

  1. 集成自定义组件代码:https://download.csdn.net/download/u010132847/87426048

有关实现自定义SpringBoot的Starter组件的更多相关文章

  1. ruby - Facter::Util::Uptime:Module 的未定义方法 get_uptime (NoMethodError) - 2

    我正在尝试设置一个puppet节点,但ruby​​gems似乎不正常。如果我通过它自己的二进制文件(/usr/lib/ruby/gems/1.8/gems/facter-1.5.8/bin/facter)在cli上运行facter,它工作正常,但如果我通过由ruby​​gems(/usr/bin/facter)安装的二进制文件,它抛出:/usr/lib/ruby/1.8/facter/uptime.rb:11:undefinedmethod`get_uptime'forFacter::Util::Uptime:Module(NoMethodError)from/usr/lib/ruby

  2. ruby-on-rails - Rails 3.2.1 中 ActionMailer 中的未定义方法 'default_content_type=' - 2

    我在我的项目中添加了一个系统来重置用户密码并通过电子邮件将密码发送给他,以防他忘记密码。昨天它运行良好(当我实现它时)。当我今天尝试启动服务器时,出现以下错误。=>BootingWEBrick=>Rails3.2.1applicationstartingindevelopmentonhttp://0.0.0.0:3000=>Callwith-dtodetach=>Ctrl-CtoshutdownserverExiting/Users/vinayshenoy/.rvm/gems/ruby-1.9.3-p0/gems/actionmailer-3.2.1/lib/action_mailer

  3. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  4. ruby - 主要 :Object when running build from sublime 的未定义方法 `require_relative' - 2

    我已经从我的命令行中获得了一切,所以我可以运行rubymyfile并且它可以正常工作。但是当我尝试从sublime中运行它时,我得到了undefinedmethod`require_relative'formain:Object有人知道我的sublime设置中缺少什么吗?我正在使用OSX并安装了rvm。 最佳答案 或者,您可以只使用“require”,它应该可以正常工作。我认为“require_relative”仅适用于ruby​​1.9+ 关于ruby-主要:Objectwhenrun

  5. ruby - 如何根据特征实现 FactoryGirl 的条件行为 - 2

    我有一个用户工厂。我希望默认情况下确认用户。但是鉴于unconfirmed特征,我不希望它们被确认。虽然我有一个基于实现细节而不是抽象的工作实现,但我想知道如何正确地做到这一点。factory:userdoafter(:create)do|user,evaluator|#unwantedimplementationdetailshereunlessFactoryGirl.factories[:user].defined_traits.map(&:name).include?(:unconfirmed)user.confirm!endendtrait:unconfirmeddoenden

  6. ruby - 在 Ruby 中有条件地定义函数 - 2

    我有一些代码在几个不同的位置之一运行:作为具有调试输出的命令行工具,作为不接受任何输出的更大程序的一部分,以及在Rails环境中。有时我需要根据代码的位置对代码进行细微的更改,我意识到以下样式似乎可行:print"Testingnestedfunctionsdefined\n"CLI=trueifCLIdeftest_printprint"CommandLineVersion\n"endelsedeftest_printprint"ReleaseVersion\n"endendtest_print()这导致:TestingnestedfunctionsdefinedCommandLin

  7. ruby - 定义方法参数的条件 - 2

    我有一个只接受一个参数的方法:defmy_method(number)end如果使用number调用方法,我该如何引发错误??通常,我如何定义方法参数的条件?比如我想在调用的时候报错:my_method(1) 最佳答案 您可以添加guard在函数的开头,如果参数无效则引发异常。例如:defmy_method(number)failArgumentError,"Inputshouldbegreaterthanorequalto2"ifnumbereputse.messageend#=>Inputshouldbegreaterthano

  8. ruby - 如何在 Grape 中定义哈希数组? - 2

    我使用Ember作为我的前端和GrapeAPI来为我的API提供服务。前端发送类似:{"service"=>{"name"=>"Name","duration"=>"30","user"=>nil,"organization"=>"org","category"=>nil,"description"=>"description","disabled"=>true,"color"=>nil,"availabilities"=>[{"day"=>"Saturday","enabled"=>false,"timeSlots"=>[{"startAt"=>"09:00AM","endAt"=>

  9. ruby - 获取模块中定义的所有常量的值 - 2

    我想获取模块中定义的所有常量的值:moduleLettersA='apple'.freezeB='boy'.freezeendconstants给了我常量的名字:Letters.constants(false)#=>[:A,:B]如何获取它们的值的数组,即["apple","boy"]? 最佳答案 为了做到这一点,请使用mapLetters.constants(false).map&Letters.method(:const_get)这将返回["a","b"]第二种方式:Letters.constants(false).map{|c

  10. ruby - 这两个 Ruby 类初始化定义有什么区别? - 2

    我正在阅读一本关于Ruby的书,作者在编写类初始化定义时使用的形式与他在本书前几节中使用的形式略有不同。它看起来像这样:classTicketattr_accessor:venue,:datedefinitialize(venue,date)self.venue=venueself.date=dateendend在本书的前几节中,它的定义如下:classTicketattr_accessor:venue,:datedefinitialize(venue,date)@venue=venue@date=dateendend在第一个示例中使用setter方法与在第二个示例中使用实例变量之间是

随机推荐