19.4 在Docker容器中运行Spring Boot

在分发云中部署的各种应用时,Docker已经成为事实标准。很多云环境都接受以Docker容器的形式部署应用,包括AWS、Microsoft Azure、Google CloudPlatform和Pivotal Web Services(简单举例)。

容器化应用程序(比如使用Docker创建的应用程序)的概念借鉴了现实世界中的联运集装箱。在运输过程中,不管里面的东西是什么,所有的联运集装箱都有一个标准的尺寸和格式。正因为如此,联运集装箱才能够很容易地堆放在船上、火车上或卡车上。按照类似的方式,容器化的应用程序遵循通用的容器格式,可以在任何地方部署和运行,而不必关心里面的应用是什么。

尽管创建Docker镜像并不困难,但是Spotify提供了一个Maven插件,借助它我们可以轻而易举地将Spring Boot的构建结果创建为Docker容器。要使用该Docker插件,需要将其添加到Spring Boot项目pom.xml文件的<build>/<plugins>代码块下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<build>
<plugins>
...
<plugin>
<groupId>com.spotify</groupId>
<artifactId>dockerfile-maven-plugin</artifactId>
<version>1.4.3</version>
<configuration>
<repository>
${docker.image.prefix}/${project.artifactId}
</repository>
<buildArgs>
<JAR_FILE>target/${project.build.finalName}.jar</JAR_FILE>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>

<configuration>代码块下,我们设置了一些属性,用于指导如何创建Docker镜像。<repository>描述了在Docker仓库中该Docker镜像的名称。按照这里的设置,其名称是Maven项目的artifact ID,加上Maven属性docker.image.prefix的值作为前缀。项目的artifact ID是Maven已知的,而前缀属性则需要我们进行设置:

1
2
3
4
<properties>
...
<docker.image.prefix>tacocloud</docker.image.prefix>
</properties>

以Taco Cloud的配料服务来讲,所形成的Docker镜像在Docker仓库的名称为tacocloud/ingredient-service。

<buildArgs>元素下面,我们声明镜像要包含Maven构建所生成的JAR文件,在这里使用Maven属性project.build.finalName来确定target目录下JAR文件的名称。

除了提供给Maven构建文件的信息之外,Docker镜像的所有定义都位于一个名叫Dockerfile的文件中。这个文件指明了新镜像要基于哪个基础镜像、要设置的环境变量、要mount的卷以及最重要的入口点(entry point)。入口点也就是基于该镜像的容器在启动时要执行的命令。对于大多数Spring Boot应用来讲,如下的Dockerfile就是一个很好的起点:

1
2
3
4
5
6
7
8
9
FROM openjdk:8-jdk-alpine
ENV SPRING_PROFILES_ACTIVE docker
VOLUME /tmp
ARG JAR_FILE
COPY ${JAR_FILE} app.jar
ENTRYPOINT ["java",\
"-Djava.security.egd=file:/dev/./urandom",\
"-jar",\
"/app.jar"]

我们将Docker文件逐行拆分,将会看到它包含如下内容。

  • FROM指令声明了新镜像要基于哪个基础镜像。新的镜像会扩展基础镜像。在本例中,基础镜像为openjdk:8-jdk- alpine,这是一个基于OpenJDK 8的容器镜像。
  • ENV指令设置了环境变量。我们可以基于激活状态的profile重写一些SpringBoot应用的配置属性,所以在本镜像中,我们将SPRING_PROFILES_ACTIVE环境变量设置为docker,从而确保Spring Boot应用启动时docker是处于激活状态的profile。
  • VOLUME指令在容器中创建了一个mount点。在本例中,它在“/tmp”创建了一个mount点,所以需要的话可以将数据写入“/tmp”目录下。
  • ARG指令声明了一个要在构建期传入的参数。在本例中,它声明了名为JAR_FILE的参数,与Maven插件<buildArgs>代码块中的参数是相同的。
  • COPY指令会将给定路径下的某个文件复制到另外一个路径下。在本例中,它会将Maven插件中声明的JAR文件复制为容器中名为app.jar的文件。
  • ENTRYPOINT描述了容器启动的时候要执行什么操作。它以数组的形式指定了要执行的命令行。在本例中,它使用java命令来运行可执行的app.jar。

