vlambda博客
学习文章列表

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》编写自定义Spring Boot启动器

Chapter 23. Writing Custom Spring Boot Starters

在本章中,我们将介绍以下主题:

  • Understanding Spring Boot autoconfiguration
  • Creating a custom Spring Boot autoconfiguration starter
  • Configuring custom conditional bean instantiations
  • Using custom @Enable annotations to toggle configurations

Introduction


在前面的章节中,我们在开发 Spring Boot 应用程序时做了很多配置,甚至更多的是自动配置。现在,是时候看看幕后花絮,了解 Spring Boot 自动配置背后的魔力,并编写一些我们自己的启动器。

这是一种非常有用的能力,特别是对于不可避免地存在专有代码的大型软件企业。能够创建内部自定义启动器会自动将一些配置或功能添加到应用程序是非常有帮助的。一些可能的候选对象是自定义配置系统、库和处理连接到数据库、使用自定义连接池、HTTP 客户端、服务器等的配置。我们将深入了解 Spring Boot 自动配置的内部结构,了解如何创建新的启动器,探索基于各种规则的 bean 的条件初始化和连接,并看到注解可以成为一个强大的工具,为启动器的消费者提供更多地控制决定应该使用哪些配置以及在哪里使用。

Understanding Spring Boot autoconfiguration


当 Spring Boot 引导应用程序并使用所需的东西准确配置它时,它具有强大的功能,所有无需我们开发人员所需的大量胶水代码。这种力量背后的秘密实际上来自 Spring 本身,或者更确切地说来自它提供的 Java 配置功能。随着我们添加更多的启动器作为依赖项,越来越多的类将出现在我们的类路径中。 Spring Boot 检测特定类的存在或不存在,并基于此信息做出一些有时相当复杂的决策,并自动创建必要的 bean 并将其连接到应用程序上下文。

听起来很简单,对吧?

在前面的秘籍中,我们添加了一些 Spring Boot 启动器,例如 spring-boot-starter-data-jpaspring-boot-starter -webspring-boot-starter-data-test等。我们将使用我们在上一章中完成的相同代码,以了解在应用程序启动期间实际发生的情况以及 Spring Boot 在将我们的应用程序连接在一起时将做出的决定。

How to do it...

  1. Conveniently, Spring Boot provides us with an ability to get the CONDITIONS EVALUATION REPORT by simply starting the application with the debug flag. This can be passed to the application either as an environment variable, DEBUG, as a system property, -Ddebug, or as an application property, --debug.
  2. Start the application by running DEBUG=true ./gradlew clean bootRun.
  3. Now, if you look at the console logs, you will see a lot more information printed there that is marked with the DEBUG level log. At the end of the startup log sequence, we will see the CONDITIONS EVALUATION REPORT as follows:
=========================CONDITIONS EVALUATION REPORT=========================Positive matches:-----------------...DataSourceAutoConfiguration      - @ConditionalOnClass classes found:    
            javax.sql.DataSource,org.springframework.jdbc.
            datasource.embedded.EmbeddedDatabaseType   
            (OnClassCondition)...Negative matches:-----------------...GsonAutoConfiguration      - required @ConditionalOnClass classes not found:  
          com.google.gson.Gson (OnClassCondition)...

How it works...

如您所见,在调试模式下打印的信息量可能有点压倒性的,所以我选择了每个只有一个正负匹配的例子。

对于报告的每一行,Spring Boot 告诉我们为什么选择包含某些配置,它们被正面匹配,或者,对于负面匹配,缺少什么阻止特定配置被包含在组合中。让我们看一下 DataSourceAutoConfiguration 的正匹配:

  • The @ConditionalOnClass classes found tells us that Spring Boot has detected the presence of a particular class, specifically two classes in our case: javax.sql.DataSource and org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType.
  • The OnClassCondition indicates the kind of matching that was used. This is supported by the @ConditionalOnClass and @ConditionalOnMissingClass annotations.

虽然 OnClassCondition 是最常见的检测类型,但 Spring Boot 还使用许多其他条件。例如 OnBeanCondition 用于检查特定 bean 实例的有无, OnPropertyCondition 用于检查有无,或属性的特定值,以及可以使用 @Conditional 注释和 Condition 接口实现。

