vlambda博客
学习文章列表

读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》应用程序打包和部署

Chapter 25. Application Packaging and Deployment

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

  • Creating a Spring Boot executable JAR
  • Creating Docker images
  • Building self-executing binaries
  • Spring Boot environment configuration, hierarchy, and precedence
  • Adding a custom PropertySource to the environment using EnvironmentPostProcessor
  • Externalizing an environmental configuration using property files
  • Externalizing an environmental configuration using environment variables
  • Externalizing an environmental configuration using Java system properties
  • Externalizing an environmental configuration using JSON
  • Setting up Consul
  • Externalizing an environmental configuration using Consul and envconsul

Introduction


除非正在使用,否则应用程序有什么用?在当今时代——当 DevOps 已成为进行软件开发的方式时,当云为王时,当构建微服务被认为是要做的事情时——很多注意力都集中在应用程序如何打包、分发、并部署在他们指定的环境中。

十二因素应用程序方法发​​挥在定义如何 现代 软件即服务 (SaaS ) 应用程序应该构建 并部署。关键原则之一是将环境配置定义与环境中的应用和存储分开。 Twelve-Factor App 方法论还支持依赖项的隔离和捆绑、开发与生产的对等,以及应用程序的易于部署和可处置性等。

Note

十二因子应用方法可以在 http://12factor.net/ 找到

DevOps 模型还鼓励我们对我们的应用程序拥有完全的所有权,从编写和测试代码一直到构建和部署它。如果我们要承担这种所有权,我们需要确保维护和间接费用不会过多,并且不会占用我们开发新功能的主要任务的太多时间。这可以通过拥有干净、定义明确和隔离的可部署工件来实现,这些工件是自包含、自执行的,并且可以部署在任何环境中而无需重新构建。

以下秘籍将引导我们完成所有必要的步骤,以实现轻松部署和维护的目标,同时拥有干净优雅的代码。

Creating a Spring Boot executable JAR


