Spring 和 Cloud Foundry 的 12 因素应用风格支持服务

工程 | Josh Long | 2015 年 1 月 27 日 | ...

The 12 因素应用宣言详细讨论了支持服务。支持服务基本上是您的应用程序为完成其工作而使用的任何联网服务。这可能是一个 MongoDB 实例、PostgreSQL 数据库、像 Amazon S3 这样的二进制存储、像 New Relic 这样的指标收集服务、RabbitMQ 或 ActiveMQ 消息队列、Memcached 或基于 Redis 的缓存、FTP 服务、电子邮件服务或任何其他服务。这里的区别与其说是服务的内容,不如说是服务如何在应用程序中公开和使用。对应用程序来说,两者都是附加资源,通过 URL 或其他存储在配置中的定位符/凭据访问。

我们研究了如何使用配置将定位符和凭据等“魔术字符串”从应用程序代码中分离出来并外部化。我们还研究了使用服务注册表在动态(通常是云)环境中维护微服务的一种活的电话簿

在这篇文章中,我们将研究平台即服务 (PaaS) 环境(如Cloud Foundry 或 Heroku)通常如何公开支持服务,并探讨如何在 Spring 应用程序内部使用这些服务。对于我们的示例,我们将使用 Cloud Foundry,因为它开源且易于在任何数据中心或托管环境中运行,尽管其中大部分内容也很容易应用于 Heroku。

我的朋友Abby Gregory KearnsCloud Foundry 支持服务组合的作用和价值进行了非常好的高级概述。

像 Cloud Foundry 这样的 PaaS 将支持服务作为操作系统进程本地环境变量公开。环境变量很方便,因为它们适用于所有语言和运行时,并且很容易在不同环境之间更改。这比尝试在本地机器上启动并运行 JNDI 要简单得多,并促进了可移植构建。我打算在这篇文章中专门从当前 Cloud Foundry 的角度来看待支持服务。但请记住,这种方法专门旨在促进在云环境外部的可移植构建。Spring 是为可移植性量身定制的;依赖注入促进了解耦 bean(例如,来自支持服务的 bean)的初始化和获取逻辑与其使用位置的分离。我们可以使用 Spring 编写处理 javax.sql.DataSources 的代码,然后编写配置,以便在应用程序从一个环境移动到另一个环境时,从正确的上下文和配置中获取该 DataSource

Cloud Foundry 下一版本(迄今为止在新闻中被称为Diego)的运行时是 Docker 优先且原生的。当然,Docker 使应用程序容器化变得容易,并且容器化应用程序与外部世界之间的接口故意保持最小化,这同样是为了促进应用程序的可移植性。正如您所料,Docker 镜像中的一个关键输入是环境变量!我们的朋友 Chris Richardson 写了几篇关于打包和构建基于 Spring Boot 的 Docker 镜像以及建立支持服务的好文章。在这篇文章中我们不会讨论 Docker(不过请持续关注!),但重要的是要理解:环境变量是外部化支持服务连接信息的简单灵活的方式。

一个与 JDBC DataSource 通信的简单 Spring Boot 应用程序

这是一个简单的 Spring Boot 应用程序,它从一个 DataSource bean 中插入并公开一些记录,Spring Boot 将自动为我们创建这个 bean,因为我们的 CLASSPATH 中有 H2 嵌入式数据库驱动程序。如果 Spring Boot 没有检测到类型为 javax.sql.DataSource 的 bean,但确实检测到嵌入式数据库驱动程序(H2、Derby、HSQL),它将自动创建一个嵌入式 javax.sql.DataSource bean。这个示例使用 JPA 将记录映射到数据库。以下是 Maven 依赖项:

Group ID Artifact ID
com.h2database h2
org.springframework.boot spring-boot-starter-data-jpa
org.springframework.boot spring-boot-starter-data-rest
org.springframework.boot spring-boot-starter-test
org.springframework.boot spring-boot-starter-actuator

以下是示例 Java 代码