负匹配向我们展示了 Spring Boot 评估过的配置列表,这意味着它们确实存在于类路径中并且被 Spring Boot 扫描但没有通过包含它们所需的条件。 GsonAutoConfiguration,虽然在类路径中可用,因为它是导入的 spring-boot-autoconfigure 工件的一部分,但没有包括在内,因为在类路径中未检测到所需的 com.google.gson.Gson 类,因此 OnClassCondition 失败。

GsonAutoConfiguration 文件的 implementation 如下所示:

@Configuration 
@ConditionalOnClass(Gson.class) 
public class GsonAutoConfiguration { 
 
  @Bean 
  @ConditionalOnMissingBean 
  public Gson gson() { 
    return new Gson(); 
  } 
 
} 

看了代码,很容易就可以很容易的把Spring Boot在启动时提供的条件注解和报告信息联系起来。

Creating a custom Spring Boot autoconfiguration starter


我们对 process 有一个高级的想法,Spring Boot 通过它决定在应用程序上下文的形成中包含哪些配置.现在,让我们尝试创建我们自己的 Spring Boot 启动器工件,我们可以将其作为可自动配置的依赖项包含在我们的构建中。

 

第 21 章中,配置 Web 应用程序,您学习了如何创建数据库 Repository 对象。因此,让我们构建一个简单的启动器,它将创建另一个 CommandLineRunner,它将获取所有 Repository 实例的集合并打印出每个条目的总数。

我们将首先将一个子 Gradle 项目添加到我们现有的项目中,该项目将容纳入门工件的代码库。我们称之为db-count-starter

How to do it...

  1. We will start by creating a new directory named db-count-starter in the root of our project.
  2. As our project has now become what is known as a multiproject build, we will need to create a settings.gradle configuration file in the root of our project with the following content:
include 'db-count-starter' 
  1. We should also create a separate build.gradle configuration file for our subproject in the db-count-starter directory in the root of our project, with the following content:
apply plugin: 'java' 
 
repositories { 
  mavenCentral() 
  maven { url "https://repo.spring.io/snapshot" } 
  maven { url "https://repo.spring.io/milestone" } 
 
} 
 
dependencies { 
  compile("org.springframework.boot:spring-boot:2.0.0.BUILD-SNAPSHOT")  
  compile("org.springframework.data:spring-data-commons:2.0.2.RELEASE") 
} 
  1. Now we are ready to start coding. So, the first thing is to create the directory structure, src/main/java/com/example/bookpubstarter/dbcount, in the db-count-starter directory in the root of our project.
  1. In the newly created directory, let's add our implementation of the CommandLineRunner file named DbCountRunner.java with the following content:
public class DbCountRunner implements CommandLineRunner { 
    protected final Log logger = LogFactory.getLog(getClass()); 
 
    private Collection<CrudRepository> repositories; 
 
    public DbCountRunner(Collection<CrudRepository> repositories) { 
        this.repositories = repositories; 
    } 
 
    @Override 
    public void run(String... args) throws Exception { 
        repositories.forEach(crudRepository -> 
            logger.info(String.format("%s has %s entries", 
                getRepositoryName(crudRepository.getClass()), 
                crudRepository.count()))); 
 
    } 
 
    private static String 
            getRepositoryName(Class crudRepositoryClass) { 
        for(Class repositoryInterface : 
                crudRepositoryClass.getInterfaces()) { 
            if (repositoryInterface.getName(). 
                    startsWith("com.example.bookpub.repository")) { 
                return repositoryInterface.getSimpleName(); 
            } 
        } 
        return "UnknownRepository"; 
    } 
} 
  1. With the actual implementation of DbCountRunner in place, we will now need to create the configuration object that will declaratively create an instance during the configuration phase. So, let's create a new class file called DbCountAutoConfiguration.java with the following content:
@Configuration 
public class DbCountAutoConfiguration { 
    @Bean 
    public DbCountRunner dbCountRunner
               (Collection<CrudRepository> repositories) { 
        return new DbCountRunner(repositories); 
    } 
}
  1. We will also need to tell Spring Boot that our newly created JAR artifact contains the autoconfiguration classes. For this, we will need to create a resources/META-INF directory in the db-count-starter/src/main directory in the root of our project.
  2. In this newly created directory, we will place the file named spring.factories with the following content:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=