我们着重介绍一下ENV指令。在任何包含Spring Boot应用程序的容器镜像中设置SPRING_PROFILES_ACTIVE环境变量通常都是一个好办法。这样的话,我们可以配置一些仅在Docker下运行应用时有效的bean和配置属性。

对于配料服务,我们需要将应用程序链接到运行在单独容器中的Mongo数据库。默认情况下,Spring Data会尝试连接localhost上监听端口27017的Mongo数据库。但是,这种做法只有在本地运行的时候才有效,并不适合容器。因此,我们需要配置spring.data.mongodb.host属性,告诉Spring Data要访问哪个主机上的Mongo。

虽然我们可能还不知道Mongo数据库运行在何处,但是我们可以通过application.yml文件配置Docker特定的配置,让它在docker profile处于激活状态时配置Spring Data连接名为mongo的主机上的Mongo:

---
spring:
  profiles: docker
  data:
    mongodb:
      host: mongo

随后,当我们启动Docker容器的时候,会将mongo主机映射到一个在不同容器中运行的Mongo数据库上。现在,我们先来构建容器镜像。借助Maven包装器,执行package和dockerfile:build goal来构建JAR文件,然后构建Docker镜像:

1
$ mvnw package dockerfile:build

此时,我们可以通过docker images来校验本地镜像仓库中的镜像(为了可读性和适应本书的宽度,这里将CREATED和SIZE列删减掉了):

1
2
3
$ docker images
REPOSITORY TAG IMAGE ID
tacocloud/ingredient-service latest 7e8ed20e768e

在启动容器之前,我们需要启动Mongo数据库的容器。如下的命令显示了运行一个名为tacocloud-mongo的新容器,其中包含Mongo 3.7.9数据库:

1
$ docker run --name tacocloud-mongo -d mongo:3.7.9-xenial

现在,我们终于可以运行配料服务容器了,并链接它到刚刚启动的Mongo容器上:

1
2
3
$ docker run -p 8080:8081 \
--link tacocloud-mongo:mongo \
tacocloud/ingredient-service

这里的docker run命令有多个值得介绍的重要组件。

  • 因为我们配置了容器中的Spring Boot应用运行在8081端口上,所以-p参数可以将内部端口映射到主机的8080端口上。
  • –link参数能够将我们的容器链接到名为tacocloud-mongo的容器上,并为其分配mongo主机名,这样Spring Data就可以使用该主机名连接它了。
  • 最后,我们指定了容器要运行的镜像名称(也就是tacocloud/ingredient-service)。

现在,Docker镜像构建完成并且已经证明可以作为本地容器运行。我们可以更进一步,将镜像推送至Dockerhub或其他Docker镜像仓库。如果你有Dockerhub账号并且已经登录,那么可以使用如下的Maven命令推送镜像:

1
$ mvnw dockerfile:push

这样的话,我们可以将镜像部署到几乎所有支持Docker容器的环境中,包括AWS、Microsoft Azure和Google Cloud Platform。你可以选择任意的环境并按照平台相关的指令部署Docker镜像。

19.3 推送JAR文件到Cloud Foundry上

服务器的硬件购买和维护成本可能代价高昂。当出现高负载时,恰当地对服务器进行扩展是非常困难的,对有些组织来说,这样做甚至是不允许的。如今,相对于在自己的数据中心运行应用,将应用部署到云中是一种人们广泛关注并且能够节省成本的方案。

我们有多种可选的云方案,但是人们目前最关注的是平台即服务(Platform as aService,PaaS)。PaaS提供了现成的应用部署平台,其中包含多种可以绑定到应用上的附加服务(比如数据库和消息代理)。除此之外,如果你的应用需要额外的处理能力,那么云平台很容易在运行时对应用进行扩展(或收缩),这是通过添加和移除实例实现的。

Cloud Foundry是一个开源的PaaS平台,起源于Pivotal(Spring框架和Spring平台中的其他库也都是由这家公司赞助的)。Cloud Foundry最令人关注的一点在于它提供了开源和基于商业的发行版,让我们可以选择如何以及在哪里使用CloudFoundry。它甚至可以运行在防火墙之内的公司数据中心里面,提供私有云方案。

虽然Cloud Foundry很乐意接受WAR文件,但是对于Cloud Foundry的需要来说,WAR文件格式过于重量级了。更简单的可执行JAR文件更适合部署到CloudFoundry中。