package demo;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import java.util.Arrays;

@SpringBootApplication
public class DemoApplication {

	public static void main(String[] args) {
		SpringApplication.run(DemoApplication.class, args);
	}

	@Bean
	CommandLineRunner seed(ReservationRepository rr) {
		return args -> Arrays.asList("Phil,Webb", "Josh,Long", "Dave,Syer", "Spencer,Gibb").stream()
			.map(s -> s.split(","))
			.forEach(namePair -> rr.save(new Reservation(namePair[0], namePair[1])));
	}
}

@RepositoryRestResource
interface ReservationRepository extends JpaRepository<Reservation, Long> {
}

@Entity
class Reservation {

	@Id
	@GeneratedValue
	private Long id;

	private String firstName, lastName;

	Reservation() {
	}

	public Reservation(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	public Long getId() {
		return id;
	}

	public String getFirstName() {
		return firstName;
	}

	public String getLastName() {
		return lastName;
	}
}

在 Cloud Foundry 中创建和绑定支持服务

该应用程序可以在本地运行,但现在让我们将其转移到 Cloud Foundry 上运行,在那里需要告知它如何连接到为其公开的支持服务(一个 PostgreSQL 实例)。

Cloud Foundry 有多种形式。像 Heroku 一样,您可以在基于 AWS 的托管版本上运行它,该版本可作为 Pivotal Web Services 提供。您可以获得免费试用账户。如果您愿意,可以使用 Pivotal 的封装版本Pivotal Cloud Foundry,并在您自己的数据中心运行。或者,您也可以使用开源版本来运行。或者使用来自IBMHP等众多其他实现中的任何一种。无论如何,您最终将拥有一个在支持服务方面表现基本相同的 PaaS。

添加支持服务是一个声明性功能:只需创建服务,然后将其绑定。Cloud Foundry 提供了一个市场命令:cf marketplace。对于 80% 的情况,您应该能够从 cf marketplace 输出的选项中进行选择。选择服务后,创建一个实例,如下所示:

cf create-service elephantsql turtle postgresql-db

特别注意,postgresql-db 是支持服务名称,elephantsql 是服务提供商名称,turtle 是(免费)层级计划名称。有了这些信息,您可以将服务绑定到已部署的应用程序。您可以使用 cf 命令行界面,或者只需在应用程序的 manifest.yml 文件中声明支持服务依赖项。

这是我们所有示例的基本 manifest.yml 文件。namehost 在不同的清单文件中会变化,但本文的示例使用这个新创建的 postgresql-db 服务。

---
applications:
- name: simple-backing-services
	memory: 512M
	instances: 1
	host: simple-backing-services-${random-word}
	domain: cfapps.io
	path: target/simple.jar
	services:
		- postgresql-db
	env:
		SPRING_PROFILES_ACTIVE: cloud
		DEBUG: "true"
		debug: "true"

现在,让我们看看几种不同的使用支持服务的方式,并了解它们的一些优缺点。

对于所有这些示例,我们将至少使用Spring Boot。Spring Boot 提供了 Spring Boot Actuator 模块。它提供了关于应用程序的非常有用的信息——指标、环境转储、所有已定义 bean 的列表等。我们将使用其中一些端点来深入了解运行 Spring 应用程序时公开的环境变量和系统属性,并了解诸如 Spring Boot 正在运行的 profile 等信息。在您的 Maven 或 Gradle 构建中添加 Spring Boot Actuator。groupIdorg.springframework.bootartifactIdspring-boot-starter-actuator。如果您使用的是从start.spring.io生成的 Spring Boot 或 spring init 命令行命令,则无需指定版本。

Cloud Foundry 的自动重配置

The Cloud Foundry Java buildpack 为您进行自动重配置根据文档

自动重配置包含三个部分。首先,它将 cloud profile 添加到 Spring 的活动 profile 列表中。其次,它将 Cloud Foundry 贡献的所有属性作为 ApplicationContext 中的一个 PropertySource 公开。最后,它重写各种类型的 bean 定义,以便自动连接到绑定到应用程序的服务。重写的类型如下:

Bean 类型 服务类型
javax.sql.DataSource 关系数据服务(例如 ClearDB, ElephantSQL)
org.springframework.amqp.rabbit.connection.ConnectionFactory RabbitMQ 服务(例如 CloudAMQP)
org.springframework.data.mongodb.MongoDbFactory Mongo 服务(例如 MongoLab)
org.springframework.data.redis.connection.RedisConnectionFactory Redis 服务(例如 Redis Cloud)
org.springframework.orm.hibernate3.AbstractSessionFactoryBean 关系数据服务(例如 ClearDB, ElephantSQL)
org.springframework.orm.hibernate4.LocalSessionFactoryBean 关系数据服务(例如 ClearDB, ElephantSQL)
org.springframework.orm.jpa.AbstractEntityManagerFactoryBean 关系数据服务(例如 ClearDB, ElephantSQL)

结果是,一个在本地 H2 上运行的应用程序将自动在 Cloud Foundry 上针对 PostgreSQL 运行,前提是您已经创建并绑定了一个指向 PostgreSQL 实例的支持服务到应用程序,就像我们上面所做的那样。这非常方便!如果您的唯二目标是 localhost 和 Cloud Foundry,这种方式完美适用。

Spring 应用程序中有用的 Environment 属性

默认的 Java buildpack(您可以在执行 cf push 时或在 manifest.yml 中声明来覆盖它)会添加一个 Spring EnvironmentPropertySource,注册一系列以 cloud. 开头的属性。一旦您将应用程序推送到 Cloud Foundry,如果您访问上述应用程序中的 REST /env 端点,您就可以看到它们。以下是我应用程序的部分输出:


{
	...
	cloud.services.postgresql-db.connection.jdbcurl: "jdbc:postgresql://babar.elephantsql.com:5432/AUSER?user=AUSER&password=WOULDNTYOULIKETOKNOW",
	...
	cloud.services.postgresql-db.connection.uri: "postgres://AUSER:[email protected]:5432/AUSER",
	cloud.services.postgresql-db.connection.scheme: "postgres",
	cloud.services.postgresql.connection.jdbcurl: "jdbc:postgresql://babar.elephantsql.com:5432/AUSER?user=AUSER&password=WOULDNTYOULIKETOKNOW",
	cloud.services.postgresql.connection.port: 5432,
	cloud.services.postgresql.connection.path: "AUSER",
	cloud.application.host: "0.0.0.0",
	cloud.services.postgresql-db.connection.password: "******",
	cloud.services.postgresql-db.connection.username: "AUSER",
	...
	cloud.application.application_name: "simple-backing-services",
	cloud.application.limits: {
		mem: 512,
		disk: 1024,
		fds: 16384
	},
	cloud.services.postgresql-db.id: "postgresql-db",
	cloud.application.application_uris: [
	"simple-backing-services-fattiest-teniafuge.cfapps.io",
	"simple-backing-services-unmummifying-prehnite.cfapps.io"
],
	cloud.application.instance_index: 0,
	...
}

您可以在 Spring 中像使用任何其他属性一样使用这些属性。它们也非常方便,因为它们不仅提供了相当标准的 Heroku 风格的连接 URI (cloud.services.postgresql-db.connection.uri),还提供了一个可以直接在 JDBC 上下文中使用 的 URI (cloud.services.postgresql.connection.jdbcurl)。只要您使用这个(或这个的分支)buildpack,您就能享受到这些属性带来的好处。

Cloud Foundry 将所有这些信息作为标准、语言和技术中立的环境变量 (VCAP_SERVICESVCAP_APPLICATION) 公开。理论上,您应该能够为任何 Cloud Foundry 实现编写应用程序并以这些变量为目标。Spring Boot 也为这些变量提供了自动配置,无论您是否使用前面提到的 Java buildpack,这种方法都有效。

Spring Boot 将这些环境变量映射到一组可以从 Spring Environment 抽象访问的属性。以下是 Spring Boot 公开的 VCAP_* 属性的一些示例输出,同样来自 /env 端点:

{
	...
	vcap.application.start: "2015-01-27 09:58:13 +0000",
	vcap.application.application_version: "9e6ba76e-039f-4585-9573-8efa9f7e9b7e",
	vcap.application.application_uris[2]: "simple-backing-services-detersive-sterigma.cfapps.io",
	vcap.application.uris: "simple-backing-services-fattiest-teniafuge.cfapps.io,simple-backing-services-grottoed-distillment.cfapps.io,...",
	vcap.application.space_name: "joshlong",
	vcap.application.started_at: "2015-01-27 09:58:13 +0000",
	vcap.services.postgresql-db.tags: "Data Stores,Data Store,postgresql,relational,New Product",
	vcap.services.postgresql-db.credentials.uri: "postgres://AUSER:[email protected]:5432/hqsugvxo",
	vcap.services.postgresql-db.tags[1]: "Data Store",
	vcap.services.postgresql-db.tags[4]: "New Product",
	vcap.application.application_name: "simple-backing-services",
	vcap.application.name: "simple-backing-services",
	vcap.application.uris[2]: "simple-backing-services-detersive-sterigma.cfapps.io",
	...
}

我倾向于稍微依赖每种方法。Spring Boot 属性很方便,因为它们提供了索引属性。vcap.application.application_uris[2] 提供了一种索引此应用程序可能路由数组的方式。这对于您想告知正在运行的应用程序其外部可访问 URI 是什么非常理想,例如,如果它需要在任何请求到来之前在启动时建立回调。它还提供了等效的技术无关 URI,但不提供 JDBC 特定的连接字符串。因此,我会同时使用这两种方法。这种方法很方便,特别是在 Spring Boot 中,因为我可以在配置中明确设置属性(例如 spring.datasource.*),以指导 Spring Boot 如何进行设置。这有助于明确性,或者如果我的同一个应用程序绑定了多个相同类型的支持服务(例如 JDBC javax.sql.DataSource)。在这种情况下,buildpack 将不知道如何处理,因此您需要明确指定应该注入哪个支持服务引用以及注入到哪里。

使用 Spring Profiles

默认情况下,Spring Boot 加载 src/main/resources/application.(properties,yml)。它还会加载特定 profile 的属性文件,形式为 src/main/resources/application-PROFILE.yml,其中 PROFILE活动 Spring profile 的名称。前面我们看到,我们的 manifest.yml 通过设置环境变量明确激活了 cloud profile。所以,假设您想要一种仅在 cloud profile 中运行时激活的配置,以及另一种在没有特定 profile 激活时激活的配置——这称为 default profile。您可以创建三个文件:src/main/resources/application-cloud.(properties,yml),它将在 cloud profile 被激活时激活;src/main/resources/application-default.(properties,yml),它将在没有其他 profile 被明确激活时激活;以及 src/main/resources/application.(properties,yml),它将在所有情况下都被激活,无论如何。

A sample src/main/resources/application.properties

spring.jpa.generate-ddl=true

A sample src/main/resources/application-cloud.properties

spring.datasource.url=${cloud.services.postgresql-db.connection.jdbcurl}

A sample src/main/resources/application-default.properties

# empty in this case because I rely on the embedded H2 instance being created
# though you could point it to another, local,
# PostgresSQL instancefor dev workstation configuration

使用 Spring Cloud PaaS 连接器

到目前为止,所有这些选项都利用了 Environment 抽象。毫无疑问,它们比手动解析 VCAP_SERVICES 变量中的 JSON 结构要简单得多,但我们可以做得更好。正如Spring Cloud Connectors项目的文档所述:

Spring Cloud 为在云平台上运行的基于 JVM 的应用程序提供了一个简单的抽象,以便在运行时发现绑定的服务和部署信息,并支持将发现的服务注册为 Spring bean。它基于插件模型,因此相同的编译应用程序可以部署在本地或多个云上,并通过 Java SPI 支持自定义服务定义。

Let's look at our revised example

package demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.config.java.AbstractCloudConfig;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.rest.core.annotation.RepositoryRestResource;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.sql.DataSource;

@SpringBootApplication
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }

    @Configuration
    @Profile("cloud")
    public static class DataSourceConfig extends AbstractCloudConfig {

        @Bean
        DataSource reservationsPostgreSqlDb() {
            return connectionFactory().dataSource("postgresql-db");
        }
    }

}

