SpringCloud

SpringCloud学习笔记,内容包括:认识微服务及SpringCloud、Eureka注册中心、Nacos注册和配置中心、Feign远程调用服务和Gateway网关

1.初识微服务

1.1 单体架构 和 分布式架构

单体架构:将业务的所有功能集中在一个项目中开发,打成一个包部署。

分布式架构:根据业务功能对系统做拆分,每个业务功能模块作为独立项目开发,称为一个服务。

1.2 微服务

可以认为微服务是一种经过良好架构设计的分布式架构方案

微服务的架构特征:

  • 单一职责:微服务拆分粒度更小,每一个服务都对应唯一的业务能力,做到单一职责
  • 自治:团队独立、技术独立、数据独立,独立部署和交付
  • 面向服务:服务提供统一标准的接口,与语言和技术无关
  • 隔离性强:服务调用做好隔离、容错、降级,避免出现级联问题

微服务拆分原则:

  • 不同微服务,不要重复开发相同业务
  • 微服务数据独立,不要访问其它微服务的数据库
  • 微服务可以将自己的业务暴露为接口,供其它微服务调用

1.3 SpringCloud

SpringCloud是目前国内使用最广泛的微服务框架。官网地址:https://spring.io/projects/spring-cloud。

SpringCloud集成了各种微服务功能组件,并基于SpringBoot实现了这些组件的自动装配,从而提供了良好的开箱即用体验。

另外,SpringCloud底层是依赖于SpringBoot的,并且有版本的兼容关系,具体可以查看官方文档,截至2023年11月,兼容关系如下:

1.4 入门案例

项目结构如下:

在微服务架构中整个项目由多个实现不同业务的模块组成,比如上图管理订单的是order-service模块,管理用户的是user-service。

每个模块间相互独立,所在的运行端口也不同,仅通过其对外暴露的接口互相调用

1.5 远程调用案例

在order-service服务中,有一个根据id查询订单的接口:

根据id查询订单,返回值是Order对象,如图:

其中的user为null

在user-service中有一个根据id查询用户的接口:

查询的结果如图:

1.5.1 案例需求

要求在查询订单的同时,根据订单中包含的userId查询出用户信息,一起返回

1.5.2 实现步骤

首先,我们在order-service服务中的OrderApplication启动类中,注册RestTemplate实例

1
2
3
4
5
6
7
8
9
10
11
12
13
@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

public static void main(String[] args) {
SpringApplication.run(OrderApplication.class, args);
}
//注册RestTemplate实例
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

然后需要修改OrderService类中的queryOrderById方法

注入restTemplate用于发送Http请求,使用**getForObject()**通过restTemplate调用userService的接口来获取User对象

2.Eureka注册中心

在上面的案例中,url直接用 ip+端口号 写死了

  • 假如有多个user-service实例地址,此时url应该怎么写,怎样才能均衡地请求不同地址
  • 假如其中一个地址宕机了,固定的 url 必然导致程序无法运行下去

因此我们需要使用 注册中心 来解决这些问题,最广为人知的注册中心就是Eureka,其结构如下:

2.1 搭建eureka-server

2.1.1 创建eureka-server服务

在工程目录下创建一个新模块,使用maven模块即可,这里名字设为eureka-service

2.1.2 引入eureka-server依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

2.1.3 编写启动类

给eureka-server服务编写一个启动类,一定要添加一个@EnableEurekaServer注解,开启eureka的注册中心功能:

1
2
3
4
5
6
7
@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaApplication.class, args);
}
}

3.2.4.编写配置文件

编写一个application.yml文件,内容如下:

1
2
3
4
5
6
7
8
9
server:
port: 10086 #运行端口
spring:
application:
name: eureka-server #微服务名称
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka #eureka注册地址

3.2.5.启动服务

启动微服务,然后在浏览器访问:http://127.0.0.1:10086 . 红框中的是已注册的服务,这里是eureka-server将自己注册进来了,以后可能eureka-server不止一个。

2.2 服务注册

接下来将user-service和order-service注册到eureka-server中去。

2.2.1 引入依赖

在user-service和order-service的pom文件中,引入下面的eureka-client依赖:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

2.2.2 配置文件

在user-service和order-service中,修改application.yml文件,添加服务名称、eureka地址:

1
2
3
4
5
6
7
spring:
application:
name: userservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka
1
2
3
4
5
6
7
spring:
application:
name: orderservice
eureka:
client:
service-url:
defaultZone: http://127.0.0.1:10086/eureka

