创建自己的 spring-boot-starter

在系统架构中,可以将通用业务和公共组件,抽取出来,做成一个 Spring Boot Starter 以便于所有系统使用

Spring Boot Starter 特性

  • 提供统一 dependency 版本管理。导入 starter 即可自动引入相关 library 或者中间件,并且保证各 dependency 之间不冲突
  • 向 Spring 容器中自动注入所需要的 Bean,并完成对应配置。依赖 Spring Boot autoconfigure 模块
  • 提供 properties 外部配置的能力,方便用户引入 starter 后,根据情况配置 starter 配置信息

自定义一个最简洁的 spring boot starter

基本信息

  • artifactId 命名
    官方包 artifactId 命名格式为:spring-boot-starter-{name}
    自定义 starter 包 artifactId 官方推荐命名方式:{name}-spring-boot-starter

实现方式

  • 方式一
    autoconfigure模块:包含自动配置的代码
    starter模块:提供对autoconfigure模块的依赖,以及一些其它的依赖
  • 方式二
    将方式一中的两个代码写成一个模块

基础依赖

  • 自动化配置依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <properties>
    <java.version>1.8</java.version>
    <project.encoding>UTF-8</project.encoding>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <!-- 其他属性 -->
    <spring-boot.version>2.1.6.RELEASE</spring-boot.version>
    </properties>

    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>1.5.9.RELEASE</version>
    </dependency>

基础实现

  • 对外提供服务的类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    public class DemoService {

    private String words;

    private String getWords() {
    return words;
    }

    public void setWords(String words) {
    this.words = words;
    }

    public String sayHello() {
    return "hello, " + words;
    }
    }
  • 配置映射

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    // 自动注入 cedar.demo 节点下的属性 words
    @ConfigurationProperties(prefix = "cedar.demo")
    public class DemoProperties {
    public static final String DEFAULT_WORDS = "world";

    private String words = DEFAULT_WORDS;

    public String getWords() {
    return words;
    }

    public void setWords(String words) {
    this.words = words;
    }
    }
  • Enable

    1
    2
    3
    4
    5
    6
    7
    8
    // 如果我们有一个模块是由多个配置类组成,这种办法是一种将这些配置类聚合到单个模块中的方便且富有表现力的方法。
    @Retention(RetentionPolicy.RUNTIME)
    @Target({ElementType.TYPE})
    @Documented
    @Import(BookingModuleConfiguration.class)
    @Configuration
    public @interface EnableBookingModule {
    }
  • 自动化配置,将配置设置到服务中

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    @Configuration
    @ConditionalOnClass({DemoService.class})
    @EnableConfigurationProperties(DemoProperties.class)
    public class DemoAutoConfiguration {
    @Autowired
    private DemoProperties demoProperties;

    @Bean
    @ConditionalOnMissingBean(DemoService.class)
    public DemoService demoService() {
    DemoService demoService = new DemoService();
    demoService.setWords(demoProperties.getWords());
    return demoService;
    }
    }
  • resources/META-INF/spring.factories 文件设置

    1
    2
    # SpringBoot 自动化配置最终要扫描 META-INF/spring.factories 来加载项目的自动化配置类
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=me.longgen.cedar.starters.demo.DemoAutoConfiguration

基本使用方式

  • mvn install 将 starter 打包到本地仓库
  • 在新项目中添加 demo-spring-boot-starter 依赖
  • application.properties 配置中,增加 cedar.demo.words 属性
  • 在需要使用 demo 服务的位置,自动注入 DemoService 即可
    1
    2
    @Autowired
    private DemoService demoService;

附录

  • SpringBoot 中的所有 @Conditional 注解及其作用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    // 当容器中有指定的Bean的条件下
    @ConditionalOnBean
    // 当类路径下有指定的类的条件下
    @ConditionalOnClass
    // 基于SpEL表达式作为判断条件
    @ConditionalOnExpression
    // 基于JVM版本作为判断条件
    @ConditionalOnJava
    // 在JNDI存在的条件下查找指定的位置
    @ConditionalOnJndi
    // 当容器中没有指定Bean的情况下
    @ConditionalOnMissingBean
    // 当类路径下没有指定的类的条件下
    @ConditionalOnMissingClass
    // 当前项目不是Web项目的条件下
    @ConditionalOnNotWebApplication
    // 指定的属性是否有指定的值
    @ConditionalOnProperty
    // 类路径下是否有指定的资源
    @ConditionalOnResource
    // 当指定的Bean在容器中只有一个,或者在有多个Bean的情况下,用来指定首选的Bean
    @ConditionalOnSingleCandidate
    // 当前项目是Web项目的条件下
    @ConditionalOnWebApplication

常见问题

  • 使用 IntelliJ IDEA 提示 Spring Boot Annotion processor not found in classpath
    需要在 pom 中添加如下 dependences。

    1
    2
    3
    4
    5
    6
    <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>${spring-boot.version}</version>
    <optional>true</optional>
    </dependency>
  • 使用 IntelliJ IDEA 提示 Re-run Spring Boot Configuration Annotation Processor to update generated metadata
    只是提醒用户修改了含 @ConfigurationProperties 注解的文件以后,及时 rebuild 更新 spring-configuration-metadata.json 文件,无需额外处理。

参考代码

  • Bean 注入与获取

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    /*****   方式一   *****/
    // 注入 Bean
    @Configuration
    public class AppConfig {

    @Bean
    public SampleService sampleService() {
    return new SampleServiceImpl();
    }
    }

    // 获取 Bean
    public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    SampleService myService = ctx.getBean("sampleService" ,SampleService.class);
    myService.doService();
    }

    /***** 方式二 *****/
    // 注入 Bean
    @Configuration
    @ComponentScan(basePackages = "com.**.service.impl")
    public class AppConfig {

    }

    // 获取 Bean
    public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
    ctx.scan("com.**.service.impl");
    ctx.refresh();
    SampleService myService = ctx.getBean("sampleService" ,SampleService.class);
    myService.doService();
    }
  • 配置信息映射
    将所有的spring配置全部放在同一个类中肯定是不合适的,这会导致那个配置类非常复杂。通常会创建多个配置类,再借助@Import将多个配置类组合成一个。@Import的功能类似于XML中的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
@Configuration
public class ConfigA {

@Bean
public A a() {
return new A();
}
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

@Bean
public B b() {
return new B();
}
}

// 获取配置
public static void main(String[] args) {
// 只加载ConfigB一个配置类,但同时也包含了ConfigA的配置
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);

System.out.println(a);
System.out.println(b);
}
// 引入多个配置类
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

@Bean
public DataSource dataSource() {
// return new DataSource
}
}