@RepositoryRestResource
interface ReservationRepository extends JpaRepository<Reservation, Long> {
}

@Entity
class Reservation {
    @Id
    @GeneratedValue
    private Long id;

    public Long getId() {
        return id;
    }

    private String firstName, lastName;

    Reservation() {
    }

    public Reservation(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }

    public String getFirstName() {
        return firstName;
    }

    public String getLastName() {
        return lastName;
    }
}

我们不再使用属性和 Spring Boot 配置 javax.sql.DataSource,而是明确地创建正确类型的对象。对于其他支持服务,如MongoDBRedisSendGrid 等,也有其他方法,并且您可以轻松地提供自己的方法。我们正在使用 Cloud Foundry 的 Spring Cloud Connectors 插件,但您完全可以使用 Heroku 的 Spring Cloud Connectors 插件,或者本地应用程序基于属性的替代方案。这样,您的应用程序在不同环境之间是相同的,只有外部配置不同。

使用 Java 配置的 @Beans

到目前为止,我们依赖于平台或框架提供的常识性默认值,但您无需放弃任何控制权。例如,您可以在 XML 或 Java 配置中明确定义一个 bean,使用环境中的值。如果您的应用程序想要使用自定义连接池或以其他方式自定义支持服务的配置,您可能会这样做。如果平台和框架对您尝试使用的支持服务没有自动支持,您也可能会这样做。


	@Bean
	@Profile("cloud")
	DataSource dataSource(
			@Value("${cloud.services.postgresql-db.connection.jdbcurl}") String jdbcUrl) {
		try {
			return new SimpleDriverDataSource(
				org.postgresql.Driver.class.newInstance() , jdbcUrl);
		}
		catch (Exception e) {
			throw new RuntimeException(e) ;
		}
	}

