京东物流 孔祥东

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

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官方文档摘抄:

手把手带你开发starter,点对点带你讲解原理

这段话的大约意思便是,麻烦咱们遵守这个命名规范:

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

非Spring官方主张命名格局:{name}-spring-boot-starter

3. 开发示例

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

3.1 新建工程

首要新建一个maven 工程,称号界说为jd-log-spring-boot-starter

手把手带你开发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 文件,大约内容便是界说的装备的元数据;效果如下截图。

手把手带你开发starter,点对点带你讲解原理

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;

整体项目结构便是这样子

手把手带你开发starter,点对点带你讲解原理

好了,现在就能够打包编译安装

手把手带你开发starter,点对点带你讲解原理

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 注解

手把手带你开发starter,点对点带你讲解原理

application.yaml 文件中装备

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

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

手把手带你开发starter,点对点带你讲解原理

咋样,自界说的Starter是不是特别的简略啊,快着手试试吧!

上面咱们讲的都是怎样去开发一个starter,可是到底为什么要这样,spring-boot 是如何去完结的?是不是还不知道?那下面咱们就来说说;

4. 原理解说

咱们上面现已看到一个starter,只需求引进到pom 文件中,再装备一下(其实都能够不装备)jd.enable=true,就能够直接运用记录日志的功用了,Spring-boot 是怎样做到的?

在开端的时分说过,Spring-boot 的好处便是能够主动安装。那下面我就来说说主动安装的原理。

比较于传统Spring 运用,咱们建立一个SpringBoot 运用,咱们只需求引进一个注解(前提:引进springBoot y依靠)@SpringBootApplication,就能够直接运行;所以咱们就从这个注解开端入手,看看这个注解到底做了写什么?

SpringBootApplication 注解

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

手把手带你开发starter,点对点带你讲解原理

前面的四个注解就不必过多叙说了,是界说注解最基本的,要害在于后边的三个注解:@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan,其实也便是说在发动类上假如不运用@SpringBootApplication 这个复合注解,直接运用者三个注解一样能够到达相同的效果。

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

手把手带你开发starter,点对点带你讲解原理

@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 注解组成的复合注解;

手把手带你开发starter,点对点带你讲解原理

首要咱们先来看@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是怎样生成的。

手把手带你开发starter,点对点带你讲解原理

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

手把手带你开发starter,点对点带你讲解原理

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

手把手带你开发starter,点对点带你讲解原理

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

手把手带你开发starter,点对点带你讲解原理

手把手带你开发starter,点对点带你讲解原理

这边再扩展一下这个内容,经过 SpringFactoriesLoader 来读取装备文件 spring.factories 中的装备文件的这种方法是一种 SPI 的思维。

@AutoConfigurationPackage 注解

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

手把手带你开发starter,点对点带你讲解原理

再进入这个类检查,它其实是一个内部类,看代码的大约意思便是读取到咱们在最外层的 @SpringBootApplication 注解中装备的扫描途径(没有装备则默许当时包下),然后把扫描途径下面的Bean注册到容器中;

手把手带你开发starter,点对点带你讲解原理

总结

好了,现在咱们大约来理一下整个主动安装的流程:

  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中的界说了啊,赶忙试试吧