为了演示如何构建和部署可执行JAR文件到Cloud Foundry,我们将会构建配料服务应用并将其部署到Pivotal Web Services(PWS)上。如果想要使用PWS,我们就需要注册一个账户。PWS提供87美元的免费试用功能,在试用期间甚至不需要提供任何信用卡信息。

注册完PWS之后,我们需要从PWS下载并安装cf命令行工具。我们将会使用cf工具将应用推送至Cloud Foundry。首先,我们需要使用它来登录PWS账号:

1
2
3
4
5
6
$ cf login -a https://api.run.pivotal.io
API endpoint: https://api.run.pivotal.io
Email> {your email}
Password> {your password}
Authenticating...
OK

非常好!现在,我们已经准备好将配料服务部署到云中了。实际上,这个项目本身现在就可以部署到Cloud Foundry中,我们所需要做的就是构建并将其推送至云端。

要使用Maven构建项目的话,我们可以使用Maven包装器执行package goal(将会在target目录下得到形成的JAR文件):

1
$ mvnw package

如果使用Gradle,那么我们可以使用Gradle包装器运行build任务(将会在build/libs目录下得到形成的JAR文件):

1
$ gradlew build

现在,剩下的事情就是使用cf命令将JAR文件推送至Cloud Foundry:

1
$ cf push ingredient-service -p target/ingredient-service-0.0.19-SNAPSHOT.jar

cf push命令的第一个参数指定了在Cloud Foundry中该应用的名称。除了其他功能之外,这个名称还会用作应用托管的子域。因此,非常重要的一点在于,我们为应用设置的名称必须是唯一的,避免与Cloud Foundry已部署的应用(包括其他Cloud Foundry用户所部署的应用)冲突。

想一个唯一名称可能会比较麻烦,cf push命令提供了–random-route选项,它会为我们随机生成一个子域。如下的命令展现了如何将配料服务应用推送至一个随机生成的路由:

1
2
3
$ cf push ingredient-service \
-p target/ingredient-service-0.0.19-SNAPSHOT.jar \
--random-route

在使用–random-route的时候,依然需要应用名称,但是会在该名称上拼接两个随机选择的单词以生成子域。

假设所有的过程都很顺利,应用应该已经部署就位并且可以处理请求了,子域是ingredient-service,那么我们可以通过浏览器访问http://ingredient-service.cfapps.io/ingredients 来查看实际效果。在响应中,我们会看到一个可用配料的列表。

在我编写该应用的时候,它会使用嵌入式的Mongo数据库(这样做只是为了测试)来存放配料数据。在生产环境中,我们可能想要使用真正的数据库。在我编写本书的时候,PWS提供了一个完全托管的MongoDB服务,名为mlab。我们可以使用cf marketplace命令查看该服务(以及其他所有可用的服务)。要创建mlab实例,我们可以使用cf create-service命令:

1
$ cf create-service mlab sandbox ingredientdb

该命令会按照沙箱(sandbox)服务计划创建一个mlab服务,名为ingredientdb。服务创建完成之后,我们可以使用cf bind-service命令将其绑定到应用上。例如,要将ingredientdb服务绑定至配料服务应用,我们可以使用如下的命令:

1
$ cf bind-service ingredient-service ingredientdb

将服务绑定至应用只是为应用提供了如何连接至服务的详情,这是通过名为VCAP_SERVICES的环境变量实现的。它并没有改变应用使用服务的方式。服务绑定完成之后,我们需要重新部署(re-stage)应用才能使绑定生效:

1
$ cf restage ingredient-service

cf restage命令会强制Cloud Foundry重新部署应用并重新计算VCAP_SERVICES的值。在这个过程中,它会发现应用绑定了一个MongoDB服务,就会使用该服务作为应用的后端数据库。

PWS提供了很多可用的服务,我们可以直接将它们绑定到应用中,包括MySQL数据库、PostgreSQL数据库,甚至现成的Eureka和Config Server服务。建议你阅读一下PWS的Marketplace页面,以了解PWS都提供了哪些服务。关于PWS如何使用,可以参考其官网。

对于Spring Boot应用的部署来讲,Cloud Foundry是一个非常棒的PaaS方案。鉴于它与Spring项目之间的关系,所以会在这两者之间提供一些协同的功能。在云中部署应用程序的另一种常见方法是将应用程序打包到Docker容器中,然后发布到云中,在将应用程序推进到AWS这样的基础设施即服务(Infrastructure-as-a-Service, IAAS)平台时更是如此。下面让我们看看如何创建一个携带Spring Boot应用程序的Docker容器。

