乐趣区

关于springboot:手把手带你开发starter点对点带你讲解原理

京东物流 孔祥东

   _____            _             ____              _   
  / ____|          (_)           |  _ \            | |  
 | (___  _ __  _ __ _ _ __   __ _| |_) | ___   ___ | |_ 
  ___ | '_ |'__| | '_ \ / _` |  _ < / _ \ / _ | __|
  ____) | |_) | |  | | | | | (_| | |_) | (_) | (_) | |_ 
 |_____/| .__/|_|  |_|_| |_|__, |____/ ___/ ___/ __|
        | |                  __/ |                      
        |_|                 |___/                       

1. 为什么要用 Starter?

  • 当初咱们就来回顾一下,在还没有 Spring-boot 框架的时候,咱们应用 Spring 开发我的项目,如果须要某一个框架,例如 mybatis,咱们的步骤个别都是:

<!—->

  • 到 maven 仓库去找须要引入的 mybatis jar 包,选取适合的版本(易发生冲突)

<!—->

  • 到 maven 仓库去找 mybatis-spring 整合的 jar 包,选取适合的版本(易发生冲突)

<!—->

  • 在 spring 的 applicationContext.xml 文件中配置 dataSource 和 mybatis 相干信息

<!—->

<!—->

  • 如果所有工作都到位,个别能够零打碎敲;但很多时候都会花一堆工夫解决 jar 抵触,配置项缺失,导致怎么都启动不起来等等,各种问题。

所以在 2012 年 10 月,一个叫 Mike Youngstrom 的人在 Spring Jira 中创立了一个性能申请,要求在 Spring Framework 中反对无容器 Web 应用程序体系结构,提出了在主容器疏导 Spring 容器内配置 Web 容器服务;这件事件对 SpringBoot 的诞生应该说是起到了肯定的推动作用。

所以 SpringBoot 设计的指标就是简化繁琐配置,疾速建设 Spring 利用。

  • 而后在开发 Spring-boot 利用的是时候,常常能够看到咱们的 pom 文件中引入了 spring-boot-starter-web、spring-boot-starter-data-redis、mybatis-spring-boot-starter 这样的依赖,而后简直不必任何配置就能够应用这些依赖的性能,真正的感触到了 开箱即用 的爽。

<!—->

  • 上面咱们就先来尝试本人开发一个 Starter。

2. 命名标准

在应用 spring-boot-starter,会发现,有的项目名称是 XX-spring-boot-starter,有的是 spring-boot-starter-XX,这个我的项目的名称有什么考究呢?从 springboot 官网文档摘录:

![]()

这段话的大略意思就是,麻烦大家恪守这个命名标准:

Srping 官网命名格局为:spring-boot-starter-{name}

非 Spring 官网倡议命名格局:{name}-spring-boot-starter

3. 开发示例

上面我就以记录日志的一个组件为示例来讲述开发一个 starter 的过程。

3.1 新建工程

首先新建一个 maven 工程, 名称定义为 jd-log-spring-boot-starter

3.2 Pom 引入依赖

<?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 http://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.5.13</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.jd</groupId>
  <artifactId>jd-log-spring-boot-starter</artifactId>
  <version>1.0-SNAPSHOT</version>
  <name>jd-log-spring-boot-starter</name>
  <url>http://www.example.com</url>
  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>


  <dependencies>
    <!-- 提供了主动拆卸性能 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <!-- 在编译时会主动收集配置类的条件,写到一个 META-INF/spring-autoconfigure-metadata.json 中 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-configuration-processor</artifactId>
    </dependency>
    <!-- 记录日志会用到切面,所以须要引入 -->
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-source-plugin</artifactId>
        <version>2.2.1</version>
        <executions>
          <execution>
            <id>attach-sources</id>
            <goals>
              <goal>jar-no-fork</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
    </plugins>
  </build>
</project>

这边略微解释一下这几个依赖:

spring-boot-autoconfigure : 提供自动化拆卸性能,是为了 Spring Boot 利用在各个模块提供自动化配置的作用;即退出对应 pom,就会有对应配置其作用;所以咱们想要主动拆卸性能,就须要引入这个依赖。