com.example.bookpubstarter.dbcount.DbCountAutoConfiguration 
  1. For the purpose of our demo, we will add the dependency to our starter artifact in the main project's build.gradle by adding the following entry in the dependencies section:
compile project(':db-count-starter') 
  1. Start the application by running ./gradlew clean bootRun.
  2. Once the application is compiled and has started, we should see the following in the console logs:
2017-12-16 INFO com.example.bookpub.StartupRunner        : Welcome to the Book Catalog System!2017-12-16 INFO c.e.b.dbcount.DbCountRunner              : AuthorRepository has 1 entries2017-12-16 INFO c.e.b.dbcount.DbCountRunner              : PublisherRepository has 1 entries2017-12-16 INFO c.e.b.dbcount.DbCountRunner              : BookRepository has 1 entries2017-12-16 INFO c.e.b.dbcount.DbCountRunner              : ReviewerRepository has 0 entries2017-12-16 INFO com.example.bookpub.BookPubApplication   : Started BookPubApplication in 8.528 seconds (JVM running for 9.002)2017-12-16 INFO com.example.bookpub.StartupRunner        : Number of books: 1

 

 

How it works...

恭喜!您现在已经构建了自己的 Spring Boot 自动配置启动器。

首先,让我们快速浏览一下我们对 Gradle 构建配置所做的更改,然后我们将详细检查启动器设置.

由于 Spring Boot 启动器是一个单独的、独立的工件,因此仅向我们现有的项目源代码树添加更多类并不能真正展示太多。要制作这个单独的工件,我们有两个选择:在我们现有的项目中制作单独的 Gradle 配置,或者完全创建一个完全独立的项目。然而,最理想的解决方案是通过将嵌套项目目录和子项目依赖项添加到  build.gradle 文件中,将我们的构建转换为 Gradle Multi-Project Build根项目。通过这样做,Gradle 实际上为我们创建了一个单独的 JAR 工件,但我们不必在任何地方 publish 它,只包括它作为编译 project(':db-count-starter') 依赖项。

Note

有关 Gradle 多项目构建的更多信息,您可以查看位于 http://gradle.org/docs/current/userguide/multi_project_builds.html

Spring Boot Auto-Configuration starter 只不过是一个使用 @Configuration 注释和 spring.factories 注释的常规 Spring Java Configuration 类META-INF 目录的类路径中,并带有适当的配置条目。

在应用程序启动期间,Spring Boot 使用 Spring Core 的一部分 SpringFactoriesLoader 来获取为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 属性键。在后台,此调用从所有 jars 或其他文件中收集位于 META-INF 目录中的所有类路径中的条目,并构建一个复合列表以添加为应用程序上下文配置。除了EnableAutoConfiguration键,我们可以声明如下自动初始化启动implementations 以类似的方式:

  • org.springframework.context.ApplicationContextInitializer
  • org.springframework.context.ApplicationListener
  • org.springframework.boot.autoconfigure.AutoConfigurationImportListener
  • org.springframework.boot.autoconfigure.AutoConfigurationImportFilter
  • org.springframework.boot.autoconfigure.template.TemplateAvailabilityProvider
  • org.springframework.boot.SpringBootExceptionReporter
  • org.springframework.boot.SpringApplicationRunListener
  • org.springframework.boot.env.PropertySourceLoader
  • org.springframework.boot.env.EnvironmentPostProcessor
  • org.springframework.boot.diagnostics.FailureAnalyzer
  • org.springframework.boot.diagnostics.FailureAnalysisReporter
  • org.springframework.test.contex.TestExecutionListener

具有讽刺意味的是,Spring Boot Starter 不需要依赖 Spring Boot 库作为其编译时依赖项。如果我们查看 DbCountAutoConfiguration 类中的类导入列表,我们将看不到 org.springframework.boot 中的任何内容包裹。我们在 Spring Boot 上声明依赖的唯一原因是因为我们的 DbCountRunner 实现实现了 org.springframework.boot.CommandLineRunner 界面。