2.2.3 实现负载均衡

给RestTemplate这个Bean添加一个**@LoadBalanced**注解

2.2.4 拓展:服务多开

启动多个user-service实例

为了演示一个服务有多个实例的场景,我们添加一个SpringBoot的启动配置,再启动一个user-service。

首先,复制原来的user-service启动配置:

然后,在弹出的窗口中,填写信息:

现在,SpringBoot窗口会出现两个user-service启动配置:

不过,第一个是8081端口,第二个是8082端口。

启动两个user-service实例:

查看eureka-server管理页面:

2.3 服务发现

在服务拉取过程中,用 服务名 代替 IP+端口

spring会自动帮助我们从eureka-server端,根据userservice这个服务名称,获取实例列表,而后完成负载均衡。

3.Nacos注册中心

3.1 安装、启动和配置Nacos

3.1.1 安装

Nacos | GitHubz主页

在官网下载压缩包解压到任意非中文目录下

3.1.2 启动和配置

  • bin 目录下有启动命令,双击startup.cmd 或者 进入cmd窗口输入以下命令即可启动Nacos

startup.cmd -m standalone

  • conf 目录下的application.properties是Nacos的配置信息,可以在这里面设置运行端口,默认为8848

然后在浏览器输入地址:http://127.0.0.1:8848/nacos即可访问,**默认的账号密码都是nacos**

3.2 服务注册

3.2.1 引入依赖

在cloud-demo父工程的pom文件中的<dependencyManagement>中引入SpringCloudAlibaba的依赖:

1
2
3
4
5
6
7
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>

然后在user-service和order-service中的pom文件中引入nacos-discovery依赖:

1
2
3
4
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

3.2.2 配置nacos地址

在user-service和order-service的application.yml中添加nacos地址:

1
2
3
4
spring:
cloud:
nacos:
server-addr: localhost:8848

注意配置完成后,仍然需要注意是否有给RestTemplate这个Bean添加一个**@LoadBalanced**注解,否则拉取服务时会报错

3.2.3 启动微服务

启动微服务后可以看到管理面板中服务列表已更新,成功识别到了几个服务

通过访问接口可以发现,前面Eureka完成的服务拉取和负载均衡,现在也能正常完成

3.3 服务分级存储模型

一个服务可以有多个实例,例如我们的user-service,可以有:

  • 127.0.0.1:8081
  • 127.0.0.1:8082
  • 127.0.0.1:8083

假如这些实例分布于全国各地的不同机房,例如:

  • 127.0.0.1:8081,在上海机房
  • 127.0.0.1:8082,在上海机房
  • 127.0.0.1:8083,在杭州机房

Nacos就将同一机房内的实例 划分为一个集群

也就是说,user-service是服务,一个服务可以包含多个集群,如杭州、上海,每个集群下可以有多个实例,形成分级模型,如图:

微服务互相访问时,应该尽可能访问同集群实例,因为本地访问速度更快。当本集群内不可用时,才访问其它集群

3.3.1 配置集群属性

修改user-service的application.yml文件,添加集群配置:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HangZhou # 集群名称

重启一个user-service实例后,我们可以在nacos控制台看到下面结果:

重启的那个userservice所在集群已经从DEFAULT变成HangZhou

3.3.2 同集群优先的负载均衡

@LoadBalanced默认使用的ZoneAvoidanceRule并不能实现根据同集群优先来实现负载均衡。

因此Nacos中提供了一个NacosRule的实现,可以优先从同集群中挑选实例。

(1)给order-service配置集群信息

修改order-service的application.yml文件,添加集群配置:

1
2
3
4
5
6
spring:
cloud:
nacos:
server-addr: localhost:8848
discovery:
cluster-name: HZ # 集群名称

(2)修改负载均衡规则

修改order-service的application.yml文件,修改负载均衡规则:

1
2
3
userservice:  #要做配置的微服务名称
ribbon:
NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 负载均衡规则

这样配置完成后,orderservice在调用userservice时就会首先从同集群中调用,其次才会调用其他集群的服务

3.3.3 对比Nacos和Eureka

  • Nacos与eureka的共同点
    1. 都支持服务注册和服务拉取
    2. 都支持服务提供者心跳方式做健康检测
  • Nacos与Eureka的区别
    1. Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式。临时实例心跳不正常会被剔除,非临时实例则不会被剔除
    2. Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    3. Nacos集群默认采用AP方式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
    4. Nacos除了可以是注册中心,也可以是配置中心

