SpringCloud
一、概述
- 官网:Spring Cloud
- Spring Cloud Alibaba官网:Spring Cloud Alibaba
1.1 简介
Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础设施的开发,如服务注册发现、配置中心、消息总线、
负载均衡、断路器、数据监控等,都可以用Spring Boot的开发风格做到一键启动和部署。Spring Cloud并没有重复制造轮子,它只是将目前各家公司开发的比较成熟、经得起实际考验的服务框架组合起来,通过Spring Boot风格进行再封装屏蔽掉了复杂的配置和实现原理,最终给开发者留出了一套简单易懂、
易部署和易维护的分布式系统开发工具包。
二、详解
2.1 Spring Cloud注册中心
服务注册与发现的基本框架包括三部分:服务提供者、注册中心和服务消费者。其中,服务提供者将自己注册到注册中心(服务注册),
服务消费者从服务注册中心获取可用的服务列表(服务发现)。
2.1.1 Spring Cloud注册中心
Spring Cloud提供了几种服务注册与发现的实现方式,包括:
- Eureka:Netflix公司开源的、最为流行的一种服务注册中心(已闭源);
- Consul:由Hashicorp公司开发的一种服务发现组件,支持多数据中心;
- ZooKeeper:由Apache项目开发的一种分布式协调框架。
这里以Eureka为例,介绍Spring Cloud注册中心。
首先需要在pom.xml文件中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
接着,在启动类中加上@EnableEurekaServer注解,表明该服务作为Eureka Server运行:
@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}
在配置文件application.yml中指定服务端口、Eureka相关配置:
server:
port: 8761
eureka:
client:
register-with-eureka: false #服务注册:用于生产者
fetch-registry: false # 服务发现:获取注册信息用于消费者
service-url: # eureka服务地址
defaultZone: ${EUREKA_SERVER:http://localhost:${server.port}/eureka/}
server:
enable-self-preservation: false # 是否开启自我保护
eviction-interval-timer-in-ms: 60000 #服务注册表的清理间隔(根据健康状态信息,去除宕机服务)
2.1.2 Spring Cloud服务注册与发现
以Eureka为例,介绍Spring Cloud服务注册与发现的实现方式。
服务提供者和服务消费者需要在pom.xml文件中添加以下依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
在启动类中加上@EnableDiscoveryClient注解,表明该服务需要作为Eureka Client运行,并向Eureka Server注册:
@SpringBootApplication
@EnableDiscoveryClient
public class DemoServiceApplication {
public static void main(String[] args) {
SpringApplication.run(DemoServiceApplication.class, args);
}
}
同时,在配置文件application.yml中指定应用名称、Eureka相关配置:
spring:
application:
name: demp-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
2.2 Spring Cloud配置中心
2.2.1 常见的配置中心
- Nacos:阿里巴巴开发维护的,致力于帮助您发现、配置和管理微服务。Nacos: Dynamic Naming and Configuration Service。
- Spring Cloud Config:Spring原生的配置中心,Spring Cloud Config为分布式系统中的外部化配置提供服务器端和客户端支持。
- Apollo(阿波罗):携程框架部门研发的分布式配置中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、
流程治理等特性,适用于微服务配置管理场景。
2.2.2 原理
2.2.2.1 BootstrapApplicationListener
SpringApplication的run方法执行时,当执行到prepareEnvironment环境准备时,会广播ApplicationEnvironmentPreparedEvent事件,此时
BootstrapApplicationListener监听到这个事件,开始执行自己的代码。
ApplicationEnvironmentPreparedEvent事件有两个特别重要的监听器:
- EnvironmentPostProcessorApplicationListener:用来处理本地配置文件的,他会解析配置文件的配置,放到Environment中
- BootstrapApplicationListener:用来专门来跟配置中心交互的
源码:
public class BootstrapApplicationListener implements ApplicationListener<ApplicationEnvironmentPreparedEvent>, Ordered {
@Override
public void onApplicationEvent(ApplicationEnvironmentPreparedEvent event) {
// 获取环境变量对象
ConfigurableEnvironment environment = event.getEnvironment();
// 读取spring.cloud.bootstrap.enabled环境属性,默认为false,可通过系统变量设置,MARKER_CLASS存在时也返回true
if (!bootstrapEnabled(environment) && !useLegacyProcessing(environment)) {
return;
}
// don't listen to events in a bootstrap context
if (environment.getPropertySources().contains(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
return;
}
// 寻找当前环境是否已存在BootstrapContext
ConfigurableApplicationContext context = null;
String configName = environment.resolvePlaceholders("${spring.cloud.bootstrap.name:bootstrap}");
for (ApplicationContextInitializer<?> initializer : event.getSpringApplication().getInitializers()) {
if (initializer instanceof ParentContextApplicationContextInitializer) {
context = findBootstrapContext((ParentContextApplicationContextInitializer) initializer, configName);
}
}
// 如果还没有被创建,则开始创建
if (context == null) {
// 这个方法是整个Bootstrap的核心
context = bootstrapServiceContext(environment, event.getSpringApplication(), configName);
// 注册注销监听器
event.getSpringApplication().addListeners(new CloseContextOnFailureApplicationListener(context));
}
// 加载BoostrapContext上的ApplicationContextInitializers到用户Context上
apply(context, event.getSpringApplication(), environment);
}
/**
*
* create bootstrap context
*
* @param environment 全局Environment
* @param application SpringApplication
* @param configName bootstrapContext对应配置文件的加载名,默认为bootstrap
* @return bootstrapContext
*/
private ConfigurableApplicationContext bootstrapServiceContext(ConfigurableEnvironment environment,
final SpringApplication application, String configName) {
// 构建一空的 bootstrapEnvironment 对象
ConfigurableEnvironment bootstrapEnvironment = new AbstractEnvironment() {
};
MutablePropertySources bootstrapProperties = bootstrapEnvironment.getPropertySources();
// 读取spring.cloud.bootstrap.location属性,一般通过系统变量设置,默认为空
String configLocation = environment.resolvePlaceholders("${spring.cloud.bootstrap.location:}");
String configAdditionalLocation = environment
.resolvePlaceholders("${spring.cloud.bootstrap.additional-location:}");
Map<String, Object> bootstrapMap = new HashMap<>();
bootstrapMap.put("spring.config.name", configName);
bootstrapMap.put("spring.main.web-application-type", "none");
if (StringUtils.hasText(configLocation)) {
bootstrapMap.put("spring.config.location", configLocation);
}
if (StringUtils.hasText(configAdditionalLocation)) {
bootstrapMap.put("spring.config.additional-location", configAdditionalLocation);
}
bootstrapProperties.addFirst(new MapPropertySource(BOOTSTRAP_PROPERTY_SOURCE_NAME, bootstrapMap));
for (PropertySource<?> source : environment.getPropertySources()) {
if (source instanceof StubPropertySource) {
continue;
}
bootstrapProperties.addLast(source);
}
// TODO: is it possible or sensible to share a ResourceLoader?
SpringApplicationBuilder builder = new SpringApplicationBuilder()
// 此处activeProfiles是通过系统变量设置的,此处稍微备注下
.profiles(environment.getActiveProfiles())
.bannerMode(Mode.OFF)
// 应用bootstrap本身的环境变量
.environment(bootstrapEnvironment)
// Don't use the default properties in this builder
.registerShutdownHook(false).logStartupInfo(false).web(WebApplicationType.NONE);
final SpringApplication builderApplication = builder.application();
// 配置入口函数类
if (builderApplication.getMainApplicationClass() == null) {
builder.main(application.getMainApplicationClass());
}
if (environment.getPropertySources().contains("refreshArgs")) {
builderApplication.setListeners(filterListeners(builderApplication.getListeners()));
}
// 增加入口类BootstrapImportSelectorConfiguration
builder.sources(BootstrapImportSelectorConfiguration.class);
final ConfigurableApplicationContext context = builder.run();
// 设置bootstrapContext的别名为bootstrap
context.setId("bootstrap");
// 配置bootstrapContext为用户Context的父类
addAncestorInitializer(application, context);
// 合并defaultProperties对应的变量至childEnvironment
bootstrapProperties.remove(BOOTSTRAP_PROPERTY_SOURCE_NAME);
mergeDefaultProperties(environment.getPropertySources(), bootstrapProperties);
return context;
}
private void apply(ConfigurableApplicationContext context, SpringApplication application,
ConfigurableEnvironment environment) {
if (application.getAllSources().contains(BootstrapMarkerConfiguration.class)) {
return;
}
application.addPrimarySources(Arrays.asList(BootstrapMarkerConfiguration.class));
// 将application容器的初始化器和bootstrap隔离容器中的初始化器进行了合并,并设置到application容器所对应的SpringApplication当中。
Set target = new LinkedHashSet<>(application.getInitializers());
target.addAll(getOrderedBeansOfType(context, ApplicationContextInitializer.class));
application.setInitializers(target);
addBootstrapDecryptInitializer(application);
environment.setActiveProfiles(context.getEnvironment().getActiveProfiles());
}
}
2.2.2.2 BootstrapConfiguration
@Configuration(proxyBeanMethods = false)
@Import(BootstrapImportSelector.class)
public class BootstrapImportSelectorConfiguration {
}
BootstrapImportSelector
作用:加载META-INF/spring.factories中BootstrapConfiguration的实现。
public class BootstrapImportSelector implements EnvironmentAware, DeferredImportSelector {
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
List<String> names = new ArrayList<>(
SpringFactoriesLoader.loadFactoryNames(BootstrapConfiguration.class, classLoader));
names.addAll(Arrays.asList(StringUtils
.commaDelimitedListToStringArray(this.environment.getProperty("spring.cloud.bootstrap.sources", ""))));
List<OrderedAnnotatedElement> elements = new ArrayList<>();
for (String name : names) {
try {
elements.add(new OrderedAnnotatedElement(this.metadataReaderFactory, name));
} catch (IOException e) {
continue;
}
}
AnnotationAwareOrderComparator.sort(elements);
String[] classNames = elements.stream().map(e -> e.name).toArray(String[]::new);
return classNames;
}
}
2.2.2.2.1 PropertySourceBootstrapConfiguration
它本身是一个ApplicationContext的初始化器,并且会自动注入Bootstrap隔离容器中配置的所有的Locator,这个Locator是实现分布式配置中心的关键。
但是这个组件并不会在Bootstrap隔离容器中生效,这个组件,实际上会在app容器中去生效,在BootstrapApplicationListener的apply方法中将bootstrap
隔离容器中的初始化器合并到application容器中,并在application容器中生效:SpringApplication.run --> prepareContext -->
applyInitializers
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(PropertySourceBootstrapProperties.class)
public class PropertySourceBootstrapConfiguration implements ApplicationListener<ContextRefreshedEvent>,
ApplicationContextInitializer<ConfigurableApplicationContext>, Ordered {
public static final String BOOTSTRAP_PROPERTY_SOURCE_NAME = BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME
+ "Properties";
private static Log logger = LogFactory.getLog(PropertySourceBootstrapConfiguration.class);
private int order = Ordered.HIGHEST_PRECEDENCE + 10;
// 依赖注入
@Autowired(required = false)
private List<PropertySourceLocator> propertySourceLocators = new ArrayList<>();
@Autowired
private PropertySourceBootstrapProperties bootstrapProperties;
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
if (!bootstrapProperties.isInitializeOnContextRefresh() || !applicationContext.getEnvironment()
.getPropertySources().contains(BootstrapApplicationListener.BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
doInitialize(applicationContext);
}
}
private void doInitialize(ConfigurableApplicationContext applicationContext) {
List<PropertySource<?>> composite = new ArrayList<>();
AnnotationAwareOrderComparator.sort(this.propertySourceLocators);
boolean empty = true;
ConfigurableEnvironment environment = applicationContext.getEnvironment();
for (PropertySourceLocator locator : this.propertySourceLocators) {
// 回调所有实现PropertySourceLocator接口实例的locate方法,例如:NacosPropertySourceLocator
Collection<PropertySource<?>> source = locator.locateCollection(environment);
if (source == null || source.size() == 0) {
continue;
}
List<PropertySource<?>> sourceList = new ArrayList<>();
for (PropertySource<?> p : source) {
if (p instanceof EnumerablePropertySource) {
EnumerablePropertySource<?> enumerable = (EnumerablePropertySource<?>) p;
sourceList.add(new BootstrapPropertySource<>(enumerable));
} else {
sourceList.add(new SimpleBootstrapPropertySource(p));
}
}
logger.info("Located property source: " + sourceList);
composite.addAll(sourceList);
empty = false;
}
if (!empty) {
MutablePropertySources propertySources = environment.getPropertySources();
String logConfig = environment.resolvePlaceholders("${logging.config:}");
LogFile logFile = LogFile.get(environment);
for (PropertySource<?> p : environment.getPropertySources()) {
if (p.getName().startsWith(BOOTSTRAP_PROPERTY_SOURCE_NAME)) {
propertySources.remove(p.getName());
}
}
// //将刚获取到的配置变量添加到环境中
insertPropertySources(propertySources, composite);
reinitializeLoggingSystem(environment, logConfig, logFile);
setLogLevels(applicationContext, environment);
handleProfiles(environment);
}
}
/**
* 将获取到的对象添加至environment中,重点关注的添加后环境变量的优先级
* @param propertySources
* @param composite
*/
private void insertPropertySources(MutablePropertySources propertySources, List<PropertySource<?>> composite) {
MutablePropertySources incoming = new MutablePropertySources();
List<PropertySource<?>> reversedComposite = new ArrayList<>(composite);
// 反转集合,后续可以按照原顺序加入当前环境集合中
Collections.reverse(reversedComposite);
for (PropertySource<?> p : reversedComposite) {
incoming.addFirst(p);
}
// overrideSystemProperties: 外部属性可以覆盖系统属性 默认true
// allowOverride: 允许覆盖 默认true
// overrideNone: 当allowOverride为true时,外部属性应该具有最低优先级,并且不应该覆盖任何现有的属性源(包括本地配置文件),默认false
PropertySourceBootstrapProperties remoteProperties = new PropertySourceBootstrapProperties();
Binder.get(environment(incoming)).bind("spring.cloud.config", Bindable.ofInstance(remoteProperties));
// 默认赋值方式
if (!remoteProperties.isAllowOverride()
|| (!remoteProperties.isOverrideNone() && remoteProperties.isOverrideSystemProperties())) {
for (PropertySource<?> p : reversedComposite) {
if (propertySources.contains(DECRYPTED_PROPERTY_SOURCE_NAME)) {
propertySources.addAfter(DECRYPTED_PROPERTY_SOURCE_NAME, p);
} else {
// 添加进当前环境属性源中,远程配置优先级高
propertySources.addFirst(p);
}
}
return;
}
// 远程配置优先级低
if (remoteProperties.isOverrideNone()) {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
return;
}
if (propertySources.contains(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME)) {
if (!remoteProperties.isOverrideSystemProperties()) {
for (PropertySource<?> p : reversedComposite) {
propertySources.addAfter(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
} else {
for (PropertySource<?> p : composite) {
propertySources.addBefore(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, p);
}
}
} else {
for (PropertySource<?> p : composite) {
propertySources.addLast(p);
}
}
}
}
- 通过Spring注入PropertySourceLocator对象。
- 并且通过PropertySourceLocator对象的locateCollection方法获取到配置变量。
- 将刚获取到的配置变量加载到environment中。
三、其他
3.1 Spring Cloud 和 SpringBoot 版本依赖关系
参考–官网:Spring Cloud
3.2 SpringCloud 和 Spring Cloud Alibaba 版本对应关系
参考–https://sca.aliyun.com/zh-cn/docs/2022.0.0.0/overview/version-explain
3.3 SpringCloud中Bootstrap配置
在SpringBoot 2.4.x的版本之后,对于bootstrap.properties/bootstrap.yaml配置文件(我们合起来成为Bootstrap配置文件)
的支持,需要导入如下的依赖:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
该依赖有两个作用:
- 这个jar包包含了一个标识类Marker,用来标识要开启Bootstrap配置文件的支持
- 引入了 spring-cloud-starter 依赖。
实际上,通过在启动参数中配置相关的属性也可以–spring.cloud.bootstrap.enabled=true(因为Bootstrap的监听器优先级比较高,
因此配置在application配置文件当中无效,application配置文件还未加载到环境当中来,所以必须在启动参数中去进行配置),
不用非得导入jar包(前提容器中已经有了cloud-context的jar包,因为这个jar包当中导入了BootstrapApplicationListener组件)。
官网:https://docs.spring.io/spring-cloud-config/reference/client.html
3.4 微服务
“微服务 ”是在2014年3月名字叫Martin Fowler所提出的,可以在他的官方博客上找到:
microservices
微服务是系统架构上的一种设计风格, 它的主旨是将一个原本独立的系统拆分成多个小型服务,这些小型服务都在各自独立的进程中运行,服务之间通过基于HTTP的RESTful
API进行通信协作。由于有了轻量级的通信协作基础, 所以这些微服务可以使用不同的语言来编写。
优点:
- 灵活性高:它将应用程序分解为小型服务(松散耦合),使其开发、维护更快,更易于理解,可以提供更高的灵活性;
- 独立扩展:它使每个服务能够独立扩展,将系统中的不同功能模块拆分成多个不同的服务,这些服务进行独立地开发和部署,每个服务都运行在自己的进程内,
这样每个服务的更新都不会影响其他服务的运行; - 支持多种编程语言:微服务可通过最佳及最合适的不同的编程语言与工具进行开发,能够做到有的放矢地解决针对性问题;
- 自动部署与持续集成工具集成:它允许以灵活的方式将自动部署与持续集成工具集成,例如Jenkins,Hudson等;
- 通用性:通过服务实现应用的组件化(按功能拆分、可独立部署和维护),围绕业务能力组织服务,根据业务不同的需求进行不同组件的使用,所做产品非项目化,对于平台具有一定的通用性。
缺点:
- 处理故障难度高:微服务架构是一个分布式系统,必须构建一个相互通信机制并处理部分故障;
- 部署工作量大:单体应用程序可以部署在负载平衡器后面的相同服务器上。但对于微服务,每个服务都有不同的实例,每个实例都需要配置、部署、缩放和监控;
- 测试复杂度高:微服务在一定程度上也会导致系统变得越来越复杂,增加了集成测试的复杂度;
- 运营成本增加:单体应用可能只需部署至一小片应用服务区集群,而微服务架构可能变成需要构建/测试/部署/运行数十个独立的服务,并可能需要支持多种语言和环境。
这导致一个整体式系统如果由20个微服务组成,可能需要40~60个进程; - 发布风险高:把系统分为多个协作组件后会产生新的接口,这意味着简单的交叉变化可能需要改变许多组件,并需协调一起发布。在实际环境中,
一个新品发布可能被迫同时发布大量服务,由于集成点的大量增加,微服务架构会有更高的发布风险; - 分布性系统问题:作为一种分布式系统,微服务引入了复杂性和其他若干问题,例如网络延迟、容错性、消息序列化、不可靠的网络、异步机制、版本化、
差异化的工作负载等,开发人员需要考虑以上的分布式系统问题。