3.3 将Spring Cloud Config与Spring Boot客户端集成
在上一章中,我们构建了一个简单的许可证服务框架,这个框架只是返回一个代表数据库中单个许可记录的硬编码Java对象。在下一个示例中,我们将构建许可证服务,并与持有许可数据的Postgres数据库进行交流。
我们将使用Spring Data与数据库进行通信,并将数据从许可证表映射到保存数据的POJO。数据库连接和一条简单的属性将从Spring Cloud配置服务器中读出。图3-6展示了许可证服务和Spring Cloud配置服务之间的交互。
图3-6 使用开发环境profile检索配置信息
当许可证服务首次启动时,将通过命令行传递两条信息:Spring的profile和许可证服务用于与Spring Cloud配置服务通信的端点。Spring的profile值映射到为Spring服务检索属性的环境。当许可证服务首次启动时,它将通过从Spring的profile传入构建的端点与Spring Cloud Config服务进行联系。然后,Spring Cloud Config服务将会根据URI上传递过来的特定Spring profile,使用已配置的后端配置存储库(文件系统、Git、Consul或Eureka)来检索相应的配置信息,然后将适当的属性值传回许可证服务。接着,Spring Boot框架将这些值注入应用程序的相应部分。
3.3.1 建立许可证服务对Spring Cloud Config服务器的依赖
让我们把焦点从配置服务器转移到许可证服务。我们需要做的第一件事,就是在许可证服务中为Maven文件添加更多的条目。代码清单3-4展示了需要添加的条目。
代码清单3-4 许可证服务所需的其他Maven依赖项
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId> ⇽--- 告诉Spring Boot将要在服务中使用Java Persistence API(JPA) </dependency>
<dependency>
<groupId>postgresql</groupId>
<artifactId>postgresql</artifactId> ⇽--- 告诉Spring Boot拉取Postgres JDBC驱动程序
<version>9.1-901.jdbc4</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-config-client</artifactId> ⇽--- 告诉Spring Boot拉取Spring Cloud Config客户端所需的所有依赖项 </dependency>
第一个和第二个依赖项spring-boot-starter-data-jpa和postgresql导入了Spring Data Java Persistence API(JPA)和Postgres JDBC驱动程序。最后一个依赖项是spring-cloud-config-client,它包含与Spring Cloud配置服务器交互所需的所有类。
3.3.2 配置许可证服务以使用Spring Cloud Config
在定义了Maven依赖项后,需要告知许可证服务在哪里与Spring Cloud配置服务器进行联系。在使用Spring Cloud Config的Spring Boot服务中,配置信息可以在bootstrap.yml和application.yml这两个配置文件之一中设置。
在其他所有配置信息被使用之前,bootstrap.yml文件要先读取应用程序属性。一般来说,bootstrap.yml文件包含服务的应用程序名称、应用程序profile和连接到Spring Cloud Config服务器的URI。希望保留在本地服务(而不是存储在Spring Cloud Config中)的其他配置信息,都可以在服务中的application.yml文件中进行本地设置。通常情况下,即使Spring Cloud Config服务不可用,我们也会希望存储在application.yml 文件中的配置数据可用。bootstrap.yml 和application.yml保存在项目的src/main/resources文件夹中。
要使许可证服务与Spring Cloud Config服务进行通信,需要添加一个licensing-service/src/ main/resources/bootstrap.yml文件,并设置3个属性,即spring.application.name、spring. profiles.active和spring.cloud.config.uri。
代码清单3-5展示了许可证服务的bootstrap.yml文件。
代码清单3-5 配置许可证服务的bootstrap.yml文件
spring:
application:
name: licensingservice ⇽--- 指定许可证服务的名称,以便Spring Cloud Config客户端知道正在查找哪个服务
profiles:
active:
default ⇽--- 指定服务应该运行的默认profile。profile映射到环境
cloud:
config:
uri: http://localhost:8888 ⇽--- 指定Spring Cloud Config服务器的位置
注意
Spring Boot应用程序支持两种定义属性的机制:YAML(Yet another Markup Language)和使用“.”分隔的属性名称。我们选择YAML作为配置应用程序的方法。YAML属性值的分层格式直接映射到spring.application.name、spring.profiles.active和spring.cloud.config.uri名称。
spring.application.name是应用程序的名称(如licensingservice)并且必须直接映射到Spring Cloud配置服务器中的目录的名称。对于许可证服务,需要在Spring Cloud配置服务器上有一个名为licensingservice的目录。
第二个属性spring.profiles.active用于告诉Spring Boot应用程序应该运行哪个profile。profile是区分Spring Boot应用程序要使用哪个配置数据的机制。对于许可证服务的profile,我们将支持服务的环境直接映射到云配置环境中。例如,通过作为profile传入开发环境,Spring Cloud配置服务器将使用开发环境的属性。如果没有设置profile,许可证服务将使用默认profile。
第三个也是最后一个属性spring.cloud.config.uri是许可证服务查找Spring Cloud配置服务器端点的位置。在默认情况下,许可证服务将在http://localhost:8888上查找配置服务器。在本章的后面,读者将看到如何在应用程序启动时覆盖boostrap.yml和application.yml文件中定义的不同属性,这样可以告知许可证微服务应该运行哪个环境。
现在,如果启动Spring Cloud配置服务,并在本地计算机上运行相应的Postgres数据库,那么就可以使用默认profile启动许可证服务。这可以通过切换到许可证服务的目录并执行以下命令来完成:
mvn spring-boot: run
通过运行此命令而不设置任何属性,许可证服务器将自动尝试使用端点(http://localhost: 8888)和在许可证服务的bootstrap.yml文件中定义的活跃profile(默认),连接到Spring Cloud配置服务器。
如果要覆盖这些默认值并指向另一个环境,可以通过将许可证服务项目编译到JAR,然后使用-D系统属性来运行这个JAR来实现。下面的命令行演示了如何使用非默认profile启动许可证服务:
java -Dspring.cloud.config.uri=http://localhost:8888 \
-Dspring.profiles.active=dev \
-jar target/licensing-service-0.0.1-SNAPSHOT.jar
使用上述命令行将覆盖两个参数,即spring.cloud.config.uri和spring.profiles. active。
使用-Dspring.cloud.config.uri=http://localhost:8888系统属性将指向一个本地运行的配置服务器。
注意
如果读者尝试从自己的台式机上使用上述的Java命令来运行从本章的GitHub存储库下载的许可证服务,将会运行失败,这是因为没有运行桌面Postgres服务器,并且GitHub存储库中的源代码在配置服务器上使用了加密。本章稍后将介绍加密。前面的例子演示了如何通过命令行来覆盖Spring属性。
使用-Dspring.profiles.active=dev系统属性,可以告诉许可证服务使用开发环境profile(从配置服务器读取),从而连接到开发环境的数据库的实例。
使用环境变量传递启动信息
在这些示例中,将这些值硬编码传递给-D参数值。在云中所需的大部分应用程序配置数据都将位于配置服务器中。但是,对于启动服务所需的信息(如配置服务器的数据),则需要启动VM实例或Docker容器并传入环境变量。
本书每章的所有代码示例都可以在Docker容器中完全运行。使用Docker,我们可以通过特定环境的Docker-compose文件来模拟不同的环境,从而协调所有服务的启动。容器所需的特定环境值作为环境变量传递到容器。例如,要在开发环境中启动许可证服务,docker/dev/docker-compose.yml文件要包含以下用于许可证服务的条目:
licensingservice:
image: ch3-thoughtmechanix/licensing-service
ports: - "8080:8080"
environment: ⇽--- 指定许可证服务容器的环境变量的开始
PROFILE: "dev" ⇽--- PROFILE环境变量被传递给Spring Boot服务命令行,告诉Spring Boot应该运行哪个profile
CONFIGSERVER_URI: http://configserver:8888 ⇽--- 配置服务的端点
CONFIGSERVER_PORT: "8888"
DATABASESERVER_PORT: "5432"
该文件中的环境条目包含两个变量PROFILE的值,这是许可证服务将要运行的Spring Boot profile。
CONFIGSERVER_URI被传递给许可证服务,该属性定义了Spring Cloud配置服务器实例
的地址,服务将从该URI读取其配置数据的。
在由容器运行的启动脚本中,我们将这些环境变量以-D参数传递到启动应用程序的JVM。
在每个项目中,可以制作一个Docker容器,然后该Docker容器使用启动脚本启动该容器中的软件。
对于许可证服务,容器中的启动脚本位于licensing-service/src/main/docker/run.sh中。
在run.sh脚本中,以下条目负责启动许可证服务的JVM:
echo "********************************************************"
echo "Starting License Server with $CONFIGSERVER_URI";
echo "********************************************************"
java -Dspring.cloud.config.uri=$CONFIGSERVER_URI
-Dspring.profiles.active=$PROFILE
-jar /usr/local/licensingservice/licensing-service-0.0.1-SNAPSHOT.jar
因为我们是通过 Spring Boot Actuator 来增强服务的自我检查能力的,所以可以通过访问http://localhost:8080/env来确认正在运行的环境。/env端点将提供有关服务的配置信息的完整列表,包括服务启动的属性和端点,如图3-7所示。
图3-7 可以通过调用/env端点来检查许可证服务加载的配置
图3-7中要注意的关键是,许可证服务的活跃profile是dev。通过观察返回的JSON,还可以看到被返回的Postgres数据库URI是开发环境URI:jdbc:postgresgl://database: 5432/eagle-ege-dev。
暴露太多的信息
围绕如何为服务实现安全性,每个组织都会有自己的规则。许多组织认为,服务不应该广播任何有关自己的信息,也不允许像/env端点这样的东西在服务上存在,因为他们相信(这是理所当然的)这样会为潜在的黑客提供太多的信息。Spring Boot为配置Spring Actuator端点返回的信息提供了丰富的功能,这些知识超出了本书的范围。Craig Walls的优秀著作《Spring Boot实战》详细介绍了这个主题,我强烈建议读者回顾一下企业安全策略并阅读
Walls的书,以便能够提供想通过Spring Actuator公开的正确级别的细节。
3.3.3 使用Spring Cloud配置服务器连接数据源
至此我们已将数据库配置信息直接注入微服务中。数据库配置设置完毕后,配置许可证微服务就变成使用标准Spring组件来构建和从Postgres数据库中检索数据的练习。许可证服务已被重构成不同的类,每个类都有各自独立的职责。这些类如表3-2所示。
表3-2 许可证服务的类及其所在位置
类名
|
位置
|
License
|
licensing-service/src/main/java/com/thoughtmechanix/licenses/model
|
LicenseRepository
|
licensing-service/src/main/java/com/thoughtmechanix/licenses/repository
|
LicenseService
|
licensing-service/src/main/java/com/thoughtmechanix/licenses/services
|
License类是模型类,它将持有从许可数据库检索的数据。代码清单3-6展示了License类的代码。
代码清单3-6 单个许可证记录的JPA模型代码
package com.thoughtmechanix.licenses.model;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
@Entity ⇽--- @Entity注解告诉Spring这是一个JPA类
@Table(name = "licenses") ⇽--- @Table映射到数据库的表
public class License{
@Id ⇽--- @Id将该字段标记为主键
@Column(name = "license_id", nullable = false) ⇽--- @Column 将该字段映射到特定数据库表中的列
private String licenseId;
@Column(name = "organization_id", nullable = false)
private String organizationId;
@Column(name = "product_name", nullable = false)
private String productName; /*为了简洁,省略了其余的代码*/ }
这个类使用了多个Java持久性注解(Java Persistence Annotations,JPA),
帮助Spring Data框架将Postgres数据库中的licenses表中的数据映射到Java对象。
@Entity注解让Spring知道这个Java POJO将要映射保存数据的对象。
@Table注解告诉Spring JPA应该映射哪个数据库表。
@Id注解标识数据库的主键。最后,数据库中的每一列将被映射到由@Column标记的各个属性。
Spring Data和JPA框架提供访问数据库的基本CRUD方法。如果要构建其他方法,
可以使用Spring Data存储库接口和基本命名约定来进行构建。Spring将在启动时从Repository接口解析方法的名称,并将它们转换为基于名称的SQL语句,然后在幕后生成一个动态代理类来完成这项工作。
代码清单3-7展示了许可证服务的存储库。
代码清单3-7 LicenseRepository接口定义查询方法
package com.thoughtmechanix.licenses.repository;
import com.thoughtmechanix.licenses.model.License;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository ⇽--- 告诉Spring Boot这是一个JPA存储库类
public interface LicenseRepository extends CrudRepository<License,String> ⇽--- 定义正在扩展Spring CrudRepository
{
public List<License> findByOrganizationId(String organizationId); ⇽--- 每个查询方法被Spring解析为SELECT...FROM查询
public License findByOrganizationId(String organizationId,String licenseId);
}
存储库接口LicenseRepository用@Repository注解标记,这个注解告诉Spring应该将这个接口视为存储库并为它生成动态代理。
Spring提供不同类型的数据访问存储库。我们选择使用Spring CrudRepository基类来扩展LicenseRepository类。
CrudRepository基类包含基本的CRUD方法。除了从CrudRepository扩展的CRUD方法外,
我们还添加了两个用于从许可表中检索数据的自定义查询方法。
Spring Data框架将拆开这些方法的名称以构建访问底层数据的查询。
注意
Spring Data框架提供各种数据库平台上的抽象层,并不仅限于关系数据库。该框架还支持NoSQL数据库,如MongoDB和Cassandra。
与第2章中的许可证服务不同,我们现在已将许可证服务的业务逻辑和数据访问逻辑从LicenseController中分离出来,
并划分在名为LicenseService的独立服务类中(如代码清单3-8所示)。
代码清单3-8 用于执行数据库命令的LicenseService类
package com.thoughtmechanix.licenses.services;
import com.thoughtmechanix.licenses.config.ServiceConfig;
import com.thoughtmechanix.licenses.model.License;
import com.thoughtmechanix.licenses.repository.LicenseRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.UUID;
@Service
public class LicenseService {
@Autowired
private LicenseRepository licenseRepository;
@Autowired ServiceConfig config;
public License getLicense(String organizationId,String licenseId) {
License license = licenseRepository.findByOrganizationIdAndLicenseId(organizationId, licenseId);
return license.withComment(config.getExampleProperty());
}
public List<License> getLicensesByOrg(String organizationId){
return licenseRepository.findByOrganizationId(organizationId);
}
public void saveLicense(License license){
license.withId( UUID.randomUUID().toString());
licenseRepository.save(license);
} /*为了简洁,省略了其余的代码*/
}
使用标准的Spring @Autowired注解将控制器、服务和存储库类连接到一起。