从零放弃学习 Spring - Spring Cloud 应用开发

Spring Cloud Consul

Spring Boot 是用来构建应用的框架,Spring Cloud 则是基于 Spring Boot 构建云上应用的框架。在开发云上应用中,一个非常重要的场景就是服务发现和配置,Spring Cloud 框架提供了服务发现和配置管理机制,只要引入对应的组件和简单的配置,就能快速接入相应的中间件。

首先介绍的就是使用 Spring Cloud Consul 组件集成 Consul 服务发现和配置管理能力。

启动 Consul Agent

在开始开发之前,先启动一个用于开发环境的 consul agent(使用 docker):

1
docker run --rm --network=host consul:1.15 agent -dev

启动成功后提供浏览器打开 http://localhost:8500/ui 访问 consul 内置的管理面板。

Discovery

依赖和配置

还是 Maven 项目,pom.xml 中引入依赖:

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
<properties>
...
<spring-cloud.version>2022.0.3</spring-cloud.version>
...
</properties>
<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
...
</dependencies>

<dependencyManagement>
<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
...
</dependencies>
</dependencyManagement>

然后要做一些配置,这个项目的配置格式用了 yaml。

首先是 application.yml

1
2
3
4
5
6
7
8
server:
port: 8080
spring:
application:
name: '@project.artifactId@'
instance_id: '${spring.cloud.client.hostname}:${server.port}'
profiles:
active: consul

spring.application.name 是必须的,可以通过 @xxx@ 引入项目的配置,spring.application.instance_id 是可选的,通过 ${xxx} 的方式引用部分静态或运行时的配置,有些示例里会配置 instance_id 包含 application.name,在 consul 里是没有必要的,还有一些示例会使用 ${random.value} 作为 instance_id 一部分,在应用意外退出时会出现一些不确定的情况。这个配置还指定了 spring.profiles.active,yaml 格式的配置文件中你可以通过数组的方式同时提供多个 profile 配置,不过我更推荐按照文件名提供多个 profile 配置。应用启动时会根据 spring.profiles.active 找对应的 application-<profile>.yml 文件,如果你没有指定 spring.profiles.active,默认值是 default。

然后就是 consul profile 对应的配置文件 application-consul.yml

1
2
3
4
5
6
7
spring:
cloud:
consul:
host: localhost
port: 8500
discovery:
catalog-services-watch-timeout: 55

服务注册和健康检查

Spring Cloud 厉害的地方就是不需要任何代码你的服务注册就成功了,运行服务后你就可以在 Consul 面板 Services 列表里看到服务名,点开服务里则是实例列表,instance_id 会被包含在实列名里(但实例处于不健康的状态)。

关于 instance_id 还有一点需要注意,就是如果使用 spring.cloud.consul.discovery.include-hostname-in-instance-id 配置,它和 spring.application.instance_id 里引用 hostname 的结果是不一样的,spring.application.instance_id 中所有不合规的字符会被替换成 -,但是直接使用 spring.cloud.consul.discovery.include-hostname-in-instance-id 不会触发替换规则,而这时候如果获取到的 hostname 是 IP 或者包含不合规字符,注册会出错。

Consul 的架构是核心 Server 组集群,近客户端部署 Agent 连接 Server 集群,客户端只和 Agent 相互通信,Agent 再把关键信息维护到 Server 集群。在使用 Consul 做服务发现时,客户端把服务信息注册到 Agent 的时候还会携带一个健康检查信息,Agent 会根据健康检查信息中的探针配置检查服务或者客户端根据 TTL 定时 keep-alive。

Spring Cloud Consul 注册服务的健康检查通过 HTTP 探针,URL是 <service>/actuator/health,这个路径来自 Spring 生态的一个组件 spring-boot-starter-actuator,这个组件会可以提供一些运维指标获取的接口:

1
2
3
4
5
6
7
8
<dependencies>
...
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
...
</dependencies>

Spring Boot 项目引入依赖后无需任何配置,开箱即用。如果不想使用这个组件,你可以直接通过 RestController + GetMapping 自定义一个 /actuator/health 接口,返回 200 状态就能满足探针条件,还可以通过配置 spring.cloud.consul.discovery.health-check-path 把探针路径修改到其他你期望的路径上。

在提供健康检查探针后,可以在 Consul 面板里观察到服务实例变成健康。

Config

使用 Consul 配置需要添加依赖

1
2
3
4
5
6
7
8
<dependencies>
...
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-config</artifactId>
</dependency>
...
</dependencies>

加入依赖后项目是没办法直接运行的,你会看到一个异常信息:

1
2
3
4
5
6
7
8
9
10
Description:

No spring.config.import property has been defined

Action:

Add a spring.config.import=consul: property to your configuration.
If configuration is not required add spring.config.import=optional:consul: instead.
To disable this check, set spring.cloud.consul.config.enabled=false or
spring.cloud.consul.config.import-check.enabled=false.