19.2 构建和部署WAR文件

在本书中,我们编写Taco Cloud应用所需的服务时,都是在IDE中运行,或者通过命令行以可执行文件的形式运行。不管是使用哪种方式,都会有一个嵌入式的Tomcat服务器(在Spring WebFlux应用中会是Netty)来为应用的请求提供服务。

在很大程度上,借助Spring Boot的自动配置,我们不需要创建web.xml文件或Servlet initializer类来声明Spring的DispatcherServlet,以实现Spring MVC相关的功能。如果要将应用程序部署到Java应用服务器中,就需要构建一个WAR文件。而且,为了让应用服务器知道如何运行应用程序,我们还需要在WAR文件中包含一个servlet initializer,以扮演web.xml文件的角色并声明DispatcherServlet。

实际上,要将Spring Boot应用构建为WAR文件并不困难。在使用Initializr创建应用的时候,如果选择了WAR方案,其实并没有额外要做的事情了。

Initializr会确保所生成的项目包含servlet initializer类,并且构建文件调整为生成WAR文件。如果你在Initializr中选择了构建为JAR文件(或者只是想知道它们之间的差异是什么),那么可以继续向下阅读。

首先,我们需要有一种配置Spring DispatcherServlet的方式。虽然可以通过web.xml文件来实现,但是Spring Boot的SpringBootServletInitializer使这个过程变得更加简单了。SpringBootServletInitializer是一个能够感知Spring Boot环境的特殊SpringWebApplicationInitializer实现。除了配置Spring的DispatcherServlet之外,SpringBootServletInitializer还会查找Spring应用上下文中所有Filter、Servlet或ServletContextInitializer类型的bean,并将它们绑定到servlet容器中。

要使用SpringBootServletInitializer,我们需要创建一个子类并重写configure()方法来指明Spring配置类。程序清单19.1展现了IngredientServiceServletInitializer,它是SpringBootServletInitializer的子类,我们将会使用它来实现配料服务应用。

程序清单19.1 通过Java启用Spring Web应用
1
2
3
4
5
6
7
8
9
10
11
package tacos.ingredients;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.web.SpringBootServletInitializer;
public class IngredientServiceServletInitializer
extends SpringBootServletInitializer {
@Override
protected SpringApplicationBuilder configure(
SpringApplicationBuilder builder) {
return builder.sources(IngredientServiceApplication.class);
}
}

我们可以看到,configure()方法以参数形式得到了一个SpringApplicationBuilder对象,并且将其作为结果返回。在中间的代码中,它调用sources()方法来注册Spring配置类。在本例中,它注册了IngredientServiceApplication类,这个类同时作为(可执行JAR的)引导类和Spring配置类。

虽然配料服务应用还有其他的Spring配置类,但是我们没有必要将它们全部注册到sources()方法中。IngredientServiceApplication类使用了@SpringBootApplication,说明将会启用组件扫描。组件扫描功能会发现其他的配置类并将它们添加进来。

在大多数情况下,SpringBootServletInitializer的子类都是样板式的。它引用了应用的主配置类。除此之外,在构建WAR时,每个应用都是相同的。我们几乎没有必要去修改它。

现在,我们已经编写完servlet initializer类。接下来,必须要对项目的构建文件做一些修改。如果使用Maven进行构建,那么所需的变更非常简单,只需要确保pom.xml中的<packaging>元素设置成war即可:

1
<packaging>war</packaging>

Gradle构建所需的变更也很简单直接,我们需要在build.gradle文件中应用war插件:

1
apply plugin: 'war'

现在,我们就可以构建应用了。如果使用Maven,那么我们可以借助Initializr所使用的Maven包装器来执行package goal:

1
mvnw package

构建成功的话,WAR文件将会出现在target目录下。如果使用Gradle来构建项目,那么可以使用Gradle包装器来执行build任务:

1
$ gradlew build

构建完成之后,我们可以在build/libs目录下找到WAR文件。剩下的事情就是部署应用了。不同应用服务器的部署过程会有所差异,所以请参考应用服务器部署过程的相关文档。

比较有意思的事情是,虽然我们构建了适用于Servlet 3.0(或更高版本)部署的WAR文件,但是这个WAR文件依然可以像可执行JAR文件那样在命令行中执行:

