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的。

17.4 小结

  • Spring Boot Admin服务器能够消费一个或多个Spring Boot应用的Actuator端点,并在一个用户友好的Web应用中展现数据。
  • Spring Boot可以向Admin服务器注册自身,也可以通过Eureka被Admin服务器自动发现。
  • 与捕获应用状态快照的Actuator端点不同,Admin服务器可以展现应用内部运行状况的实时视图。
  • 借助Admin服务器能够很容易地过滤Actuator结果,在有些场景下,还可以以可视化图表的形式展现数据。
  • 因为Admin服务器就是一个Spring Boot应用,所以可以使用任意可用的Spring Security方式来保护它。