spring-boot-configuration-processor:将自定义的配置类生成配置元数据,所以在援用自定义 STARTER 的工程的 YML 文件中,给自定义配置初始化时,会有属性名的提醒;确保在应用 @ConfigurationProperties 注解时,能够优雅的读取配置信息,引入该依赖后,IDEA 不会呈现“spring boot configuration annotation processor not configured”的谬误;编译之后会在 META-INF 下生成一个 spring-configuration-metadata.json 文件,大略内容就是定义的配置的元数据;成果如下截图。

spring-boot-starter-aop:这个就不必解释了,因为示例是记录日志,咱们用到切面的性能,所以须要引入。

3.3 定义属性配置

/**
 * @author kongxiangdong2
 * @Title: LogProperties
 * @ProjectName jd-log-spring-boot-starter
 * @Description: TODO
 * @date 2022/9/110:04
 */
@ConfigurationProperties(prefix = "jd")
@Data
public class LogProperties {


    /**
     * 是否开启日志
     */
    private boolean enable;


    /**
     * 平台:不同服务应用的辨别,默认取 spring.application.name
     */
    @Value("${spring.application.name:#{null}}")
    private String platform;

@ConfigurationProperties:该注解和 @Value 注解作用相似,用于获取配置文件中属性定义并绑定到 Java Bean 或者属性中;换句话来说就是将配置文件中的配置封装到 JAVA 实体对象,方便使用和治理。

这边咱们定义两个属性,一个是是否开启日志的开关,一个是标识平台的名称。

3.4 定义主动配置类

/**
 * @author kongxiangdong2
 * @Title: JdLogAutoConfiguration
 * @ProjectName jd-log-spring-boot-starter
 * @Description: TODO
 * @date 2022/9/110:06
 */
@Configuration
@ComponentScan("com.jd")
@ConditionalOnProperty(prefix = "jd",name = "enable",havingValue = "true",matchIfMissing = false)
@EnableConfigurationProperties({LogProperties.class})
public class JdLogAutoConfiguration {//}

这个类最要害了,它是整个 starter 最重要的类,它就是将配置主动装载进 spring-boot 的;具体是怎么实现的,上面在解说原理的时候会再具体说说,这里先实现示例。

@Configuration:这个就是申明这个类是一个配置类

@ConditionalOnProperty:作用是能够指定 prefix.name 配置文件中的属性值来断定 configuration 是否被注入到 Spring, 就拿下面代码的来说,会依据配置文件中是否配置 jd.enable 来判断是否须要加载 JdLogAutoConfiguration 类,如果配置文件中不存在或者配置的是等于 false 都不会进行加载,如果配置成 true 则会加载;指定了 havingValue,要把配置项的值与 havingValue 比照,统一则加载 Bean; 配置文件短少配置,但配置了 matchIfMissing = true,加载 Bean,否则不加载。

在这里略微扩大一下常常应用的 Condition

注解 类型 阐明
@ConditionalOnClass Class Conditions 类条件注解 以后 classpath 下有指定类才加载
@ConditionalOnMissingClass Class Conditions 类条件注解 以后 classpath 下无指定类才加载
@ConditionalOnBean Bean ConditionsBean 条件注解 当期容器内有指定 bean 才加载
@ConditionalOnMissingBean Bean ConditionsBean 条件注解 当期容器内无指定 bean 才加载
@ConditionalOnProperty Property Conditions 环境变量条件注解(含配置文件) prefix 前缀 name 名称 havingValue 用于匹配配置项值 matchIfMissing 没找指定配置项时的默认值
@ConditionalOnResource ResourceConditions 资源条件注解 有指定资源才加载
@ConditionalOnWebApplication Web Application Conditionsweb 条件注解 是 web 才加载
@ConditionalOnNotWebApplication Web Application Conditionsweb 条件注解 不是 web 才加载
@ConditionalOnExpression SpEL Expression Conditions 合乎 SpEL 表达式才加载

@EnableConfigurationProperties 使 @ConfigurationProperties 注解的类失效。

3.5 配置 EnableAutoConfiguration

在 resources/META-INF/ 目录新建 spring.factories 文件,配置内容如下;

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.jd.JdLogAutoConfiguration

好了,至此自定义 Starter 大体框架曾经好了,上面就是咱们记录日志的性能。

3.6 业务性能实现

首先咱们先定义一个注解 Jdlog

/**
 * @author kongxiangdong2
 * @Title: Jdlog
 * @ProjectName jd-log-spring-boot-starter
 * @Description: TODO
 * @date 2022/9/110:04
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Jdlog {}

定义切面执行逻辑,这边就简略的打印一下配置文件的属性值 + 指标执行办法 + 耗时。

import com.jd.annotation.Jdlog;
import com.jd.config.LogProperties;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;


/**
 * @author kongxiangdong2
 * @Title: LogAspectjProcess
 * @ProjectName jd-log-spring-boot-starter
 * @Description: TODO
 * @date 2022/9/111:12
 */
@Aspect
@Component
@Slf4j
@AllArgsConstructor
public class LogAspectjProcess {