接下来的方向?

Thus far we've looked only at consuming services that Cloud Foundry exposes. If you want to consume a service that you've got off-PaaS, it's easy enough to treat it like any other backing service using Cloud Foundry 的用户提供服务。这种机制只是一个巧妙的方式,用来告诉 Cloud Foundry 关于您希望应用程序与之通信的自定义服务的定位器和凭据信息。一旦完成这些,Cloud Foundry 应用程序和服务就可以将该服务绑定到它们的应用程序并像往常一样使用它。用户提供服务非常适合那些您不打算让 Cloud Foundry 管理的服务,例如固定的 Oracle 实例。Cloud Foundry 不会添加新实例,也不会删除它们,并且不控制授权。

如果您希望 Cloud Foundry 管理某个服务,您需要使用服务代理 API将其适配到 Cloud Foundry。这在您自己的环境中部署 Cloud Foundry 时更为重要,并且需要管理员权限(例如,在托管的 Pivotal Cloud Foundry 中您没有)。服务代理 API 是一组 Cloud Foundry 需要了解的知名 REST 回调。实现您自己的自定义服务代理非常容易,甚至还有一个方便的基于 Spring Boot 的项目相应的示例

也请查看这些示例

获取 Spring 新闻通讯

订阅 Spring 新闻通讯,保持联系

订阅

快人一步

VMware 提供培训和认证,助您加速进步。

了解更多

获取支持

Tanzu Spring 通过简单的订阅为您提供 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区的所有即将举行的活动。

查看全部