1
$ java -jar target/ingredient-service-0.0.19-SNAPSHOT.war

实际上,使用一个部署制件,我们同时实现了两种部署方案。

将微服务放到应用服务器中?

按照我们的初衷,更大的Taco Cloud应用是由多个微服务应用组成的,而配料服务只是其中之一。但是,在这里,我们所讨论的是将配料服务部署为一个单独的应用,并将其放到应用服务器中。这样做是合理的吗?

微服务通常和其他的应用一样,应该可以独立部署。尽管离开了Taco Cloud应用的上下文之后,配料服务也没有太大的用处,但是没有理由不能将其部署到Tomcat或其他应用服务器中。不过,我们需要注意的是,不要期望它能够具有像部署到云中那样的可扩展性。

虽然WAR文件作为Java部署的主流方案已经有20多年的历史了,但是它们确实是为将应用程序部署到传统Java应用服务器而设计的。按照我们所选择的平台,现代云部署方案并不需要WAR文件,有些甚至都不支持这种格式。随着我们进入云部署的新时代,JAR文件可能是更好的选择。

19.1 权衡各种部署方案

我们可以以多种方式构建和运行Spring Boot应用。在附录中将介绍其中的一部分,包括:

  • 使用Spring Tool Suite或IntelliJ IDEA在IDE中运行应用;
  • 在命令行中通过Maven spring-boot:run goal或Gradle bootRun任务运行应用;
  • 使用Maven或Gradle生成一个可执行的JAR文件,既可以在命令行运行,也可以部署到云中;
  • 使用Maven或Gradle生成一个WAR文件,以部署到传统的Java应用服务器中。

这些可选方案都非常适合在开发阶段运行应用。但是,如果我们想要将应用部署到生产环境或者其他非开发环境,又该怎么办呢?

通过IDE或者Maven、Gradle运行应用并不适用于生产环境,可执行的JAR文件或者传统的WAR文件才是将应用部署到生产环境的可行方案。既然可以部署为WAR文件或JAR文件,那我们该选择哪种呢?通常,这种选择取决于要将应用部署到传统的Java应用服务器中还是部署到云中。

  • 部署到Java应用服务器中:如果必须要将应用部署到Tomcat、WebSphere、WebLogic或其他传统的Java应用服务器中,其实我们别无选择,只能将应用构建为WAR文件。
  • 部署到云中:如果你计划将应用部署到云中,不管是Cloud Foundry、Amazon Web Services(AWS)、Azure、Google Cloud Platform还是其他云平台,那么可执行的JAR文件是最佳选择。即便云平台支持WAR部署,JAR文件格式也要比WAR格式简单得多,WAR文件是专门针对应用服务器部署设计的。

在本章中,我们将会关注3种部署场景。

  • 将Spring Boot应用以WAR文件的形式部署到Java应用服务器中,比如Tomcat。
  • 将Spring Boot应用作为可执行的JAR文件,推送到Cloud Foundry中。
  • 将Spring Boot应用打包到Docker容器中,将其部署到任何支持Docker形式的平台中。

首先,我们看一下如何将配料服务应用构建为一个WAR文件,这样它就可以部署到像Tomcat这样的应用服务器中了。

第19章 部署Spring

本章内容:
  • 将Spring应用构建为WAR或JAR文件
  • 推送Spring应用至Cloud Foundry
  • 使用Docker容器化Spring应用

想一下你最喜欢的动作片。现在我们想象一下,你要去电影院看那部电影,在高速追逐、爆炸和战斗中体验一场激动人心的视听之旅,但是电影最终却在好人打倒坏人之前戛然而止。电影院的灯一亮,所有人都被带出影院,我们没有看到电影里的冲突是如何解决的。虽然开头很精彩,但是重要的是影片的高潮部分。没有它,那就是为了行动而行动。

现在想象一下我们开发了应用程序,并在解决业务问题方面投入了大量的精力和创造力,但是从来没有将应用程序部署给其他人使用和享受。当然,我们编写的大多数应用程序都不涉及汽车追逐或爆炸(至少我不希望如此),但是在开发过程中会有一定的忙乱。并不是我们所写的每一行代码都是为生产而写的,但是,如果没有任何代码被部署的话,将是极端令人失望的。