    LogProperties logProperties;


    /**
     * 定义切点
     */
    @Pointcut("@annotation(com.jd.annotation.Jdlog)")
    public void pointCut(){}


    /**
     * 盘绕告诉
     *
     * @param thisJoinPoint
     * @param jdlog
     * @return
     */
    @Around("pointCut() && @annotation(jdlog)")
    public Object around(ProceedingJoinPoint thisJoinPoint, Jdlog jdlog){


        // 执行办法名称
        String taskName = thisJoinPoint.getSignature()
                .toString().substring(thisJoinPoint.getSignature()
                                .toString().indexOf(" "),
                        thisJoinPoint.getSignature().toString().indexOf("("));
        taskName = taskName.trim();
        long time = System.currentTimeMillis();
        Object result = null;
        try {result = thisJoinPoint.proceed();
        } catch (Throwable throwable) {throwable.printStackTrace();
        }
        log.info("{} -- method:{} run :{} ms",logProperties.getPlatform(), taskName,
                (System.currentTimeMillis() - time));
        return result;


    

整体我的项目构造就是这样子

好了,当初就能够打包编译装置

3.7 测试应用

而后就能够在其余我的项目中引入应用了;上面以一个简略的 spring-boot web 我的项目做个测试,在 pom 中引入上面的依赖配置。

  <dependency>
      <groupId>com.jd</groupId>
      <artifactId>jd-log-spring-boot-starter</artifactId>
      <version>1.0-SNAPSHOT</version>
    </dependency>

减少一个 http 拜访的办法,标注上 @Jdlog 注解

application.yaml 文件中配置

jd:
  enable: true
  platform: "测试项目"

启动测试,拜访地址 http://localhost:8080/test/me…,控制台打印如下:

咋样,自定义的 Starter 是不是特地的简略啊,快入手试试吧!

下面咱们讲的都是怎么去开发一个 starter, 然而到底为什么要这样,spring-boot 是如何去实现的?是不是还不晓得?那上面咱们就来说说;

代码示例地址:https://coding.jd.com/kongxia…

4. 原理解说

咱们下面曾经看到一个 starter, 只须要引入到 pom 文件中,再配置一下(其实都能够不配置)jd.enable=true,就能够间接应用记录日志的性能了,Spring-boot 是怎么做到的?

在开始的时候说过,Spring-boot 的益处就是能够主动拆卸。那上面我就来说说主动拆卸的原理。

相比于传统 Spring 利用,咱们搭建一个 SpringBoot 利用,咱们只须要引入一个注解(前提:引入 springBoot y 依赖)@SpringBootApplication,就能够间接运行;所以咱们就从这个注解开始动手,看看这个注解到底做了写什么?

SpringBootApplication 注解

点开 @SpringBootApplication 注解能够看到蕴含了 @SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan 三个注解。

后面的四个注解就不必过多叙述了,是定义注解最根本的,关键在于前面的三个注解:@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan, 其实也就是说在启动类上如果不应用 @SpringBootApplication 这个复合注解,间接使用者三个注解一样能够达到雷同的成果。

@SpringBootConfiguration 注解:咱们再次点进去看这个注解,其实它就是一个 @Configuration 注解。

@ComponentScan 注解

@ComponentScan 注解:配置包扫描定义的扫描门路,把合乎扫描规定的类拆卸到 spring 容器

@EnableAutoConfiguration 注解

@EnableAutoConfiguration 关上主动拆卸(主动配置着重来看该注解)

注解 作用 解释
@SpringBootConfiguration 标记以后类为配置类 加上这个注解就是为了让以后类作为一个配置类交由 Spring 的 IOC 容器进行治理,因为后面咱们说了,SpringBoot 实质上还是 Spring,所以原属于 Spring 的注解 @Configuration 在 SpringBoot 中也能够间接利用
@ComponentScan 配置包扫描定义的扫描门路,把合乎扫描规定的类拆卸到 spring 容器 用于定义 Spring 的扫描门路,等价于在 xml 文件中配置 <context:component-scan>,如果不配置扫描门路,那么 Spring 就会默认扫描以后类所在的包及其子包中的所有标注了 @Component,@Service,@Controller 等注解的类。
@EnableAutoConfiguration 关上主动拆卸 上面着重解说

咱们再次点击 @EnableAutoConfiguration 进入查看,它是一个由 @AutoConfigurationPackage 和 @Import 注解组成的复合注解;

首先咱们先来看 @Import 这个注解,这个是比拟要害的一个注解;

在说这个注解之前咱们先举个例子,如果咱们有一个类 Demo, 它是一个不在启动配置类目录之下的,也就意味着它不会被扫描到,Spring 也无奈感知到它的存在,那么如果须要能将它被扫描到,是不是咱们能够通过加 @Import 注解来导入 Demo 类,相似如下代码

@Configuration
@Import(Demo.class)
public class MyConfiguration {}

所以,咱们能够晓得 @Import 注解其实就是为了去导入一个类。所以这里 @Import({AutoConfigurationImportSelector.class}) 就是为了导入 AutoConfigurationImportSelector 类,那咱们持续来看这个类,AutoConfigurationImportSelector 实现的是 DeferredImportSelector 接口,这是一个提早导入的类;再细看会有一个办法比拟显眼,依据注解元数据来抉择导入组件,当注解元数据空,间接返回一个空数组;否则就调用 getAutoConfigurationEntry,办法中会应用 AutoConfigurationEntry 的 getConfigurations(),configurations 是一个List<String>, 那么咱们看下 AutoConfigurationEntry 是怎么生成的。

进入到 getAutoConfigurationEntry 办法中能够看到次要是 getCandidateConfigurations 来获取候选的 Bean,并将其存为一个汇合;后续的办法都是在去重,校验等一系列的操作。

咱们持续往 getCandidateConfigurations 办法里看,最终通过 SpringFactoriesLoader.loadFactoryNames 来获取最终的 configurations,并且能够通过断言发现会应用到 META-INF/spring.factories 文件,那么咱们再进入 SpringFactoriesLoader.loadFactoryNames()中来看下最终的实现。

SpringFactoriesLoader.loadFactoryNames()办法会读取 META-INF/spring.factories 文件下的内容到 Map 中,再联合传入的 factoryType=EnableAutoConfiguration.class, 因而会拿到 org.springframework.boot.autoconfigure.EnableAutoConfiguration 为 key 对应的各个 XXAutoConfiguration 的值,而后 springboot 在联合各个 starter 中的代码实现对于 XXAutoConfiguration 中的 Bean 的加载动作。

这边再扩大一下这个内容,通过 SpringFactoriesLoader 来读取配置文件 spring.factories 中的配置文件的这种形式是一种 SPI 的思维。

@AutoConfigurationPackage 注解

进入这个注解看,其实它就是导入了 Registrar 这个类

再进入这个类查看,它其实是一个外部类,看代码的大略意思就是读取到咱们在最外层的 @SpringBootApplication 注解中配置的扫描门路(没有配置则默认以后包下),而后把扫描门路上面的 Bean 注册到容器中;

总结

好了,当初咱们大略来理一下整个主动拆卸的流程:

  1. 启动类中通过应用 @SpringBootApplication 实现主动拆卸的性能;

<!—->

  1. 理论注解 @SpringBootApplication 是借助注解 @EnableAutoConfiguration 的性能。

<!—->

  1. 在注解 @EnableAutoConfiguration 中又有两个注解,@AutoConfigurationPackage,@EnableAutoConfiguration。

<!—->

  1. 通过 @AutoConfigurationPackage 实现对于以后我的项目中 Bean 的进行加载;

<!—->

  1. @EnableAutoConfiguration 通过 @Import({AutoConfigurationImportSelector.class})实现对于 Pom 引入的 start 中的 XXAutoConfiguration 的加载;

<!—->

  1. @AutoConfigurationImportSelector 类中通过 SpringFactoriesLoader 读取 META-INF/spring.factories 中 key 为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的各个 XXAutoConfiguration 的值,而后 springboot 在联合各个 start 中的代码实现对于 XXAutoConfiguration 中的 Bean 的加载动作;

到这里是不是曾经能够很了然对咱们之前开发 starter 中的定义了啊,连忙试试吧

退出移动版