遇到这个问题如果你去网上搜索解决方案,大多告诉你,这是需要 bootstrap 配置而 Spring Cloud 默认不再支持,所以你要添加一个 spring-cloud-starter-bootstrap 依赖来使用 bootstrap 配置。实际上解决办法已经写在错误信息里了。

引入 spring-cloud-starter-consul-config 依赖后,应用在启动时会自动从 Consul 获取应用对应的配置,合并入本地配置作为启动配置,你可以通过 spring.cloud.consul.config.enabled=false 禁用这个过程,或者通过 spring.config.import=optional:consul:,来告诉框架尝试从 Consul 获取配置。

配置功能通过 Consul 的 KV 实现,配置组件会去拉取几个路径下的 KV 合并成一个配置,路径分别对应服务名专用配置和应用公共配置。假设有一个应用 spring.application.namedemospring.profiles.activedevelopment,在默认情况下,它的应用专属配置 KV 路径为 config/demo,development/,配置组件会去递归拉取路径下所有 KV,假设我们希望添加配置 app.home.helloPrefix=Hello,,我们需要在 Consul 里创建 KV config/demo,development/app/home/helloPrefix,值为 Hello,

这种基于路径方式的管理在 Consul 里维护可能会比较麻烦,根据 Consul KV Watch+递归机制,目录下一个 KV 变动,会把目录下所有 KV 全部遍历返回。所以推荐一种更简单的维护方式,通过配置 spring.cloud.consul.config.format=YAML,让配置组件直接去拉取一个固定的 KV,而不是获取整个目录。例如上面的例子,配置组件会从 config/demo,development/data 直接获取内容并通过 YAML 解析,KV 路径中的 data 可以通过 spring.cloud.consul.config.data-key 自定义。

使用 Config

Spring 框架提供了一个非常方便的注解 @Value 从配置自动装配值,如果你还需要支持配置自动刷新,只需要增加一行注解 @RefreshScope

1
2
3
4
5
6
7
8
9
10
11
12
13
@RestController
@RequestMapping("/home")
@RefreshScope
public class HomeController {

@Value("${app.home.helloPrefix:}")
String helloPrefix;

@GetMapping("/hello")
public String getHello() {
return helloPrefix + "World";
}
}

完整项目代码参考 spring-cloud-demo

服务间调用

我原计划是不打算写这个的。

在 Spring Cloud 框架里,最简单的服务调用方法就是通过 OpenFeign,这样服务提供方不需要做太大改动,只需要编写客户端接口就行了。

在上面 demo 服务基础上,我们再创建一个服务叫 consumer。

项目 pom.xml:

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
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
...
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

核心是用到了 spring-cloud-starter-openfeign,它能提供一个接入了服务发现,并且带负载均衡的客户端,用来访问其他服务。

定义客户端接口:

1
2
3
4
5
6
@FeignClient("demo")
public interface DemoClient {

@GetMapping("/hello")
String getHello();
}

注解 FeignClient 需要提供服务名,要和 Consul 上的 Service 名称对应,因为 spring.application.name 不一定等价于 Consul 上注册的名称。

然后就是在 Bean 里使用客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@RestController
@EnableDiscoveryClient(autoRegister = false)
@EnableFeignClients
@SpringBootApplication
public class ConsumerApplication {

static final Logger log = LoggerFactory.getLogger(ConsumerApplication.class);

@Autowired
DemoClient demoClient;

@GetMapping("/invoke")
String invoke() {
log.info("invoke");
return demoClient.getHello();
}

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

}

这里我为了方便把示例代码直接放 Application 类里了。

EnableDiscoveryClient 注解默认情况下不需要写,会默认开启,这里写注解 @EnableDiscoveryClient(autoRegister = false) 是为了禁用注册服务到 Service Registry,因为 Consumer 服务只需要调用其他服务。

EnableFeignClients 注解用来启用 OpenFeign 客户端,可以自定义客户端的类或者扫描的包。

完整项目代码参考 spring-cloud-consumer

Nacos

Nacos 是国服非常热门的一个分布式中间件,作用和 Consul 差不多,场景更加垂直。阿里提供了 Nacos 关于 Spring 集成的组件,使用也很方便。参考之前的项目,把 Consul 相关的依赖替换成 Nacos 对应的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<version>2022.0.0.0-RC1</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
<version>2022.0.0.0-RC1</version>
</dependency>
</dependencies>

新增配置文件 application-naocs.yml:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
nacos:
config:
file-extension: yaml
server-addr: localhost:8848
discovery:
server-addr: localhost:8848
config:
import: 'optional:nacos:${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}'

然后把 spring.profiles.active 设为 nacos,就可以方便地把 Consul 换成 Nacos。

这里要注意的是 Nacos 配置组件需要 spring.config.import 提供 dataId,我们按照 Nacos 默认的 dataId 构造格式 ${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension} 就行了。

如果在代码中没有直接依赖 Consul 的组件,这两个中间件互换的开发成本是不大的。

最后

本来计划一篇里把 Spring Cloud Gateway 和 Spring Cloud Kubernetes 都聊了,但是写烦了,这两部分拆两篇单独讲。

参考