到目前为止,我们一直在关注Spring Boot所提供的帮助应用开发的特性。在这个过程中,已经有了一些令人兴奋的进展。如果不越过终点线,也就是部署应用程序,那么这一切都是徒劳的。

在本章中,我们将会在使用Spring Boot开发应用的基础上再进一步,看一下如何部署这些应用。尽管对于部署过基于Java应用的人来说,这些事情是显而易见的,但是Spring Boot以及相关的Spring项目有一些独特之处,它们使得Spring Boot应用的部署与众不同。

实际上,与大多数以WAR文件部署的Java Web应用不同,Spring Boot提供了多种部署方案。在开始学习如何部署Spring Boot应用之前,我们看一下所有的可选方案并选择最适合需求的几种。

18.4 小结

  • 大多数Actuator端点都可以作为MBean使用,可以被JMX客户端消费。
  • Spring会自动启用JMX,用来监控Spring应用上下文中的bean。
  • Spring bean可以通过添加@ManagedResource注解导出为MBean。通过为bean类添加@ManagedOperation@ManagedAttribute注解,它的方法和属性可以导出为托管的操作和属性。
  • Spring bean可以使用NotificationPublisher发送通知给JMX客户端。

18.3 发送通知

借助Spring的NotificationPublisher,MBeans可以推送通知到感兴趣的JMX客户端。NotificationPublisher有一个sendNotification()方法,当得到一个Notification对象时,它会发送通知给任意订阅该MBean的JMX客户端。

要让某个MBean发送通知,它必须要实现NotificationPublisherAware接口,该接口要求实现一个setNotificationPublisher()方法。例如,我们希望每创建100个taco就发送一个通知。我们可以修改TacoCounter类,让它实现NotificationPublisherAware,并使用注入的NotificationPublisher每创建100个taco时就发送通知。程序清单18.2展现了启用通知功能TacoCounter所需要的变更。

程序清单18.2 每创建100个taco就发送通知
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
@Service
@ManagedResource
public class TacoCounter
extends AbstractRepositoryEventListener<Taco>
implements NotificationPublisherAware {
private AtomicLong counter;
private NotificationPublisher np;
...
@Override
public void setNotificationPublisher(NotificationPublisher np) {
this.np = np;
}
...
@ManagedOperation
public long increment(long delta) {
long before = counter.get();
long after = counter.addAndGet(delta);
if ((after / 100) > (before / 100)) {
Notification notification = new Notification(
"taco.count", this,
before, after + "th taco created!");
np.sendNotification(notification);
}
return after;
}
}

在JMX客户端中,我们需要订阅TacoCounter MBean来接收通知。每创建100个taco,客户端就会收到通知。图18.5展现了通知在JConsole中的样子。

image-20211023132709075

图18.5 JConsole订阅了TacoCounter MBean,每创建100个taco就会收到通知

通知是应用程序主动向监视客户端发送数据和告警的好办法。这样做的话,就不需要客户端轮询托管属性或调用托管操作了。

18.2 创建自己的MBean

借助Spring,可以很容易地将任意bean导出为JMX MBean。我们唯一需要做的就是在bean类上添加@ManagedResource注解,然后在方法或属性上添加@ManagedOperation或@ManagedAttribute。Spring会负责剩余的事情。

例如,我们想要提供一个MBean,用来跟踪通过Taco Cloud创建了多少个taco订单,那么我们可以定义一个服务bean,在这个服务中保持已创建taco的数量。程序清单18.1展现了该服务。

程序清单18.1 统计已创建taco订单数量的MBean
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
package tacos.tacos;
import java.util.concurrent.atomic.AtomicLong;
import
org.springframework.data.rest.core.event.AbstractRepositoryEventListener;
import org.springframework.jmx.export.annotation.ManagedAttribute;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Service;
@Service
@ManagedResource
public class TacoCounter
extends AbstractRepositoryEventListener<Taco> {
private AtomicLong counter;
public TacoCounter(TacoRepository tacoRepo) {
long initialCount = tacoRepo.count();
this.counter = new AtomicLong(initialCount);
}
@Override
protected void onAfterCreate(Taco entity) {
counter.incrementAndGet();
}
@ManagedAttribute
public long getTacoCount() {
return counter.get();
}
@ManagedOperation
public long increment(long delta) {
return counter.addAndGet(delta);
}
}