Spring 如果不提​​供一种很好的方式来打包整个应用程序(包括其所有依赖项、资源、等等在一个复合的可执行 JAR 文件中。创建 JAR 文件后,只需运行 java -jar <name>.jar 命令即可启动它。

我们将继续使用我们在前几章中构建的应用程序代码,并将添加必要的功能来打包它。让我们继续看看如何创建 Spring Boot Uber JAR。

Note

Uber JAR 通常被称为封装在单个复合 JAR 文件中的应用程序包,该文件内部包含一个 /lib 目录,其中包含所有依赖的内部 jar 和可选的 /bin 目录和可执行文件。

How to do it...

  1. Let's go to our code directory from Chapter 24, Application Testing, and execute ./gradlew clean build
  2. With the Uber JAR built, let's launch the application by executing java -jar build/libs/ch6-0.0.1-SNAPSHOT.jar
  3. This will result in our application running in the JAR file with the following console output:
  .   ____          _            __ _ _ / / ___'_ __ _ _(_)_ __  __ _    ( ( )___ | '_ | '_| | '_ / _` |     /  ___)| |_)| | | | | || (_| |  ) ) ) )  '  |____| .__|_| |_|_| |___, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot ::  (v2.0.0.BUILD-SNAPSHOT)...(The rest is omitted for conciseness)...2017-12-17 INFO: Registering beans for JMX exposure on startup2017-12-17 INFO: Tomcat started on port(s): 8080 (http) 8443  
    (https)2017-12-17 INFO: Welcome to the Book Catalog System!2017-12-17 INFO: BookRepository has 1 entries2017-12-17 INFO: ReviewerRepository has 0 entries2017-12-17 INFO: PublisherRepository has 1 entries2017-12-17 INFO: AuthorRepository has 1 entries2017-12-17 INFO: Started BookPubApplication in 12.156 seconds (JVM 
    running for 12.877)2017-12-17 INFO: Number of books: 1

How it works...

如您所见,获取 packaged 可执行 JAR 文件相当简单。所有的魔法都已经编码并作为 Spring Boot Gradle 插件的一部分提供给我们。插件的添加增加了许多任务,允许我们打包 Spring Boot 应用程序、运行它并构建 JAR、TAR、WAR 文件等。例如,我们在本书中一直使用的 bootRun 任务是由 Spring Boot Gradle 插件等提供的。我们可以通过执行 ./gradlew tasks 来查看可用的 Gradle 任务的完整列表。当我们运行这个命令时,我们将得到以下输出:

------------------------------------------------------------All tasks runnable from root project------------------------------------------------------------Application tasks-----------------bootRun - Run the project with support for auto-detecting main 
    class and reloading static resourcesrun - Runs this project as a JVM applicationBuild tasks-----------assemble - Assembles the outputs of this project.bootJar - Assembles an executable jar archive containing the main 
    classes and their dependencies.build - Assembles and tests this project.buildDependents - Assembles and tests this project and all projects 
    that depend on it.buildNeeded - Assembles and tests this project and all projects it 
    depends on.classes - Assembles classes 'main'.clean - Deletes the build directory.jar - Assembles a jar archive containing the main classes.testClasses - Assembles classes 'test'.Build Setup tasks-----------------init - Initializes a new Gradle build. [incubating]Distribution tasks------------------assembleBootDist - Assembles the boot distributionsassembleDist - Assembles the main distributionsbootDistTar - Bundles the project as a distribution.bootDistZip - Bundles the project as a distribution.distTar - Bundles the project as a distribution.distZip - Bundles the project as a distribution.installBootDist - Installs the project as a distribution as-is.installDist - Installs the project as a distribution as-is.

前面的输出不完整;我已经排除了不相关的任务组,例如 IDE、文档等,但您会在控制台上看到它们。在任务列表中,我们会看到bootRunbootJar等任务。这些任务已由 Spring Boot Gradle 插件添加,执行它们会将所需的 Spring Boot 步骤添加到构建管道中。执行 ./gradlew tasks --all 可以看到实际的任务依赖,不仅打印可见任务,还打印依赖的内部任务和任务依赖关系。例如,当我们运行 build 任务时,以下所有依赖任务也被执行:

build - Assembles and tests this project. [assemble, check]assemble - Assembles the outputs of this project. [bootJar, 
    distTar, distZip, jar]

可以看到 build 任务会执行 assemble 任务,而后者又会调用 bootJar,实际上是在其中创建 Uber JAR。

该插件还提供了许多非常有用的配置选项。虽然我不打算详细介绍所有这些,但我会提到我认为非常有用的两个:

bootJar { 
  classifier = 'exec' 
  baseName = 'bookpub' 
} 

此配置允许我们指定可执行 JAR 文件 classifier 以及 JAR baseName,允许常规 JAR 仅包含应用程序代码和名称中带有 classifier 的可执行 JAR,bookpub-0.0.1-SNAPSHOT-exec.jar

另一个有用的配置选项允许我们指定哪些依赖 JAR 需要解包,因为由于某种原因,它们不能作为嵌套的内部 JAR 包含在内。当您需要系统 Classloader 中可用的东西时,这非常方便,例如通过启动系统设置自定义 SecurityManager特性:

bootJar { 
  requiresUnpack = '**/some-jar-name-*.jar' 
} 

在此示例中,当应用程序启动时,some-jar-name-1.0.3.jar 依赖项的内容将被解压缩到文件系统上的临时文件夹中。

Creating Docker images


码头工人,码头工人,码头工人!在我参加的所有会议和技术聚会中,我听到这个短语越来越多。 Docker的到来受到了社区张开双臂的欢迎,并立即成为热门话题。 Docker 生态系统一直在迅速扩展,许多其他公司提供服务、支持和补充框架,例如 Apache Mesos、 Amazon Elastic Beanstalk、ECS和 Kubernetes,仅举几例。甚至微软也在其 Azure 云服务中提供 Docker 支持,并与 Docker 合作将 Docker 引入 Windows 操作系统。

Docker 大受欢迎的原因在于它能够以自包含容器的形式打包和部署应用程序。容器比传统的成熟虚拟机更轻量级。它们中的多个可以在单个操作系统实例之上运行,因此与传统 VM 相比,可以在相同硬件上部署的应用程序数量增加。

在这个秘籍中,我们将了解如何将 Spring Boot 应用程序打包为 Docker 镜像,以及如何部署和运行它。

构建一个 Docker 镜像并在你的开发机器上运行它是可行的,但不如与世界分享它有趣。您需要将它发布到某个地方才能部署,尤其是当您考虑将它与 Amazon 或其他类似云的环境一起使用时。幸运的是,Docker 不仅为我们提供了容器解决方案,还为我们提供了一个存储库服务,Docker Hub,位于 https://hub.docker.com, 我们可以在其中创建存储库并发布我们的 Docker 镜像。所以把它想象成 Docker 的 Maven Central。

How to do it...

  1. The first step will be to create an account on Docker Hub so that we can publish our images. Go to https://hub.docker.com and create an account. You can also use your GitHub account and log in using it if you have one.
  2. Once you have an account, we will need to create a repository named springbootcookbook.
  3. With this account created, now is the time to build the image. For this, we will use one of the Gradle Docker plugins. We will start by changing build.gradle to modify the buildscript block with the following change:
buildscript { 
  dependencies { 
    classpath("org.springframework.boot:spring-boot-gradle- 
      plugin:${springBootVersion}") 
    classpath("se.transmode.gradle:gradle-docker:1.2") 
  } 
} 
  1. We will also need to apply this plugin by adding the apply plugin: 'docker' directive to the build.gradle file.
  2. We also need to explicitly add the application plugin to build.gradle as well, since it is no longer automatically included by the Spring Boot Gradle plugin.
  3. Add apply plugin: 'application' to the list of plugins in the build.gradle file.
  4. Lastly, we will need to add the following Docker configuration to the build.gradle file as well:
task distDocker(type: Docker,  
                overwrite: true,  
                dependsOn: bootDistTar) { 
    group = 'docker' 
    description = "Packs the project's JVM application
    as a Docker image." 
 
    inputs.files project.bootDistTar 
    def installDir = "/" + project.bootDistTar.archiveName  
                         - ".${project.bootDistTar.extension}" 
 
    doFirst { 
        tag "ch6" 
        push false 
        exposePort 8080 
        exposePort 8443 
        addFile file("${System.properties['user.home']}
        /.keystore"), "/root/" 
        applicationName = project.applicationName 
        addFile project.bootDistTar.outputs.files.singleFile 
 
        entryPoint = ["$installDir/bin/${project.applicationName}"] 
    } 
} 
  1. Assuming that you already have Docker installed on your machine, we can proceed to creating the image by executing ./gradlew clean distDocker.
  2. For Docker installation instructions, please visit the tutorial that is located at https://docs.docker.com/installation/#installation. If everything has worked out correctly, you should see the following output:
> Task :distDocker
Sending build context to Docker daemon  68.22MB
  Step 1/6 : FROM aglover/java8-pier
   ---> 3f3822d3ece5
  Step 2/6 : EXPOSE 8080
   ---> Using cache
   ---> 73717aaca6f3
  Step 3/6 : EXPOSE 8443
   ---> Using cache
   ---> 6ef3c0fc3d2a
  Step 4/6 : ADD .keystore /root/
   ---> Using cache
   ---> 6efebb5a868b
  Step 5/6 : ADD ch6-boot-0.0.1-SNAPSHOT.tar /
   ---> Using cache
   ---> 0634eace4952
  Step 6/6 : ENTRYPOINT /ch6-boot-0.0.1-SNAPSHOT/bin/ch6
   ---> Using cache
   ---> 39a853b7ddbb
  Successfully built 39a853b7ddbb
  Successfully tagged ch6:0.0.1-SNAPSHOT
    
    
  BUILD SUCCESSFUL
  Total time: 1 mins 0.009 secs.
    
  
  1. We can also execute the following Docker images command so as to see the newly created image:
$ docker imagesREPOSITORY           TAG                IMAGE ID         CREATED             VIRTUAL  SIZEch6                  0.0.1-SNAPSHOT     39a853b7ddbb     17 minutes ago      1.04 GBaglover/java8-pier   latest             69f4574a230e     11 months ago       1.01 GB
  1. With the image built successfully, we are now ready to start it in Docker by executing the following command:
docker run -d -P ch6:0.0.1-SNAPSHOT.
  1. After the container has started, we can query the Docker registry for the port bindings so that we can access the HTTP endpoints for our service. This can be done via the docker ps command. If the container is running successfully, we should see the following result (names and ports will vary):
CONTAINER ID        IMAGE               COMMAND               
    CREATED             STATUS              PORTS                                                  
    NAMES37b37e411b9e        ch6:latest         "/ch6-boot-0.0.1-S..." 
    10 minutes ago      Up 10 minutes       0.0.0.0:32778-
    >8080/tcp,      0.0.0.0:32779->8443/tcp   drunk_carson
  1. From this output, we can tell that the port mapping for the internal port 8080 has been set up to be 32778 (your port will vary for every run). Let's open http://localhost:32778/books in the browser to see our application in action, as shown in the following screenshot:
读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》应用程序打包和部署

Note

如果您使用带有 boot2docker 的 macOS X,那么您将不会在本地运行 Docker 容器。在这种情况下,您将使用 boot2docker ip 而不是本地主机来连接到应用程序。有关如何使 boot2docker 集成更容易的更多提示,请访问http://viget.com/extend/how-to-use-docker-on-os-x-the-missing-指南。还可以使用由 Ian Sinnott 慷慨创建的漂亮 Docker 外观,它会自动启动 boot2docker 并处理环境变量。要获取包装器,请转到 https://gist.github.com/iansinnott/0a0c212260386bdbfafb

How it works...

在前面的示例中,我们看到让我们的 build 将应用程序打包到 Docker 容器。额外的 Gradle-Docker 插件完成了 Dockerfile 创建、镜像构建和发布的大部分工作;我们所要做的就是给它一些关于我们想要图像的内容和方式的说明。因为 Spring Boot Gradle 插件使用 boot 发行版,Gradle-Docker 插件不知道它需要使用引导的 TAR 存档。为了解决这个问题,我们重写了 distDocker 任务。让我们详细检查这些说明:

  • The group and description attributes merely help with displaying the task properly when the ./gradlew tasks command is executed.
  • The inputs.files project.bootDistTar directive is very important. This is what instructs the distDocker task to use the TAR archive created by the Spring Boot distribution, instead of the generic one.
  • The def installDir = "/" + project.bootDistTar.archiveName - ".${project.bootDistTar.extension}" directive is creating a variable, containing the directory where the untarred artifacts will be placed inside the Docker container.
  • The exposePort directive tells the plugin to add an EXPOSE <port> instruction to the Dockerfile so that when our container is started, it will expose these internal ports to the outside via port mapping. We saw this mapping while running the docker ps command.
  • The addFile directive tells the plugin to add an ADD <src> <dest> instruction to the Dockerfile so that when the container is being built, we will copy the file from the source filesystem in the filesystem in the container image. In our case, we will need to copy the .keystore certificate file that we configured in one of our previous recipes for the HTTPS connector, which we instructed in tomcat.https.properties to be loaded from ${user.home}/.keystore. Now, we need it to be in the /root/ directory directory as, in the container, our application will be executed under the root. (This can be changed with more configurations.)

Note

Gradle-Docker 插件默认使用项目名称作为镜像的名称。反过来,项目名称是 Gradle 从项目的目录名称中推断出来的,除非配置了显式的属性值。由于代码示例适用于 Chapter 25应用程序打包和部署 项目目录被命名为ch25,因此是图像的名称。项目名称可以通过在 gradle.properties 中添加 name='some_project_name' 来显式配置。

如果查看生成的 Dockerfile,该文件位于项目根目录的 build/docker/ 目录中,您将看到以下两条指令:

ADD ch25-boot-0.0.1-SNAPSHOT.tar / 
ENTRYPOINT ["/ch6-boot-0.0.1-SNAPSHOT/bin/ch6"] 

ADD 指令添加由 bootDistTar 任务生成的 TAR 应用程序存档,并包含捆绑为 tarball 的应用程序。我们甚至可以通过执行 tar tvf build/distributions/ch6-boot-0.0.1-SNAPSHOT.tar来查看生成的tarball的内容。在容器的构建过程中,TAR 文件的内容会被提取到容器的 / 目录中,然后用于启动应用程序。

其后是 ENTRYPOINT 指令。这告诉 Docker 执行 /ch6-boot-0.0.1-SNAPSHOT/bin/ ch6,我们将其视为 tarball 内容的一部分,一旦容器启动,就会自动启动我们的应用程序。

Dockerfile中的第一行,即FROM aglover/java8-pier,是使用aglover/java8-pier 镜像,其中包含安装了 Java 8 的 Ubuntu 操作系统作为我们容器的基础镜像,我们将在其上安装我们的应用程序。此映像来自 Docker Hub 存储库,由插件自动使用,但如果需要,可以通过配置设置进行更改。

如果您在 Docker Hub 上创建了一个帐户,我们还可以将创建的 Docker 镜像发布到注册表。作为公平的警告,生成的图像可能有数百兆字节大小,因此上传可能需要一些时间。要发布此图像,我们需要将标签更改为 tag "<docker hub 用户名>/<docker hub 存储库名称>" 并添加 push true 设置到 build.gradle 中的 distDocker 任务定义:

task distDocker(type: Docker,  
                overwrite: true,  
                dependsOn: bootDistTar) { 
    ... 
    doFirst { 
        tag "<docker hub username>/<docker hub repository name>" 
        push true 
        ... 
    } 
} 

tag 属性设置创建的图像标签,默认情况下,插件假定它驻留在 Docker 集线器存储库。如果 push 配置设置为 true,它将在此处发布它,就像我们的例子一样。

Note

有关所有 Gradle-Docker 插件配置选项的完整列表,请查看 < span>https://github.com/Transmode/gradle-docker GitHub 项目页面。

启动 Docker 映像时,我们使用 -d-P 命令行参数。它们的用途如下:

  • -d: This argument indicates the desire to run the container in a detached mode where the process starts in the background
  • -P: This argument instructs Docker to publish all the internally exposed ports to the outside so that we can access them

Note

有关所有可能的命令行选项的详细说明,请参阅 https://docs.docker.com/reference/commandline/cli/

Building self-executing binaries


从 Spring Boot 1.3 版开始,Gradle 和 Maven 插件支持生成真正的可执行二进制文件的选项。这些看起来像普通的 JAR 文件,但 JAR 的内容与包含命令构建逻辑的启动脚本融合在一起,并且能够自行启动而无需执行 java - jar file.jar 命令显式。此功能非常方便,因为它允许轻松配置 Linux 自动启动服务,例如 init.dsystemd,以及launchd 在 macOS X 上。

Getting ready

对于这个秘籍,我们将使用我们现有的应用程序构建。我们将研究如何创建自启动可执行 JAR 文件以及如何修改默认启动脚本以添加对自定义 JVM 启动参数的支持,例如 -D start设置系统属性、JVM 内存、垃圾收集和其他设置。

对于这个秘籍,确保 build.gradle 使用 Spring Boot 2.0.0 或更高版本。如果不是,则在 buildscript 配置块中更改以下设置:

ext { 
  springBootVersion = '2.0.0.BUILD-SNAPSHOT' 
} 

Spring Boot 版本的相同升级也应该在 db-counter-starter/build.gradle 文件中完成。

How to do it...

  1. Building a default self-executing JAR file is very easy; actually, it is done automatically once we execute the ./gradlew clean bootJar command.
  2. We can proceed to launch the created application simply by invoking ./build/libs/bookpub-0.0.1-SNAPSHOT.jar.
  1. In an enterprise environment, it is rare that we are satisfied with the default JVM launch arguments as we often need to tweak the memory settings, GC configurations, and even pass the startup system properties in order to ensure that we are using the desired version of the XML parser or a proprietary implementation of class loader or security manager. To accomplish those needs, we will modify the default launch.script file to add support for the JVM options. Let's start by copying the default launch.script file from the https://github.com/spring-projects/spring-boot/blob/master/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/resources/org/springframework/boot/loader/tools/launch.script Spring Boot GitHub repository in the root of our project.

launch.script 文件仅在 Linux 和 OS X 环境中受支持。如果您希望为 Windows 制作自执行 JAR,则需要提供您自己的 launch.script 文件,该文件专为 Windows shell 命令执行而定制。好消息是它是唯一需要的特殊东西。本秘籍中的所有说明和概念也可以在 Windows 上正常运行,前提是兼容的 launch.script 模板。

  1. We will modify the copied launch.script file and add the following content right above the line 142 mark (this is showing only the relevant part of the script so as to condense the space):
...
# Find Java
if [[ -n "$JAVA_HOME" ]] && [[ -x "$JAVA_HOME/bin/java" ]]; then
javaexe="$JAVA_HOME/bin/java"
elif type -p java 2>&1> /dev/null; then
javaexe=java
elif [[ -x "/usr/bin/java" ]]; then
javaexe="/usr/bin/java"
else
echo "Unable to find Java"
exit 1
fi
# Configure JVM Options
jvmopts="{{jvm_options:}}"
arguments=(-Dsun.misc.URLClassPath.disableJarChecking=true $jvmopts $JAVA_OPTS -jar $jarfile $RUN_ARGS "$@")
# Action functions
start() {
...
  1. With the custom launch.script file in place, we will need to add the options setting to our build.gradle file with the following content:
applicationDefaultJvmArgs = [ 
    "-Xms128m", 
    "-Xmx256m" 
] 
 
bootJar { 
    classifier = 'exec' 
    baseName = 'bookpub' 
    launchScript { 
        script = file('launch.script') 
        properties 'jvm_options' : applicationDefaultJvmArgs.join(' ') 
    } 
} 
  1. We are now ready to launch our application. First, let's use the ./gradlew clean bootRun command, and if we look at the JConsole VM Summary tab, we will see that our arguments indeed have been passed to the JVM, as follows:
读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》应用程序打包和部署
  1. We can also build the self-starting executable JAR by running the ./gradlew clean bootJar command and then executing ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar in order to launch our application. We should expect to see a similar result in JConsole.
  1. Alternatively, we can also use the JAVA_OPTS environment variable to override some of the JVM arguments. Say we want to change the minimum memory heap size to 128 megabytes. We can launch our application using the JAVA_OPTS=-Xmx128m ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar command and this would show us the following effect in JConsole:
读书笔记《developing-java-applications-with-spring-and-spring-boot-ebook》应用程序打包和部署

How it works...

通过对 launch.script 的小定制,我们能够create一个自执行的可部署应用程序,打包为一个独立的 JAR 文件,除此之外,还可以配置它,以便使用各种特定于操作系统的自动启动框架启动。

Spring Boot Gradle 和 Maven 插件为我们提供了许多参数自定义选项,甚至可以在 launch.script 中嵌入类似 mustache 的模板占位符,以后可以将其替换为构建期间的值。我们利用此功能将我们的 JVM 参数注入到使用 launchScript{properties} 配置设置的文件中。

在我们自定义版本的 launch.script 中,我们添加了 jvmopts="{{jvm_options:}}" 行,它将在构建和打包期间被 jvm_options 参数的值替换。此参数在我们的 build.gradle 文件中声明为 launchScript.properties 参数 :launchScript{properties 'jvm_options':applicationDefaultJvmArgs.join('')}

JVM 参数可以是硬编码的,但最好在我们的应用程序如何开始使用 bootRun 任务和它从自执行 JAR 启动时如何启动之间保持一致。为此,我们将使用与 arguments 相同的 applicationDefaultJvmArgs 集合我们将为 bootRun 执行目的进行定义,仅将所有不同的参数折叠在由空格分隔的单行文本中。使用这种方法,我们只需定义一次 JVM 参数,并在两种执行模式下使用它们。

Note

需要注意的是,这种重用也适用于使用 Gradle 定义的 distZipdistTar 任务构建的应用程序分发。 application 插件,以及 Spring Boot Gradle 的 bootDistZipbootDistTar

我们可以通过启动我们的自执行 JAR 而不是默认情况下 distTar 任务生成的 TAR 文件的内容来修改构建以创建 Docker 映像。为此,我们需要使用以下代码更改我们的 distDocker 配置块:

task distDocker(type: Docker, overwrite: true, 
                dependsOn: bootJar) { 
  ... 
  inputs.files project.bootJar 
  doFirst { 
    ... 
    addFile file("${System.properties['user.home']}/.keystore"), 
       "/root/" 
    applicationName = project.applicationName 
    addFile project.bootJar.outputs.files.singleFile 
 
    def executableName = "/" + 
       project.bootJar.outputs.files.singleFile.name 
    entryPoint = ["$executableName"] 
  } 
} 

这将使我们的 distDocker 任务将可执行 jar 放入 Docker 映像而不是 TAR 存档中。

Spring Boot environment configuration, hierarchy, and precedence


在前面的几个秘籍中,我们研究了如何以各种方式打包我们的应用程序以及如何部署它。下一个合乎逻辑的步骤是需要配置应用程序以提供一些行为控制以及一些特定于环境的配置值,这些值可能而且很可能会因环境而异。

这种 environmental 配置的常见示例 difference 是数据库设置。我们当然不想通过在我们的开发机器上运行的应用程序连接到生产环境数据库。在某些情况下,我们希望应用程序以不同的模式运行或使用不同的配置文件集,正如 Spring 所指的那样。例如,在实时或模拟器模式下运行 application

对于这个秘籍,我们将从代码库的先前状态开始,添加对不同配置文件的支持,并检查如何将属性值用作其他属性中的占位符。

How to do it...

  1. We will start by adding an @Profile annotation to the @Bean creation of schedulerRunner by changing the definition of the schedulerRunner(...) method in BookPubApplication.java, located in the src/main/java/org/test/bookpub directory at the root of our project, to the following content:
@Bean 
@Profile("logger") 
public StartupRunner schedulerRunner() { 
    return new StartupRunner(); 
} 
  1. Start the application by running ./gradlew clean bootRun.
  2. Once the application is running, we should no longer see the previous log output from the StartupRunner class, which looked like this:
2017-12-17 --- org.test.bookpub.StartupRunner : Number of books: 1
  1. Now, let's build the application by running ./gradlew clean bootJar and start it by running ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger; we will see the log output line show up again.
  2. Another functionality that is enabled by the profile selector is the ability to add profile-specific property files. Let's create an application-inmemorydb.properties file in the src/main/resources directory at the root of our project with the following content:
spring.datasource.url = jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE 
  1. Let's build the application by running ./gradlew clean bootJar and start it by running ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger,inmemorydb, which will use the inmemorydb profile configuration in order to use the in-memory database instead of the file-based one.

How it works...

在这个秘籍中,我们实验使用配置文件并根据活动配置文件应用额外的配置设置。 Profiles 最初是在 Spring Framework 3.2 中引入的,用于在上下文中有条件地配置 bean,具体取决于哪些配置文件处于活动状态。在 Spring Boot 中,此功能被进一步扩展以允许配置分离。

通过在我们的 StartupRunner@Bean 创建方法上放置一个 @Profile("logger") 注解,Spring 将被指示创建仅当记录器配置文件已被激活时 bean。通常,这是通过在应用程序启动期间在命令行中传递 --spring.profiles.active 选项来完成的。在测试中,另一种方法是在 Test 类上使用 @ActiveProfiles("profile") 注释,但不支持执行普通应用程序。也可以否定配置文件,例如 @Profile("!production")。当使用这样的注释时(用 ! 标记否定),只有在没有配置文件生产处于活动状态时才会创建 bean。

在启动期间,Spring Boot 将通过命令行传递的所有选项视为应用程序属性,因此在启动期间传递的任何内容最终都将作为能够使用的属性值。这种相同的机制不仅适用于新属性,还可以用作覆盖现有属性的一种方式。假设我们已经在 application.properties 文件中定义了一个活动配置文件,如下所示: spring.profiles.active=basic 。通过命令行传递 --spring.profiles.active=logger 选项,我们将从 basic 替换活动配置文件到 logger。如果我们想包含一些配置文件而不考虑活动配置,Spring Boot 为我们提供了一个 spring.profiles.include 选项来配置。以这种方式设置的任何配置文件都将添加到活动配置文件列表中。

由于这些选项只不过是 regular Spring Boot 应用程序属性,因此它们都遵循相同的覆盖优先级层次结构。选项概述如下:

  • Command-line arguments: These values supersede every other property source in the list, and you can always rest assured that anything passed via --property.name=value will take precedence over the other means.
  • JNDI attributes: They are the next in precedence priority. If you are using an application container that provides data via a JNDI java:comp/env namespace, these values will override all the other settings from below.
  • Java system properties: These values are another way to pass the properties to the application either via the -Dproperty=name command-line arguments or by calling System.setProperty(...) in the code. They provide another way to replace the existing properties. Anything coming from System.getProperty(...) will win over the others in the list.
  • OS environment variables: Whether from Windows, Linux, OS X, or any other, they are a common way to specify a configuration, especially for locations and values. The most notable one is JAVA_HOME, which is a common way to indicate where the JVM location resides in the filesystem. If neither of the preceding settings are present, the ENV variables will be used for the property values instead of the ones mentioned as follows:

Note

由于操作系统环境变量通常不支持点 (.) 或破折号 (-),因此 Spring Boot 提供了自动重新映射在属性评估期间将下划线 (_) 替换为点 (.) 的机制;它还处理大小写转换。因此,JAVA_HOME成为java.home的同义词。

  • random.*: This provides special support for the random values of primitive types that can be used as placeholders in configuration properties. For example, we can define a property named some.number=${random.int} where ${random.int} will be replaced by some random integer value. The same goes for ${random.value} for textual values and ${random.long} for longs.
  • application-{profile}.properties: They are the profile-specific files that get applied only if a corresponding profile gets activated.
  • application.properties: They are the main property files that contain the base/default application configuration. Similar to the profile-specific ones, these values can be loaded from the following list of locations, with the top one taking priority over the lower entries:
    • file:config/: This is a /config directory located in the current directory:
    • file:: This is the current directory
    • classpath:/config: This is a /config package in the classpath
    • classpath:: This is a root of the classpath
  • @Configuration annotated classes annotated with @PropertySource: These are any in-code property sources that have been configured using annotations. We have seen an example of such usage the Adding custom connectors recipe from Chapter 22, Web Framework Behavior Tuning. They are very low in the precedence chain and are only preceded by the default properties.
  • Default properties: They are configured via the SpringApplication.setDefaultProperties(...) call and are seldom used, as it feels very much like hardcoding values in code instead of externalizing them in configuration files.

 

 

Adding a custom PropertySource to the environment using EnvironmentPostProcessor


如果企业已经使用特定的配置 系统,自定义编写或现成的,Spring Boot 为我们提供了通过 creationPropertySource 实现的“indexterm">

How to do it...

假设我们有一个现有的配置设置,它使用流行的 Apache Commons 配置框架并将配置数据存储在 XML 文件中:

  1. To mimic our supposed pre-existing configuration system, add the following content to the dependencies section in the build.gradle file:
dependencies { 
  ... 
  compile project(':db-count-starter') 
  compile("commons-configuration:commons-
     configuration:1.10") 
  compile("commons-codec:commons-codec:1.6") 
  compile("commons-jxpath:commons-jxpath:1.3") 
  compile("commons-collections:commons-collections:3.2.1") 
  runtime("com.h2database:h2") 
  ... 
} 
  1. Follow this up by creating a simple configuration file named commons-config.xml in the src/main/resources directory at the root of our project with the following content:
<?xml version="1.0" encoding="ISO-8859-1" ?> 
<config> 
  <book> 
    <counter> 
      <delay>1000</delay> 
      <rate>${book.counter.delay}0</rate> 
    </counter> 
  </book> 
</config> 
  1. Next, we will create the PropertySource implementation file named ApacheCommonsConfigurationPropertySource.java in the src/main/java/org/test/bookpub directory at the root of our project with the following content:
public class ApacheCommonsConfigurationPropertySource 
   extends EnumerablePropertySource<XMLConfiguration> { 
  private static final Log logger = LogFactory.getLog(
   ApacheCommonsConfigurationPropertySource.class); 
 
  public static final String 
     COMMONS_CONFIG_PROPERTY_SOURCE_NAME = "commonsConfig"; 
 
  public ApacheCommonsConfigurationPropertySource(
     String name, XMLConfiguration source) { 
    super(name, source); 
  } 
 
  @Override 
  public String[] getPropertyNames() { 
    ArrayList<String> keys = 
       Lists.newArrayList(this.source.getKeys()); 
    return keys.toArray(new String[keys.size()]); 
  } 
 
  @Override 
  public Object getProperty(String name) { 
    return this.source.getString(name); 
  } 
 
  public static void addToEnvironment(
     ConfigurableEnvironment environment, XMLConfiguration 
       xmlConfiguration) { 
    environment.getPropertySources().addAfter(
      StandardEnvironment.
        SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, new 
          ApacheCommonsConfigurationPropertySource( 
           COMMONS_CONFIG_PROPERTY_SOURCE_NAME, 
             xmlConfiguration)); 
    logger.trace("ApacheCommonsConfigurationPropertySource 
      add to Environment"); 
  } 
} 
  1. We will now create the EnvironmentPostProcessor implementation class so as to bootstrap our PropertySource named ApacheCommonsConfigurationEnvironmentPostProcessor.java in the src/main/java/org/test/bookpub directory at the root of our project with the following content:
package com.example.bookpub;

import org.apache.commons.configuration.ConfigurationException;
import org.apache.commons.configuration.XMLConfiguration;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.env.ConfigurableEnvironment;

public class ApacheCommonsConfigurationEnvironmentPostProcessor  
       implements EnvironmentPostProcessor {

    @Override
    public void postProcessEnvironment( 
                   ConfigurableEnvironment environment,  
                   SpringApplication application) {
        try {
            ApacheCommonsConfigurationPropertySource 
               .addToEnvironment(environment,
                    new XMLConfiguration("commons- 
                                         config.xml"));
        } catch (ConfigurationException e) {
            throw new RuntimeException("Unable to load commons-config.xml", e);
        }
    }
} 
  1. Finally, we will need to create a new directory named META-INF in the src/main/resources directory at the root of our project and create a file named spring.factories in it with the following content:
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=
com.example.bookpub.ApacheCommonsConfigurationEnvironmentPostProcessor 

 

  1. With the setup done, we are now ready to use our new properties in our application. Let's change the configuration of the @Scheduled annotation for our StartupRunner class located in the src/main/java/org/test/bookpub directory at the root of our project, as follows:
@Scheduled(initialDelayString = "${book.counter.delay}", 
   fixedRateString = "${book.counter.rate}") 
  1. Let's build the application by running ./gradlew clean bootJar and start it by running ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger in order to ensure that our StartupRunner class is still logging the book count every ten seconds, as expected.

How it works...

在这个秘籍中,我们探索了如何添加我们自己的自定义PropertySource这使我们能够在 Spring Boot 环境中桥接现有系统。让我们来看看 inner 各个部分如何组合在一起的工作原理。

在上一节中,我们了解了不同的配置定义是如何叠加的,以及使用什么规则将它们叠加在一起。这将帮助我们更好地理解使用自定义 PropertySource 实现的 Apache Commons Configuration 的桥接是如何工作的。 (这不应与 @PropertySource 注释混淆!)

第 23 章中,编写自定义 Spring Boot Starters< /em>,我们了解了关于使用spring.factories< /code>,因此我们已经知道该文件用于定义 Spring Boot 在应用程序启动期间应自动合并的类。这次唯一的区别是,我们将配置 SpringApplicationRunListener 设置,而不是配置 EnableAutoConfiguration 设置。

 

 

我们创建了以下两个类来支持我们的需求:

  • ApacheCommonsConfigurationPropertySource: This is the extension of the EnumerablePropertySource base class that provides you with internal functionality in order to bridge XMLConfiguration from Apache Commons Configuration to the world of Spring Boot by providing transformation to get the specific property values by name via the getProperty(String name) implementation, and the list of all the supported property names via the getPropertyNames() implementation. In situations where you are dealing with the use case when the complete list of the available property names is not known or is very expensive to compute, you can just extend the PropertySource abstract class instead of using EnumerablePropertySource.
  • ApacheCommonsConfigurationEnvironmentPostProcessor: This is the implementation of the EnvironmentPostProcessor interface that gets instantiated by Spring Boot during the application startup and receives notification callback after the initial environment initialization has been completed, but before the application context startup. This class is configured in spring.factories and is automatically created by Spring Boot.

在我们的后处理器中,我们实现了 postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) 方法,它使我们能够访问 ConfigurableEnvironment实例。在调用此回调时,我们将获得一个环境实例,该实例已经填充了前面层次结构中的所有属性。但是,我们将有机会在列表中的任何位置注入我们自己的 PropertySource 实现,我们将在 ApacheCommonsConfigurationPropertySource.addToEnvironment(. ..) 方法。

在我们的例子中,我们将选择在 systemEnvironment 下面按优先级顺序插入我们的源代码,但如果需要,我们可以将此顺序更改为我们想要的任何最高优先级。请注意不要将其设置得太高,以至于无法通过命令行参数、系统属性或环境变量覆盖您的属性。

 

Externalizing an environmental configuration using property files


前面的秘籍向我们介绍了 application 属性以及它们的配置方式。正如本章开头提到的,在应用程序部署过程中,几乎不可避免地会有一些依赖于环境的属性值。它们可以是数据库配置、服务拓扑,甚至是简单的功能配置,其中可能会在开发中启用某些东西,但还没有为生产做好准备。

在这个秘籍中,我们将学习如何使用外部驻留的 properties 文件进行环境特定的配置,该配置可能驻留在本地文件系统或互联网上的野外。

在这个秘籍中,我们将使用与上一秘籍中使用的所有现有配置相同的应用程序。我们将使用它来尝试使用位于本地文件系统中的外部配置属性以及来自 Internet URL(例如 GitHub 或任何其他)的启动。

How to do it...

  1. Let's start by adding a bit of code to log the value of our particular configuration property so that we can easily see the change in it as we do different things. Add an @Bean method to the BookPubApplication class located in the src/main/java/org/test/bookpub directory at the root of our project with the following content:
@Bean 
public CommandLineRunner configValuePrinter(
   @Value("${my.config.value:}") String configValue) { 
  return args -> LogFactory.getLog(getClass()).
     info("Value of my.config.value property is: " + 
       configValue); 
} 
  1. Let's build the application by running ./gradlew clean bootJar and start it by running ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger so as to see the following log output:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of 
    my.config.value property is:
  1. The value is empty, as we expected. Next, we will create a file named external.properties in our home directly with the following content:
my.config.value=From Home Directory Config
  1. Let's run our application by executing ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger --spring.config.location=file:/home/<username>/external.properties in order to see the following output in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From Home Directory Config

Note

对于 macOS 用户,可以在 /Users/ 文件夹中找到主目录。

  1. We can also load the file as an HTTP resource and not from the local filesystem. So, place a file named external.properties with the content of my.config.value=From HTTP Config somewhere on the web. It can even be checked in a GitHub or BitBucket repository, as long as it is accessible without any need for authentication.
  2. Let's run our application by executing ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger --spring.config.location=http://<your file location path>/external.properties in order to see the following output in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From HTTP Config

How it works...

在深入研究 external 配置设置的详细信息之前,让我们快速查看为打印属性而添加的代码日志中的值。焦点元素是 @Value 注解,可用于类字段或方法参数;它还指示 Spring 使用注释中定义的值自动注入带注释的变量。如果值位于以美元符号为前缀的环绕花括号中,(${ }),Spring 将用相应应用程序 property< 中的值替换它/span> 或使用默认值,如果提供,在冒号后添加文本数据 (:< /代码>)。

在我们的例子中,我们将它定义为 @Value("${my.config.value:}")String configValue,所以除非应用程序属性名为 my.config.value 存在,空字符串的默认值将分配给 configValue 方法参数。这种构造非常方便,无需显式连接环境对象的实例以从中获取特定的属性值,并且在测试期间简化了代码,需要模拟的对象更少。

能够指定应用程序 properties 配置文件的位置的支持旨在支持动态的大量 环境 拓扑,尤其是在云环境中。当编译后的应用程序被捆绑到不同的云镜像中时,通常会出现这种情况,这些镜像用于不同的环境,并由 Packer、Vagrant 等部署工具专门组装。

在这种情况下,在制作镜像时将配置文件放入镜像文件系统是很常见的,具体取决于它的目标环境。 Spring Boot 提供了一种非常方便的功能,可以通过命令行参数指定应该添加到应用程序配置包中的配置属性文件所在的位置。

使用 --spring.config.location 启动选项,我们可以指定一个或多个文件的位置,然后可以用逗号分隔(,) 添加到默认值。文件名称可以是来自本地文件系统、类路径或远程 URL 的文件。这些位置将由 DefaultResourceLoader 类解析,或者,如果通过 SpringApplication 构造函数或设置器进行配置,则由实现由 SpringApplication 实例提供。

如果位置包含目录,则名称应以 / 结尾,以便让 Spring Boot 知道它应该查找 application.properties< /code> 文件在这些目录中。

 

如果您想更改文件的默认名称,Spring Boot 也为您提供了此功能。只需将 --spring.config.name 选项设置为您想要的任何文件名。

Note

重要的是要记住,无论是否存在--spring.config.location 设置。这样,您始终可以在 application.properties 中保留默认配置,并通过启动设置覆盖您需要的配置。

Externalizing an environmental configuration using environment variables


在前面的秘籍中,我们有 number 次,暗示 Spring Boot 应用程序的配置值可以使用 OS 环境变量传递和覆盖。操作系统依靠这些变量来存储有关各种事物的信息。我们可能需要设置几次 JAVA_HOMEPATH,这些都是环境变量的例子。如果使用 Heroku 或 Amazon AWS 等 PaaS 系统部署应用程序,操作系统环境变量也是一项非常重要的功能。在这些环境中,数据库访问凭证和各种 API 令牌等配置值都通过环境变量提供。

它们的强大之处在于能够完全外部化简单键值数据对的配置,而无需依赖将属性或其他文件放置在特定位置,并将其硬编码在应用程序代码库中。这些变量对于特定的操作系统也是不可知的,并且可以以相同的方式在 Java 程序中使用,System.getenv(),无论程序在哪个操作系统上运行.

在这个秘籍中,我们将探索如何利用这种能力将配置属性传递给我们的 Spring Boot 应用程序。我们将继续使用上一个秘籍中的代码库,并尝试几种不同的方式来启动应用程序和使用操作系统环境变量来更改某些属性的配置值。

 

How to do it...

  1. In the previous recipe, we added a configuration property named my.config.value. Let's build the application by running ./gradlew clean bootJar and start it by running MY_CONFIG_VALUE="From ENV Config" ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger so as to see the following output in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of 
    my.config.value property is: From ENV Config
  1. If we want to use the environment variables while running our application via the Gradle bootRun task, the command line will be MY_CONFIG_VALUE="From ENV Config" ./gradlew clean bootRun and should produce the same output as in the preceding step.
  2. Conveniently enough, we can even mix and match how we set the configurations. We can use the environment variable to configure the spring.config.location property and use it to load other property values from the external properties file, as we did in the previous recipe. Let's try this by launching our application by executing SPRING_CONFIG_LOCATION= file:/home/<username>/external.properties ./gradlew bootRun. We should see the following in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of 
my.config.value property is: From Home Directory Config

Note

虽然使用环境变量非常方便,但如果这些变量的数量太多,它确实会产生维护开销。为帮助解决此问题,最好使用委托方法,通过设置SPRING_CONFIG_LOCATION变量来配置特定于环境的属性文件的位置,通常是通过加载它们从 URL 位置。

 

 

How it works...

正如您从环境配置层次结构部分中了解到的,Spring Boot 提供了多种提供配置属性的方法。其中每一个都通过适当的 PropertySource 实现进行管理。在实现 ApacheCommonsConfigurationPropertySource 时,我们研究了如何创建 PropertySource 的自定义实现。 Spring Boot 已经提供了一个 SystemEnvironmentPropertySource 实现供我们开箱即用。这甚至会自动注册到环境接口的默认实现:SystemEnvironment

由于 SystemEnvironment 实现 provides 在众多不同的 PropertySource 实现,覆盖是无缝进行的,仅仅是因为 SystemEnvironmentPropertySource 类在列表中比 application.properties 文件一。

您应该注意的一个重要方面是使用带有下划线 (_) 的 ALL_CAPS 来分隔单词而不是传统的传统 all.lower.cased 格式,用点 (.) 分隔 Spring Boot 中用于命名配置属性的单词。这是由于某些操作系统的特性,即 Linux 和 OS X,它们禁止在名称中使用点 (.),而是鼓励使用 ALL_CAPS 下划线分隔符号。

在不需要使用环境变量来指定或覆盖配置属性的情况下,Spring 为我们提供了 -Dspring.getenv.ignore 系统属性,可以设置为true 并防止使用环境变量。如果由于在某些应用程序服务器上运行代码或可能不允许访问环境变量的特定安全策略配置而在日志中看到错误或异常,您可能希望将此设置更改为 true。

Externalizing an environmental configuration using Java system properties


虽然 environment 变量在极少数情况下可能会受到影响,但始终可以信任良好的旧 Java 系统属性有你。除了使用以双破折号 (--) 为前缀的属性名称表示的环境变量和命令行参数之外,Spring Boot 提供<一个 id="id325891479" class="indexterm"> 您能够使用普通的 Java 系统属性来设置或覆盖配置属性。

这在许多情况下很有用,特别是如果您的应用程序在容器中运行,该容器在启动期间通过您想要访问的系统属性设置某些值,或者如果属性值未通过命令行设置-D 参数,而是在某些库中通过代码和调用 System.setProperty(...),特别是如果属性正在从内部访问值 一种静态方法。虽然可以说这些情况很少见,但只需要一个让您向后弯腰努力尝试将此值集成到您的应用程序中。

在这个秘籍中,我们将使用与前一个相同的应用程序可执行文件,唯一的区别是我们使用 Java 系统属性而不是命令行参数或环境变量来在运行时设置我们的配置属性。

How to do it...

  1. Let's continue our experiments by setting the my.config.value configuration property. Build the application by running ./gradlew clean bootJar and start it by running java -Dmy.config.value="From System Config" -jar ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar so as to see the following in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From System Config

 

  1. If we want to be able to set the Java system property while running our application using the Gradle's bootRun task, we will need to add this to the applicationDefaultJvmArgs configuration in the build.gradle file. Let's add -Dmy.config.value=Gradle to this list and start the application by running ./gradlew clean bootRun. We should see the following in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: Gradle
  1. As we made the applicationDefaultJvmArgs setting to be shared with launch.script, rebuilding the application by running ./gradlew clean bootJar and starting it by running ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar should yield the same output in the logs as in the preceding step.

How it works...

您可能已经猜到,Java 系统属性被用于环境变量的类似 机制所使用,您会正确的。唯一真正的区别是 PropertySource 的实现。这一次,StandardEnvironment 使用了更通用的 MapPropertySource 实现。

您可能还注意到需要使用 java -Dmy.config.value="From System Config" -jar ./build/libs/bookpub-0.0.1- 启动我们的应用程序SNAPSHOT-exec.jar 命令,而不仅仅是简单地调用自执行打包的 JAR。这是因为,与环境变量和命令行参数不同,Java 系统属性必须在 Java 可执行文件上设置在其他所有内容之前。

我们确实设法通过有效地硬编码 build.gradle 文件中的值来解决这一需求,该文件与我们对 所做的增强相结合launch.script,允许我们将 my.config.value 属性嵌入到自执行 jar 的命令行中,以及与 Gradle 的 < code class="literal">bootRun 任务。

 

将这种 方法 与配置属性一起使用的风险在于,它总是会覆盖我们在更高层设置的值配置,例如 application.properties 等。除非您明确构建 Java 可执行命令行并且不使用打包 JAR 的自启动功能,否则最好不要使用 Java 系统属性并考虑使用命令行参数或环境变量。

Externalizing an environmental config using JSON


我们已经研究了许多不同的方法来外部添加或覆盖特定属性的值,或者使用环境 变量、系统属性或命令行参数。所有这些选项都为我们提供了很大的灵活性,但除了外部属性文件之外,都仅限于一次设置一个属性。在使用属性文件时,该语法在表示嵌套的分层数据结构方面并不是最好的,并且可能会有些棘手。为了避免这种情况,Spring Boot 为我们提供了在外部传递 JSON 编码内容的能力,该内容包含整个配置层次结构的设置。

在这个秘籍中,我们将使用与前一个相同的应用程序可执行文件,唯一的区别是使用外部 JSON 内容在运行时设置我们的配置属性。

How to do it...

  1. Let's continue our experiments by setting the my.config.value configuration property. Build the application by running ./gradlew clean bootJar and start it by running java -jar ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.application.json={"my":{"config":{"value":"From external JSON"}}} so as to see the following in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From external JSON
  1. If we want to be able to set the content using Java system properties, we can use -Dspring.application.json instead, assigning the same JSON content as the value.
  1. Alternatively, we can also rely on the SPRING_APPLICATION_JSON environment variable to pass the same JSON content in the following way:
SPRING_APPLICATION_JSON={"my":{"config":{"value":"From external JSON"}}} java -jar ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar --spring.profiles.active=logger 

How it works...

就像我们看到的所有其他配置方法一样,JSON 内容由专用的 EnvironmentPostProcessor 实现使用。唯一的区别是将 JSON 树扁平化为扁平属性映射,以匹配点分隔的属性命名样式。在我们的例子中, my->config->value 嵌套映射被转换为只有一个键 my.config 的平面映射.value,其值为 From external JSON

JSON 内容的设置可以来自任何属性源,在加载时可从环境中获得,其中包含一个名为 spring.application.json 的键,其值为 valid JSON 内容,并不仅限于由环境变量设置或使用 SPRING_APPLICATION_JSON 名称或 Java 系统属性。

此功能对于批量提供外部定义的、特定于环境的配置非常有用。最好的方法是使用 Chef、Puppet、Ansible、Packer 等机器/图像配置工具在机器实例上设置 SPRING_APPLICATION_JSON 环境变量。这使您能够将整个配置层次结构存储在外部的一个 JSON 文件中,然后只需在特定机器上配置正确的内容在配置期间,只需设置一个环境变量。该机器上运行的所有应用程序将在启动时自动使用它。

Setting up Consul


到目前为止,我们对 configuration 所做的一切都与本地数据集相关联。在真实的大型企业环境中,情况并非总是如此,并且经常希望能够在数百甚至数千个实例或机器上进行大规模的配置更改。

有许多工具可以帮助你完成这项任务,在这个秘籍中,我们将看看一个在我看来从小组中脱颖而出的工具,它让你能够干净而优雅地配置环境使用分布式数据存储的启动应用程序的变量。该工具的名称是 Consul。它是 Hashicorp 的开源产品,旨在发现和配置大型分布式基础架构中的服务。

在这个秘籍中,我们将看看如何安装和配置 Consul,并试验它提供的一些关键功能。这将为我们的下一个秘籍提供必要的熟悉度,我们将使用 Consul 提供启动应用程序所需的配置值。

How to do it...

  1. Go to https://consul.io/downloads.html and download the appropriate archive, depending on the operating system that you are using. Consul supports Windows, OS X, and Linux, so it should work for the majority of readers.

Note

如果您是 OS X 用户,您可以使用 Homebrew 安装 Consul,方法是运行brew install caskroom/cask/brew-cask,然后运行brew cask install领事

  1. After the installation, we should be able to run consul --version and see the following output:
Consul v1.0.1
Protocol 2 spoken by default, understands 2 to 3 (agent will automatically use protocol >2 when speaking to compatible agents)
  1. With Consul successfully installed, we should be able to start it by running the consul agent -server -bootstrap-expect 1 -data-dir /tmp/consul command and our terminal window will display the following:
==> WARNING: BootstrapExpect Mode is specified as 1; this is the same as Bootstrap mode.==> WARNING: Bootstrap mode enabled! Do not enable unless necessary==> WARNING: It is highly recommended to set GOMAXPROCS higher than 1==> Starting Consul agent...==> Starting Consul agent RPC...==> Consul agent running!         Node name: <your machine name>'        Datacenter: 'dc1'            Server: true (bootstrap: true)      Client Addr: 127.0.0.1 (HTTP: 8500, HTTPS: -1, DNS: 8600, RPC: 8400)    Cluster Addr: 192.168.1.227 (LAN: 8301, WAN: 8302)Gossip encrypt: false, RPC-TLS: false, TLS-Incoming: false         Atlas: <disabled>==> Log data will now stream in as it occurs:    2017/12/17 20:34:43 [INFO] serf: EventMemberJoin: <your machine name> 192.168.1.2272017/12/17 20:34:43 [INFO] serf: EventMemberJoin: <your machine name>.dc1 192.168.1.227    2017/12/17 20:34:43 [INFO] raft: Node at 192.168.1.227:8300 [Follower] entering Follower state    2017/12/17 20:34:43 [INFO] consul: adding server <your machine name> (Addr: 192.168.1.227:8300) (DC: dc1)    2017/12/17 20:34:43 [INFO] consul: adding server <your machine name>.dc1 (Addr: 192.168.1.227:8300) (DC: dc1)    2017/12/17 20:34:43 [ERR] agent: failed to sync remote state: No cluster leader    2017/12/17 20:34:45 [WARN] raft: Heartbeat timeout reached, starting election    2017/12/17 20:34:45 [INFO] raft: Node at 192.168.1.227:8300 [Candidate] entering Candidate state    2017/12/17 20:34:45 [INFO] raft: Election won. Tally: 1    2017/12/17 20:34:45 [INFO] raft: Node at 192.168.1.227:8300 [Leader] entering Leader state    2017/12/17 20:34:45 [INFO] consul: cluster leadership acquired    2017/12/17 20:34:45 [INFO] consul: New leader elected: <your machine name>    2017/12/17 20:34:45 [INFO] raft: Disabling EnableSingleNode (bootstrap)    2017/12/17 20:34:45 [INFO] consul: member '<your machine name>' joined, marking health alive    2017/12/17 20:34:47 [INFO] agent: Synced service 'consul'
  1. With the Consul service running, we can verify that it contains one member by running the consul members command, and should see the following result:
Node                 Address        Status  Type    Build  Protocol  DC<your_machine_name> 2.168.1.227:8301 alive  server   0.5.2     2    dc1
  1. While Consul can also provide discovery for services, health checks, distributed locks, and more, we are going to focus on the key/value service as this is what will be used to provide the configuration in the next recipe. So, let's put the From Consul Config value in the key/value store by executing the curl -X PUT -d 'From Consul Config' http://localhost:8500/v1/kv/bookpub/my/config/value command.

Note

如果您使用的是 Windows,您可以从http://curl.haxx.se 获取curl  /download.html

  1. We can also retrieve the data by running the curl http://localhost:8500/v1/kv/bookpub/my/config/value command and should see the following output:
[{"CreateIndex":20,"ModifyIndex":20,"LockIndex":0,"Key":"bookpub/my/config/value","Flags":0,"Value":"RnJvbSBDb25zdWwgQ29uZmln"}]
  1. We can delete this value by running the curl -X DELETE http://localhost:8500/v1/kv/bookpub/my/config/value command.
  2. In order to modify the existing value and change it for something else, execute the curl -X PUT -d 'newval' http://localhost:8500/v1/kv/bookpub/my/config/value?cas=20 command.

 

 

How it works...

关于 Consul 的工作原理及其键/值服务的所有可能选项的详细说明需要一本书,因此我们将只看基本部分。强烈建议您在 https:/ 阅读 Consul 的文档/consul.io/intro/getting-started/services.html

第3步中,我们启动了Consul 服务器模式下的代理。它充当主主节点,在实际部署中,运行在各个实例上的本地代理将使用服务器节点连接并从中检索数据。为了我们的测试目的,我们将只使用这个服务器节点,就好像它是一个本地代理一样。

启动时显示的信息显示,我们的节点已作为服务器节点启动,在端口 8500 上建立 HTTP 服务以及 DNS 和 RPC 服务,如果是这样选择的话连接到它。 We can also see that there is only one node in the cluster, ours, and we are the elected leader running in a healthy state.

由于我们将通过 cURL 使用方便的 RESTful HTTP API,我们所有的请求都将使用端口 8500 上的 localhost。作为一个 RESTful API,它完全遵循 CRUD 动词术语,为了插入数据,我们将在 /v1/ 上使用 PUT 方法kv 端点以设置 bookpub/my/config/value 键。

检索数据更加直接:我们只需使用所需的密钥向同一个 /v1/kv 服务发出 GET 请求. DELETE 也是如此,唯一的区别是方法名称。

更新操作需要 URL 中的更多信息,即 cas 参数。该参数的值应该是所需键的ModifyIndex,可以从GET请求中获取。在我们的例子中,它的值为 20。

Externalizing an environmental config using Consul and envconsul


在前面的秘籍中,我们安装了 Consul 服务并试验了它的键/值功能,以了解我们如何操作其中的数据,以便将 Consul 与我们的应用程序集成,并使数据提取过程无缝且无创地从应用角度。

 

因为我们不希望我们的应用程序 知道任何关于 Consul 并且必须明确<一个 id="id325954115" class="indexterm"> 连接到它,即使存在这种可能性,我们将使用另一个实用程序,它也是由 Hashicorp 作为开源创建的,称为 envconsul。它将为我们连接到 Consul 服务,提取指定的 configuration 键/值树,并 expose 它作为环境变量在启动我们的应用程序时使用。很酷,对吧?

Getting ready

在我们开始启动我们在前面的秘籍中创建的应用程序之前,我们需要安装 envconsul 实用程序。

https://github.com/ 下载适用于您各自操作系统的二进制文件hashcorp/envconsul/releases 并将可执行文件解压缩到您选择的任何目录,但最好将其放在 PATH 中的某个位置。

从下载的存档中提取 envconsul 后,我们就可以开始使用它来配置我们的应用程序了。

How to do it...

  1. If you have not already added the value for the my/config/value key to Consul, let's add it by running curl -X PUT -d 'From Consul Config' http://localhost:8500/v1/kv/bookpub/my/config/value.
  2. The first step is to make sure envconsul can connect to the Consul server and that it extracts the correct data based on our configuration key. Let's execute a simple test by running the envconsul --once --sanitize --upcase --prefix bookpub env command. We should see the following in the output:
...TERM=xterm-256colorSHELL=/bin/bashLANG=en_US.UTF-8HOME=/Users/<your_user_name>...MY_CONFIG_VALUE=From Consul Config

 

  1. After we have verified that envconsul is returning the correct data to us, we will use it to launch our BookPub application by running envconsul --once --sanitize --upcase --prefix bookpub ./gradlew clean bootRun. Once the application has started, we should see the following output in the logs:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From Consul Config
  1. We can do the same thing by building the self-starting executable JAR by running ./gradlew clean bootJar, and start it by running envconsul --once --sanitize --upcase --prefix bookpub ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar to make sure we see the same output in the logs as in the preceding step. If you see Gradle instead of From Consul Config, make sure the applicationDefaultJvmArgs configuration in build.gradle does not have -Dmy.config.value=Gradle in it.
  2. Another marvelous ability of envconsul is not only to export the configuration key values as environment variables, but also to monitor for any changes and restart the application if the values in Consul change. Let's launch our application by running envconsul --sanitize --upcase --prefix bookpub ./build/libs/bookpub-0.0.1-SNAPSHOT-exec.jar, and we should see the following value in the log:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From Consul Config
  1. We will now use the consul command to get the current ModifyIndex of our key and update its value to From UpdatedConsul Config by opening another terminal window and executing curl http://localhost:8500/v1/kv/bookpub/my/config/value, grabbing the ModifyIndex value, and using it to execute curl -X PUT -d 'From UpdatedConsul Config' http://localhost:8500/v1/kv/bookpub/my/config/value?cas=<ModifyIndex Value>. We should see our running application magically restart itself and our newly updated value displayed in the log at the end:
2017-12-17 --- ication$$EnhancerBySpringCGLIB$$b123df6a : Value of my.config.value property is: From UpdatedConsul Config

 

 

How it works...

我们刚刚做的很甜蜜,对吧?让我们更详细地研究幕后发生的魔法。我们将首先剖析命令行并解释每个参数控制选项的作用。

我们的第一个执行命令行是 envconsul --once --sanitize --upcase --prefix bookpub ./gradlew clean bootRun,所以让我们看看我们到底做了什么,如如下:

  • First, one might notice that there is no indication about which Consul node we should be connecting to. This is because there is an implicit understanding or an assumption that you already have a Consul agent running locally on localhost:8500. If this is not the case for whatever reason, you can always explicitly specify the Consul instance to connect via the --consul localhost:8500 argument added to the command line.
  • The --prefix option specifies the starting configuration key segment in which to look for the different values. When we were adding keys to Consul, we used the following key: bookpub/my/config/value. By specifying the --prefix bookpub option, we tell envconsul to strip the bookpub part of the key and use all the internal tree elements in bookpub to construct the environment variables. Thus, my/config/value becomes the environment variable.
  • The --sanitize option tells envconsul to replace all the invalid characters with underscores (_). So, if we were to only use --sanitize, we would end up with my_config_value as an environment variable.
  • The --upcase option, as you might already have guessed, changes the environment variable key to all upper case characters, so when combined with the --sanitize option, my/config/value key gets transformed into the MY_CONFIG_VALUE environment variable.
  • The --once option indicates that we only want to externalize the keys as environment variables once and do not want to continuously monitor for changes in the Consul cluster. If a key in our prefix tree has changed its value, we re-externalize the keys as environment variables and restart the application.

最后一个选项 --once 提供了非常有用的功能选择。如果您只对通过使用 Consul 共享配置的应用程序的初始引导感兴趣,那么键将被设置为环境变量,应用程序将被启动,并且 envconsul 将认为它的工作已完成。但是,如果您想监视 Consul 集群中键/值的更改,并且在更改发生后,重新启动您的应用程序以反映新更改,然后删除 --once 选项和 envconsul 将在更改发生后重新启动应用程序。

这种行为对于数据库连接配置的近乎即时的更改非常有用和方便。想象一下,您需要从一个数据库快速故障转移到另一个数据库,并且您的 JDBC URL 是通过 Consul 配置的。您需要做的就是推送一个新的 JDBC URL 值,envconsul 几乎会立即检测到此更改并重新启动应用程序,告诉它连接到一个新的数据库节点。

目前,这个功能是通过发​​送一个传统的SIGTERM信号给一个应用程序运行进程来实现的,告诉它终止,一旦进程退出,重新启动应用程序。这可能并不总是理想的行为,特别是如果应用程序需要一些时间才能启动并能够占用流量。您不希望关闭整个 Web 应用程序集群,即使只是几分钟。

为了更好地处理这种情况,envconsul 得到了增强,能够发送许多可以配置的标准信号通过新添加的 --kill-signal 选项。使用此选项,我们可以指定使用任何 SIGHUP、SIGTERM、SIGINT、SIGQUIT、SIGUSR1 或 SIGUSR2 信号来代替默认的 SIGTERM,一旦检测到键/值更改,就将其发送到正在运行的应用程序进程。

由于大多数行为都非常特定于特定操作系统和在其上运行的 JVM,Java 中的进程信号处理并不那么清晰和直接。列表中的某些信号无论如何都会终止应用程序,或者在 SIGQUIT 的情况下,JVM 会将 Core Dump 打印到标准输出中。但是,有一些方法可以配置 JVM,具体取决于操作系统,让我们使用 SIGUSR1 和 SIGUSR2 而不是对这些信号本身进行操作,但不幸的是,该主题超出了本书的范围。

以下是如何 处理信号处理程序: https://github.com/spotify/daemon-java< /a>,或在 https://docs.oracle.com/javase/8/docs/technotes/guides/troubleshoot/signals.html了解详细说明。