4.Nacos配置中心

4.1 统一配置管理

4.1.1 在面板中添加配置

注意命名规则必须严格按照以下方式命名,否则拉取不到配置内容。如果不写 -[profile] ,那么这个配置文件就会是 多环境共享 的配置文件

注意:项目的核心配置,需要热更新的配置才有放到nacos管理的必要。基本不会变更的一些配置还是保存在微服务本地比较好。

4.1.2 微服务拉取配置

引入nacos-config依赖

首先,在user-service服务中,引入nacos-config的客户端依赖:

1
2
3
4
5
<!--nacos配置管理依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>

添加bootstrap.yaml

然后,在user-service中添加一个bootstrap.yaml文件,内容如下(application.yaml中重复的内容可以删除):

1
2
3
4
5
6
7
8
9
10
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
file-extension: yaml # 文件后缀名

在读取配置时,会根据 服务名称-开发环境.文件后缀名拼接而成的文件名来寻找配置

比如本例中,就是去读取userservice-dev.yaml

读取nacos配置

使用**@Value("${}")**可以读取配置项,试着引用刚才在面板中配置的pattern.dateformat

启动服务后访问接口,可以看到配置内容已经成功被拉取到

4.1.3 实现热更新

按以上操作完成发现,如果在nacos面板中修改的配置内容,而服务仍在运行中,此时刷新页面,会发现新的配置内容还是没有被读取到,仍然是老的配置内容。

这是因为服务没有实现热更新,我们只需要在**@Value注入的变量所在类上添加注解@RefreshScope**

这样,无需重启服务也可以读取到新的配置内容

5.Feign远程调用

5.1 Feign替代RestTemplate

5.1.1 导入依赖

1
2
3
4
5
<!--Feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

5.1.2 添加注解

给需要远程调用的服务添加注解**@EnableFeignClients**

5.1.3 编写Client接口

新建一个包clients专门存放client,编写UserClient,内容与RestFul风格的接口类似,如下:

当调用userClient.getById(id)时,Feign就会自动向http://userservice/user/id发送get请求

调用方式:

5.2 Feign性能优化

Feign底层发起http请求,依赖于其它的框架。其底层客户端实现包括:

  • URLConnection:默认实现,不支持连接池

  • Apache HttpClient :支持连接池

  • OKHttp:支持连接池

因此提高Feign的性能主要手段就是使用连接池代替默认的URLConnection。

此外还可以通过配置较低的日志级别等手段来优化性能

引入依赖

1
2
3
4
5
<!--httpClient的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>

配置连接池

在order-service的application.yml中添加配置:

1
2
3
4
5
6
7
8
9
feign:
client:
config:
default: # default全局的配置
loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息,更低级别的是NONE
httpclient:
enabled: true # 开启feign对HttpClient的支持
max-connections: 200 # 最大的连接数
max-connections-per-route: 50 # 每个路径的最大连接数

5.3 最佳实践-抽取

由于项目中的每一个模块都在调用远程服务时,都需要编写Client、POJO,而且这些内容在每个模块中都一模一样,当模块数量增多时,每次都要复制相同的内容到新模块中,不仅麻烦而且浪费。

因此,可以把这些共同需要的Client、POJO等专门放在一个新模块中,而其他模块只需要导入依赖即可使用

抽取

首先创建一个名为feign-api的新模块

在feign-api中然后引入feign的starter依赖

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

然后,order-service中编写的UserClient、User都复制到feign-api项目中,其他项目中的相同内容可以删除

其他模块导入feign-api

1
2
3
4
5
<dependency>
<groupId>cn.itcast.demo</groupId>
<artifactId>feign-api</artifactId>
<version>1.0</version>
</dependency>

修改order-service中的所有与上述三个组件有关的导包部分,改成导入feign-api中的包

解决包扫描问题

1
2
@Autowired
private UserClient userClient;

由于@Autowired只会在扫描启动类所在的包,所以处于另一个包中的UserClient无法被扫描到并创建对象存入IOC容器,所以会报错找不到UserClient。以下是两种解决方法,在启动类上配置Feign需要的包或接口。方式一火力覆盖、方式二精准打击。

方式一:

指定Feign应该扫描的包:

1
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")

方式二:

指定需要加载的Client接口:

1
@EnableFeignClients(clients = {UserClient.class})

6.Gateway网关

6.1 搭建网关