Configuring custom conditional bean instantiations


在前面的示例中,您学习了如何启动基本的 Spring Boot Starter。在应用程序类路径中包含 jar 时,将自动创建 DbCountRunner bean 并将其添加到应用程序上下文中。在本章的第一个秘籍中,我们还看到 Spring Boot 能够根据一些条件进行条件配置,例如类路径中特定类的存在、bean 的存在等。

对于这个秘籍,我们将通过条件检查来增强我们的启动器。仅当尚未创建此类的其他 bean 实例并将其添加到应用程序上下文时,这才会创建 DbCountRunner 的实例。

How to do it...

  1. In the DbCountAutoConfiguration class, we will add an @ConditionalOnMissingBean annotation to the dbCountRunner(...) method, as follows:
@Bean 
@ConditionalOnMissingBean 
public DbCountRunner 
   dbCountRunner(Collection<CrudRepository> repositories) { 
  return new DbCountRunner(repositories); 
} 
  1. We will also need to add a dependency on the spring-boot-autoconfigure artifact to the dependencies section of the db-count-starter/build.gradle file:
compile("org.springframework.boot:spring-boot-autoconfigure:2.0.0.BUILD-SNAPSHOT")
  1. Now, let's start the application by running ./gradlew clean bootRun in order to verify that we will still see the same output in the console logs as we did in the previous recipe
  2. If we start the application with the DEBUG switch so as to see the Auto-Configuration Report, which we already learned in the first recipe of this chapter, we will see that our autoconfiguration is in the Positive Matches group, as follows:
DbCountAutoConfiguration#dbCountRunner      - @ConditionalOnMissingBean (types: com.example.bookpubstarter.dbcount.DbCountRunner; SearchStrategy: all) found no beans (OnBeanCondition)
  1. Let's explicitly/manually create an instance of DbCountRunner in our main BookPubApplication configuration class, and we will also override its run(...) method, just so we can see the difference in the logs:
protected final Log logger = LogFactory.getLog(getClass()); 
@Bean 
public DbCountRunner dbCountRunner
                     (Collection<CrudRepository> repositories) { 
  return new DbCountRunner(repositories) { 
    @Override 
    public void run(String... args) throws Exception { 
      logger.info("Manually Declared DbCountRunner"); 
    } 
  }; 
} 
  1. Start the application by running DEBUG=true ./gradlew clean bootRun.
  2. If we look at the console logs, we will see two things: the Auto-Configuration Report will print our autoconfiguration in the Negative Matches group and, instead of the count output for each repository, we will see Manually Declared DbCountRunner text to appear:
DbCountAutoConfiguration#dbCountRunner      - @ConditionalOnMissingBean (types: com.example.bookpubstarter.dbcount.DbCountRunner; SearchStrategy: all) found the following [dbCountRunner] (OnBeanCondition)2017-12-16 INFO com.example.bookpub.BookPubApplication$1    : Manually Declared DbCountRunner

How it works...

正如我们从上一节中了解到的,Spring Boot 会在应用程序上下文创建期间自动处理来自 spring.factories 的所有配置类条目。在没有任何额外指导的情况下,使用 @Bean 注释进行注释的所有内容都将用于创建 Spring Bean。此功能实际上是普通旧 Spring Framework Java 配置的一部分。 Spring Boot 最重要的是能够有条件地控制何时应该执行某些 @Configuration@Bean 注释的规则以及何时最好忽略它们。

在我们的例子中,我们使用 @ConditionalOnMissingBean 注释来 instruct Spring Boot仅当没有其他 bean 与已在其他地方声明的类类型或 bean 名称匹配时,才创建我们的 DbCountRunner bean。因为我们在 BookPubApplication 配置中为 DbCountRunner 显式创建了一个 @Bean 条目,这优先并导致 OnBeanCondition 检测到 bean 的存在;从而指示 Spring Boot 在应用程序上下文设置期间不要使用 DbCountAutoConfiguration

 

Using custom @Enable annotations to toggle configuration