TacoCounter类使用了@Service注解,所以它将会被组件扫描功能所发现并且会注册一个实例作为bean存放到Spring应用上下文中。它还使用了@ManagedResource注解,表明这个bean是一个MBean。作为MBean,它暴露了一个属性和一个操作。getTacoCount()方法使用了@ManagedAttribute注解,将会暴露为一个MBean属性;而increment()方法使用了@ManagedOperation注解,将会暴露为MBean操作。

图18.4展现了TacoCounter MBean在JConsole中是什么样子。

image-20211023132546372

图18.4 在JConsole中看到的TacoCounter的操作和属性

TacoCounter还有一个技巧,不过这与JMX并没有什么关系。这个类扩展了AbstractRepositoryEventListener,每当通过TacoRepository保存Taco的时候,它都会得到通知。在本例中,在创建和保存新Taco对象的时候,onAfterCreate()方法将会被调用,我们在这里让计数器增加1。AbstractRepositoryEventListener还提供了多个方法来处理对象创建、保存和删除前后的事件。

使用MBean的操作和属性在很大程度上是一个拉取操作。换句话说,就算MBean属性的值发生了变化,除非通过JMX客户端查看该属性,否则我们也不会知道。接下来,我们换一个话题,看一下如何将MBean的通知推送至JMX客户端。

18.1 使用Actuator MBean

我们可以回头看一下表16.1,除了“/heapdump”之外,这里列出的所有端点均暴露成了MBean。我们可以使用任意的JMX客户端连接Actuator端点MBean。借助Java开发工具集中的JConsole,我们可以看到Actuator MBean列到了org.springframework.boot域下,如图18.1所示。

image-20211023113822314

图18.1 Actuator端点会自动暴露为JMX MBean

Actuator MBean端点非常好的一点在于它们默认就是对外暴露的。我们没有必要明确声明要包含哪些MBean端点,但是对于HTTP端点,我们是需要这样做的。我们可以通过设置management.endpoints.jmx.exposure.include和management.endpoints.jmx.exposure.exclude属性来缩小可选的范围。例如,我们想要限制Actuator端点只暴露“/health”“/info”“/bean”和“/conditions”端点,那么可以按照如下的方式设置management.endpoints.jmx.exposure.include:

1
2
3
4
5
management:
endpoints:
jmx:
exposure:
include: health,info,bean,conditions

我们只想排除其中的几个端点的话,可以按照如下的方式设置management.endpoints. jmx.exposure.exclude属性:

1
2
3
4
5
management:
endpoints:
jmx:
exposure:
exclude: env,metrics

在这里,我们使用management.endpoints.jmx.exposure.exclude排除了“/env”和“/metrics”端点。所有其他的Actuator端点依然会暴露为MBean。

要在JConsole中调用一个或多个Actuator MBean所托管的操作,可以在左侧树中展开MBean端点,然后在Operations下选择所需的操作。

例如,你想要探查tacos.ingredients包的日志级别,那么可以展开LoggersMBean并点击名为loggerLevels的操作,如图18.2所示。在右上方的表单中,在name文本域中输入包名(tacos.ingredients),然后点击loggerLevels按钮。

image-20211023113927984

图18.2 使用JConsole展现Spring Boot应用的日志级别

在点击loggerLevels按钮之后,将会弹出一个对话框,展现来自“/loggers”端点MBean的响应,大致如图18.3所示。

image-20211023113941223

图18.3 在JConsole中,“/loggers”端点MBean所展现的日志级别

尽管JConsole UI的使用方式有些笨拙,但是你应该可以掌握它的技巧并使用相同的方式来探索其他的Actuator端点。

如果你不喜欢JConsole,也没有问题,有很多其他的JMX客户端可供选择。

第18章 使用JMX监控Spring

本章内容:
  • 使用Actuator端点的MBean
  • 将Spring bean暴露为MBean
  • 发布通知

JMX(Java Management Extensions,Java管理扩展)作为监视和管理Java应用程序的标准方法已经存在超过了15年。通过暴露名为MBean(托管bean)的托管组件,外部的JMX客户端可以通过调用MBean中的操作、探查属性和监视事件来管理应用程序。

在Spring Boot应用中,JMX会自动启用。这样的话,Actuator的所有端点均会暴露为MBean。另外,它还会搭建一个很便利的环境,能够很容易地将Spring应用上下文中的bean暴露为MBean。作为探索Spring和JMX功能的开始,我们首先看一下Actuator端点是如何暴露为MBean的。