6.1.1 新建网关模块

6.1.2 导入依赖,编写启动类

引入网关和nacos服务发现依赖

1
2
3
4
5
6
7
8
9
10
<!--网关-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服务发现依赖-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

编写启动类

1
2
3
4
5
6
7
@SpringBootApplication
public class GatewayApplication {

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

6.1.3 配置文件

创建application.yml文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
server:
port: 10010 # 网关端口
spring:
application:
name: gateway # 服务名称
cloud:
nacos:
server-addr: localhost:8848 # nacos地址
gateway:
routes: # 网关路由配置
- id: user-service # 路由id,自定义,只要唯一即可
# uri: http://127.0.0.1:8081 # 路由的目标地址 http就是固定地址
uri: lb://userservice # 路由的目标地址 lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求

我们将符合Path 规则的一切请求,都代理到 uri参数指定的地址。

本例中,我们将 /user/**开头的请求,代理到lb://userservice,lb是负载均衡,根据服务名拉取服务列表,实现负载均衡。

6.1.4 测试

访问10010端口,成功访问到了服务

6.2 过滤器工厂

6.2.1.路由过滤器的种类

Spring提供了31种不同的路由过滤器工厂。例如:

名称 说明
AddRequestHeader 给当前请求添加一个请求头
RemoveRequestHeader 移除请求中的一个请求头
AddResponseHeader 给响应结果中添加一个响应头
RemoveResponseHeader 从响应结果中移除有一个响应头
RequestRateLimiter 限制请求的流量

更多过滤器参考官网文档:Spring Cloud Gateway

6.2.2 添加过滤器

以AddRequestHeader为例

只需要修改gateway服务的application.yml文件,添加路由过滤器即可:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
filters: # 过滤器
- AddRequestHeader=Message,This is a message! # 添加请求头

当前过滤器写在userservice路由下,因此仅仅对访问userservice的请求有效。如果需要对所有微服务都起作用,可以用默认过滤器default-filters,使用方式如下:

1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: user-service
uri: lb://userservice
predicates:
- Path=/user/**
default-filters: # 默认过滤项
- AddRequestHeader=Message,This is a message!

这样配置好之后,就可以在接口处调用这个参数测试一下能否拿到 Message:

可以看到使用@RequestHeader成功拿到了Message

1
2
3
4
@GetMapping("/message")
public String testFilter(@RequestHeader("Message") String message) {
return message;
}

6.2.3 全局过滤器

GatewayFilter,网关提供了31种,但每一种过滤器的作用都是固定的。如果我们希望拦截请求,做自己的业务逻辑则没办法实现。

GlobalFilterGatewayFilter的作用一样。区别在于GatewayFilter通过配置定义,处理逻辑是固定的;而GlobalFilter的逻辑需要自己写代码实现

案例:

假如需要拦截请求并判断用户身份,放行条件为请求中带有参数authorization,值为admin

实现:

在gateway中定义一个类,实现GlobalFilter接口:

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
/**
* @param exchange 请求上下文,里面可以获取Request、Response等信息
* @param chain 用来把请求委托给下一个过滤器
* @return {@code Mono<Void>} 返回标示当前过滤器业务结束
*/
@Order(-1) //过滤器优先级,越小优先级越高
@Component
public class AuthorizeFilter implements GlobalFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 1.获取请求参数
MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
// 2.获取authorization参数
String auth = params.getFirst("authorization");
// 3.校验
if ("admin".equals(auth)) {
// 放行,把请求委托给下一个过滤器
return chain.filter(exchange);
}
// 4.拦截
// 4.1.禁止访问,设置状态码
exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
// 4.2.结束处理
return exchange.getResponse().setComplete();
}
}

可以看到传递了authorization=admin才能访问

6.3 解决跨域问题

在gateway服务的application.yml文件中,添加下面的配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
cloud:
gateway:
# 。。。
globalcors: # 全局的跨域处理
add-to-simple-url-handler-mapping: true # 解决options请求被拦截问题
corsConfigurations:
'[/**]':
allowedOrigins: # 允许哪些网站的跨域请求
- "http://localhost:8090"
allowedMethods: # 允许的跨域ajax的请求方式
- "GET"
- "POST"
- "DELETE"
- "PUT"
- "OPTIONS"
allowedHeaders: "*" # 允许在请求中携带的头信息
allowCredentials: true # 是否允许携带cookie
maxAge: 360000 # 这次跨域检测的有效期