允许 Spring Boot 自动 评估类路径和检测到的配置 可以让一个简单的应用程序运行起来非常快速和容易。但是,有时我们想要提供配置类,但需要启动库的使用者显式启用这样的配置,而不是依赖 Spring Boot 自动决定是否应该包含它。

我们将修改我们之前的配方,使启动器通过元注释启用,而不是使用 spring.factories 路由。

How to do it...

  1. First, we will comment out the content of the spring.factories file located in db-count-starter/src/main/resources in the root of our project, as follows:
#org.springframework.boot.autoconfigure.EnableAutoConfiguration=
#com.example.bookpubstarter.dbcount.DbCountAutoConfiguration
  1. Next, we will need to create the meta-annotation. We will create a new file named EnableDbCounting.java in the db-count-starter/src/main/java/com/example/bookpubstarter/dbcount directory in the root of our project with the following content:
@Target(ElementType.TYPE) 
@Retention(RetentionPolicy.RUNTIME) 
@Import(DbCountAutoConfiguration.class) 
@Documented 
public @interface EnableDbCounting { 
} 
  1. We will now add the @EnableDbCounting annotation to our BookPubApplication class and also remove the dbCountRunner(...) method from it, as shown in the following snippet:
@SpringBootApplication 
@EnableScheduling 
@EnableDbCounting 
public class BookPubApplication { 
 
  public static void main(String[] args) { 
    SpringApplication.run(BookPubApplication.class, args); 
  } 
 
  @Bean 
  public StartupRunner schedulerRunner() { 
    return new StartupRunner(); 
  } 
} 
  1. Start the application by running ./gradlew clean bootRun.

How it works...

运行应用程序后,您可能注意到的第一件事是打印的计数都显示为 0,即使 StartupRunner图书数量:1 打印到控制台,如下输出所示:

c.e.b.dbcount.DbCountRunner         : AuthorRepository has 0 entriesc.e.b.dbcount.DbCountRunner         : BookRepository has 0 entriesc.e.b.dbcount.DbCountRunner         : PublisherRepository has 0 entriesc.e.b.dbcount.DbCountRunner         : ReviewerRepository has 0 entriescom.example.bookpub.StartupRunner   : Welcome to the Book Catalog System!com.example.bookpub.StartupRunner   : Number of books: 1

这是因为 Spring Boot 是 randomly 执行 CommandLineRunners 并且,当我们改变使用 @EnableDbCounting 注释的配置,它在 BookPubApplication 类本身的配置之前得到处理。由于数据库填充是由我们在 StartupRunner.run(...) 方法和 DbCountRunner.run(... ) 发生在此之前,数据库表没有数据,因此报告 0 计数。

如果我们想强制执行顺序,Spring 使用 @Order 注释为我们提供了这种能力。让我们用 @Order(Ordered.LOWEST_PRECEDENCE - 15) 注释 StartupRunner 类。由于 LOWEST_PRECEDENCE 是分配的默认顺序,我们将确保 StartupRunner 将在 DbCountRunner 通过稍微减少订单号。让我们再次运行该应用程序,现在我们将看到计数已正确显示。

现在这个小小的排序问题已经过去了,让我们检查我们对@做了什么EnableDbCounting 注释更详细一点。

如果没有包含配置的 spring.factories,Spring Boot 并不真正知道在应用程序上下文创建期间应该包含 DbCountAutoConfiguration 类.默认情况下,配置组件扫描将仅从 BookPubApplication 包及以下包中查找。由于包不同——com.example.bookpubcom.example.bookpubstarter.dbcount——扫描器不会选择它了。

这就是我们新创建的元注释发挥作用的地方。在@EnableDbCounting注解中,有一个键嵌套注解,@Import(DbCountAutoConfiguration.class),它使事情发生.这是 Spring 提供的一个注解,它可以用来注解其他注解,其中声明了应该在流程中导入哪些配置类。通过使用 @EnableDbCounting 注释我们的 BookPubApplication 类,我们传递地告诉 Spring 它应该包含 DbCountAutoConfiguration 也是应用程序上下文的一部分。

使用方便的元注释、spring.factories 和条件 bean 注释,我们现在可以创建复杂而精细的自定义自动配置 Spring Boot 启动器,以满足我们企业的需求。