Spring Cloud Commons 主要包括如下模块的接口和默认实现:
其中的限流策略以及重试策略是没有天然带的,但是其他模块的实现一般会带上这些功能。我们先从服务发现相关接口开始分析
服务发现相关
核心接口DiscoveryClient
public interface DiscoveryClient extends Ordered {
int DEFAULT_ORDER = 0;
//描述
String description();
//通过 serviceId 获取服务实例
List<ServiceInstance> getInstances(String serviceId);
//获取所有服务的名称
List<String> getServices();
@Override
default int getOrder() {
return DEFAULT_ORDER;
}
}
DiscoveryClient 扩展了 Ordered
接口,这个和之前提到的@Order
注解的作用是一样的。
服务实例的信息包括:
public interface ServiceInstance {
//实例id,并不是必须的
default String getInstanceId() {
return null;
}
//服务id,用于区分不同微服务
String getServiceId();
//服务实例提供服务的地址
String getHost();
//服务实例提供服务的端口
int getPort();
//是否使用的是 HTTPS
boolean isSecure();
//提供服务的 URI 地址
URI getUri();
//一些元数据信息
Map<String, String> getMetadata();
//使用的传输协议,例如 http,https 等等
default String getScheme() {
return null;
}
}
Spring Cloud 从 Feinchley 版本之后,越来越重视异步 Reactor 编程与 WebFlux,所以所有同步的接口基本上都有对应的异步接口,这里的DiscoveryClient
对应的就是ReactiveDiscoveryClient
:
public interface ReactiveDiscoveryClient extends Ordered {
int DEFAULT_ORDER = 0;
//描述
String description();
//通过 serviceId 获取服务实例,这里返回的是 Flux,究竟如何使用会在后面的例子中详细阐明
Flux<ServiceInstance> getInstances(String serviceId);
//获取所有服务的名称,这里返回的是 Flux,究竟如何使用会在后面的例子中详细阐明
Flux<String> getServices();
@Override
default int getOrder() {
return 0;
}
}
如何通过配置文件配置服务实例?
使用 SimpleDiscoveryClient
与 SimpleReactiveDiscoveryClient
假设要调用的微服务的域名是固定的,我们可以直接通过将这些域名写入配置文件。这个场景一般发生在:
- 基于 Kubernetes ingress nginx 与 coredns 的内网域名解析负载均衡
- 外网统一提供服务的域名
我们通过一个例子来说明下 SimpleDiscoveryClient
与 SimpleReactiveDiscoveryClient
,这里的代码可以从这里下载,首先引入依赖:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.1</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-commons</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2020.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
编写application.yml,这里列举了SimpleDiscoveryClient
与SimpleReactiveDiscoveryClient
所有可能的配置:
spring:
cloud:
discovery:
client:
# SimpleDiscoveryClient与SimpleReactiveDiscoveryClient的配置
simple:
instances:
#微服务1
service1:
#实例1
- host: instance1 #地址
port: 8080 #端口
instanceId: 'instance1:8080' #可以不填,实例id
#实例2
- uri: 'https://instance2:443' #指定了 scheme 为 https,host 为 instance2,端口为443
#微服务2
service2:
#实例3
- host: instance3 #地址
port: 80 #端口
instanceId: ${spring.cloud.discovery.client.simple.instances.service2[0].host}:${spring.cloud.discovery.client.simple.instances.service2[0].port} #可以不填,实例id
#实例4
- uri: 'https://instance4:8080' #指定了 scheme 为 https,host 为 instance4,端口为8080
# 指定 SimpleDiscoveryClient的排序顺序为1,默认是0,越小越优先
order: 1
# actuator 配置
management:
endpoint:
health:
# health 接口总是输出详细信息
show-details: always
endpoints:
jmx:
exposure:
# jmx 不暴露任何 actuator 接口
exclude: '*'
web:
exposure:
# http 暴露所有 actuator 接口
include: '*'
我们配置了四个不同实例:
我们可以直接通过 uri 配置,也可以具体配置其中的 host,port 和 isSecure,两者是等价的。例如:
spring:
cloud:
discovery:
client:
# SimpleDiscoveryClient与SimpleReactiveDiscoveryClient的配置
simple:
instances:
#微服务1
service1:
#实例1
- host: instance1 #地址
port: 8080 #端口
等价于
spring:
cloud:
discovery:
client:
# SimpleDiscoveryClient与SimpleReactiveDiscoveryClient的配置
simple:
instances:
#微服务1
service1:
#实例1
- uri: http://instance1:8080
instanceId 不一定需要指定,serviceId 会根据实例配置的上一级取,就算自己配置了,例如:
spring:
cloud:
discovery:
client:
# SimpleDiscoveryClient与SimpleReactiveDiscoveryClient的配置
simple:
instances:
#微服务1
service1:
#实例1
- host: instance1 #地址
port: 8080 #端口
serviceId: service2 #无效,实际还是service1
serviceId 还是实际合理的那个,也就是 service1。
这些机制在后面的源码分析就会理解了。
我们的测试代码会用到 Spring Boot 的事件机制,也就是在 ApplicationContext 到某一生命周期的时候,这些事件会被发布出来,由实现了对应事件的ApplicationListener
接口的 Bean 消费,Spring boot 中,事件主要包括:
-
ApplicationStartingEvent
:这个是spring boot应用一开始启动时,发出的事件,只是用来标识,应用开始启动了,一般没什么用 -
ApplicationEnvironmentPreparedEvent
:这个是在创建好Environment(通过上下文配置,判断到底创建StandardServletEnvironment(针对Servlet环境),StandardReactiveWebEnvironment(针对Reactive环境)还是StandardEnvironment(针对无servlet环境))之后发出的事件。 -
ApplicationContextInitializedEvent
: 这个是在创建好Context并调用ApplicationContextInitializer初始化context之后发布这个事件,在加载bean信息之前 -
ApplicationPreparedEvent
:加载bean信息之后,但是还没有创建bean的时候,发步这个事件。这个事件是和调用ApplicationContextAware
设置ApplicationContext一起进行的,可以看出,setApplicationContext方法里面不能去获取bean,因为bean可能还没有初始化完成 -
ApplicationStartedEvent
: 加载初始化各种需要的bean并依赖注入之后,在运行ApplicationRunner
做一些用户自定义的初始化操作之前,会发布这个事件。 -
ApplicationReadyEvent
:运行ApplicationRunner
做一些用户自定义的初始化操作之后,会发布这个事件。
我们使用ApplicationReadyEvent
的ApplicationListener
确保所有的DiscoveryClient
都初始化完成并可以使用作为测试类。
编写测试类:
TestSimpleDiscoveryClient
/**
* 通过消费 ApplicationReadyEvent 来确保 DiscoveryClient 初始化完成并可用
*/
@Slf4j
@Component
public class TestSimpleDiscoveryClient implements ApplicationListener<ApplicationReadyEvent> {
/**
* 初始化的方法返回类型是 DiscoveryClient 并且不是 Primary,这里只能通过 @Resource 自动装载不能通过 @Autowired
* 这里不排除以后返回类型修改为 SimpleDiscoveryClient 的可能性
* @see org.springframework.cloud.client.discovery.simple.SimpleDiscoveryClientAutoConfiguration
*/
@Resource
private SimpleDiscoveryClient simpleDiscoveryClient;
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
List<String> services = simpleDiscoveryClient.getServices();
services.forEach(serviceId -> {
log.info("{}: {}", serviceId, simpleDiscoveryClient.getInstances(serviceId));
});
}
}
TestSimpleReactiveDiscoveryClient
/**
* 通过消费 ApplicationReadyEvent 来确保 DiscoveryClient 初始化完成并可用
*/
@Slf4j
@Component
public class TestSimpleReactiveDiscoveryClient implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private SimpleReactiveDiscoveryClient simpleReactiveDiscoveryClient;
@Override
public void onApplicationEvent(ApplicationReadyEvent event) {
simpleReactiveDiscoveryClient.getServices().subscribe(serviceId -> {
simpleReactiveDiscoveryClient.getInstances(serviceId).collect(Collectors.toList()).subscribe(serviceInstances -> {
log.info("{}: {}", serviceId, serviceInstances);
});
});
}
}
@SpringBootApplication
public class DiscoveryClientMain {
public static void main(String[] args) {
SpringApplication.run(DiscoveryClientMain.class, args);
}
}
启动后,可以看到日志:
2021-01-19 09:38:05.646 INFO 6168 --- [ main] .h.s.c.i.s.d.s.TestSimpleDiscoveryClient : service2: [DefaultServiceInstance{instanceId='instance3:80', serviceId='service2', host='instance3', port=80, secure=false, metadata={}}, DefaultServiceInstance{instanceId='null', serviceId='service2', host='instance4', port=8080, secure=true, metadata={}}]
2021-01-19 09:38:05.647 INFO 6168 --- [ main] .h.s.c.i.s.d.s.TestSimpleDiscoveryClient : service1: [DefaultServiceInstance{instanceId='instance:8080', serviceId='service1', host='instance1', port=8080, secure=false, metadata={}}, DefaultServiceInstance{instanceId='null', serviceId='service1', host='instance2', port=443, secure=true, metadata={}}]
2021-01-19 09:38:05.913 INFO 6168 --- [ main] .s.d.s.TestSimpleReactiveDiscoveryClient : service2: [DefaultServiceInstance{instanceId='instance3:80', serviceId='service2', host='instance3', port=80, secure=false, metadata={}}, DefaultServiceInstance{instanceId='null', serviceId='service2', host='instance4', port=8080, secure=true, metadata={}}]
2021-01-19 09:38:05.913 INFO 6168 --- [ main] .s.d.s.TestSimpleReactiveDiscoveryClient : service1: [DefaultServiceInstance{instanceId='instance:8080', serviceId='service1', host='instance1', port=8080, secure=false, metadata={}}, DefaultServiceInstance{instanceId='null', serviceId='service1', host='instance2', port=443, secure=true, metadata={}}]