一、注册中心、配置管理 Nacos
Alibaba Nacos,服务提供者和服务消费者将自己的信息注册到注册中心,注册中心通过心跳机制来确保每个服务都可以正常运行,服务消费者订阅注册中心,注册中心为服务消费者推送变更信息
Nacos 可以将配置集中管理,又可以在配置变更时,及时通知微服务,实现配置的热更新
1、功能
- 动态服务发现
- 配置管理
2、注册中心
1)搭建
- 创建 nacos 所需要的数据库
- 在服务器创建 nacos 配置文件 custom.env,指定模式和连接 Mysql 相关信息
- 使用 docker 创建 nacos 镜像,指明配置文件
- 访问
ip:8848/nacos
,账号密码 nacos
2)服务注册
将服务注册到 Nacos 中,让它统一管理
- 服务中添加
spring-cloud-starter-alibaba-nacos-discovery
的依赖 - 服务中添加 Nacos 配置,改服务端口其他配置相同会自动变成集群
spring:
application:
name: item-service # 服务名称
cloud:
nacos:
server-addr: 192.168.150.101:8848 # nacos地址
3)服务发现
在一个服务中得到已经注册的其他服务的信息
- 服务中添加
spring-cloud-starter-alibaba-nacos-discovery
的依赖 - 自动注入 DiscoveryClient,使用
List<ServiceInstance> instance = discoveryClient.getInstances("服务名称")
得到实例列表,然后通过ServiceInstance instance = instances.get(RandomUtil.randomInt(instances.size()));
随机得到一个服务对象,就可以使用此对象获得相关信息,如:instance.getUri()
获得服务的地址
3、配置管理
搭建好注册中心即搭建好了 nacos 服务,里面就包含服务管理、配置管理、权限控制、集群管理等功能
1、服务的配置管理
- 在 nacos 服务上添加配置文件,
Data ID(配置文件的ID):服务名称[-profile].[后缀名]
,不指定profile
的配置文件会被所有环境优先加载,达到配置文件在不同环境下共享的效果,如果还有本地配置,本地配置加载的优先级最高,但也容易被覆盖 - 服务读取 nacos 配置文件:在本服务中配置
application.yml
文件,然后引入spring-cloud-starter-alibaba-nacos-config
依赖 - 编码中使用配置内容:可以使用
@Value("${a.b(配置文件的键)}")
来注入到变量中 - 配置热更新:在 @Value 注入的变量所在类上添加注解
@RefreshScope
或者 使用@ConfigurationProperties(prefix = "配置文件中的键")
注解代替 @Value 注解。注意 Spring 的配置不支持热更新
# 2.application.yml
# 在这个配置文件中,会去命名空间的组名下根据 `${spring.application.name}-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}`作为文件id
# 本例中是去读取 nacos 服务上名为 userservice-dev.yaml 的配置文件
spring:
application:
name: userservice # 服务名称
profiles:
active: dev #开发环境,这里是dev
cloud:
nacos:
server-addr: localhost:8848 # Nacos地址
config:
namespace: 命名空间
group: 组名
file-extension: yaml # 文件后缀名
2、共享的配置管理
- 在 nacos 中添加共享配置,如数据库信息
- 服务中引入
spring-cloud-starter-alibaba-nacos-config
和spring-cloud-starter-bootstrap
依赖 - 服务中读取共享配置:在服务中创建
bootstrap.yaml
,配置 file-extension 和 shared-configs
// 3.bootstrap.yaml
spring:
cloud:
nacos:
server-addr: 192.168.150.101 # nacos地址
config:
file-extension: yaml # 文件后缀名
shared-configs: # 共享配置。extension-configs可以实现引用其他配置文件,需要指定 dataId、group
- dataId: shared-jdbc.yaml # 共享mybatis配置
- dataId: shared-log.yaml # 共享日志配置
- dataId: shared-swagger.yaml # 共享日志配置
3、配置优先级
项目应用名配置文件 > 扩展配置文件 > 共享配置文件 > 本地配置文件(VM Options 参数)
想让本地配置文件最终生效,需要在 nacos 对应的配置文件中添加配置:
spring:cloud:config:override-none: true
4、动态路由
Geteway 是在 Spring 中,所以不支持 @ConfigurationProperties 的热更新。我们需要编写自己实现
实现
- 创建 nacos 配置文件 gateway-routes.json 来记录路由信息
- 在 Geteway 服务中引入
spring-cloud-starter-alibaba-nacos-config
和spring-cloud-starter-bootstrap
依赖 - 在 Geteway 服务中编写类实现动态路由:在组件类中通过
@PostConstruct
注解创建一个项目启动就加载的方法,在方法中使用注入对象的方法来监听指定文件实现动态路由,nacosConfigManager.getConfigService().getConfigAndSignListener("1中配置文件的id","配置文件的分组",超时时间, new Listener() {}
,监听变化的接口 Listener 需要实现两个方法,Executor getExecutor()
来确定使用什么线程池,void receiveConfigInfo(String configInfo)
具体实现动态路由的方法,configInfo 是变化后的路由信息
# 1.创建 nacos 配置文件 gateway-routes.json 来记录路由信息
[
{
"id": "item",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0":"/items/**", "_genkey_1":"/search/**"}
}],
"filters": [],
"uri": "lb://item-service"
},
{
"id": "cart",
"predicates": [{
"name": "Path",
"args": {
"_genkey_0":"/carts/**"}
}],
"filters": [],
"uri": "lb://cart-service"
}
]
// 3.在 Geteway 服务中编写类实现动态路由
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {
private final RouteDefinitionWriter writer;//更新路由对象
private final NacosConfigManager nacosConfigManager;//实现动态路由的核心对象
// 路由配置文件的id和分组
private final String dataId = "gateway-routes.json";//配置文件名字 id
private final String group = "DEFAULT_GROUP";//配置文件分组
// 保存更新过的路由id,然后每次先删除
private final Set<String> routeIds = new HashSet<>();
@PostConstruct //让项目启动就加载
public void initRouteConfigListener() throws NacosException {
// 1.注册监听器并首次拉取配置
String configInfo = nacosConfigManager.getConfigService()
.getConfigAndSignListener(dataId, group, 5000, new Listener() {
@Override
public Executor getExecutor() {
return 线程池对象;
}
@Override
public void receiveConfigInfo(String configInfo) {
updateConfigInfo(configInfo);
}
});
// 2.首次启动时,更新一次配置,不写的话路由信息会一直为空
updateConfigInfo(configInfo);
}
private void updateConfigInfo(String configInfo) {
log.debug("监听到路由配置变更,{}", configInfo);
// 1.反序列化
List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);
// 2.更新前先清空旧路由
// 2.1.清除旧路由
for (String routeId : routeIds) {
writer.delete(Mono.just(routeId)).subscribe();
}
routeIds.clear();
// 2.2.判断是否有新的路由要更新
if (CollUtils.isEmpty(routeDefinitions)) {
// 无新路由配置,直接结束
return;
}
// 3.更新路由
routeDefinitions.forEach(routeDefinition -> {
// 3.1.更新路由
writer.save(Mono.just(routeDefinition)).subscribe();
// 3.2.记录路由id,方便将来删除
routeIds.add(routeDefinition.getId());
});
}
}
4、集群
- 搭建数据库,初始化数据库表结构,一般采用主从模式
- 进入 nacos 的 conf 目录,修改配置文件 cluster.conf.example,重命名为 cluster.conf,然后添加每个 nacos 服务的地址
- 修改 application.properties 文件,添加数据库配置
- 启动每个 nacos 服务
- 使用 nginx 进行反向代理:监听 80 端口,将 /nacos 请求反向代理给每个 nacos 服务实现集群
- 编程中的 yml 配置文件,直接改成 nginx 监听的端口即可
二、注册中心 Eureka
Spring Cloud Eureka
1、搭建
- 创建 eureka-server 服务
- 导入
spring-cloud-starter-netflix-eureka-server
依赖 - 编写启动类:添加
@EnableEurekaServer
- 编写 yml 配置文件:
eureka:client:service-url: defaultZone: http://127.0.0.1:10086/eureka(本eureka服务地址)
- 启动服务,访问:
http://127.0.0.1:10086
2、服务注册
- 服务中引入依赖:
spring-cloud-starter-netflix-eureka-client
- 编写 yml 配置文件:
eureka:client:service-url: defaultZone: http://127.0.0.1:10086/eureka(eureka服务地址)
- 启动服务,服务就注册到 eureka 中了
3、服务发现
- 先进行服务注册
- 给 RestTemplate 这个 Bean 添加一个 @LoadBalanced 注解,然后路径请求就可以直接使用服务名代替达到负载均衡的效果
三、远程调用 OpenFeign
Spring Cloud OpenFeign,是一个声明式的 http 客户端,是 SpringCloud 在 Eureka 公司开源的 Feign 基础上改造而来。基于 SpringMVC 的常见注解,优雅的实现 http 请求的发送
1、基础使用
- 服务中添加
spring-cloud-starter-openfeign
,spring-cloud-starter-loadbalancer
依赖 - 在启动类上添加
@EnableFeignclients
注解来开启远程调用 - 编写 OpenFeign 客户端:@FeignClient 指定要请求的服务列表;@GetMapping 指定请求方式和请求路径;方法参数指定请求参数;方法返回值指定返回后封装的类型
// 3. 编写 OpenFeign 客户端
// 向 http://item-service/items 发送一个 GET 请求,携带 ids 为请求参数,并自动将返回值处理为 List<ItemDTO>
@FeignClient("item-service")
public interface ItemClient {
@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
2、开启连接池
Feign 底层默认 HttpURLConnection 实现,不支持连接池,为了更好的性能,通常使用支持连接池的实现 OKHttp、Apache HttpClient
使用
- 引入
feign-okhttp
依赖 - 在配置文件中配置:
feign:okhttp:enabled: true
# 开启 OKHttp 功能
3、最佳实践
项目中一般是将所有的远程调用封装到一个模块中,然后让其他服务引入这个模块,但是可能会出现注解扫描不到的情况,所以要在启动类的注解上添加
@EnableFeignclients(basePackages = "远程调用模块的包路径")
4、输出日志
OpenFeign 只会在 FeignClient 所在包的日志级别为 DEBUG 时,才会输出日志;四个级别:NONE:不记录,默认值;BASIC:仅记录请求的方法,URL 以及响应状态码和执行时间;HEADERS:在 BASIC 的基础上,额外记录了请求和响应的头信息;FULL:记录所有请求和响应的明细,包括头信息、请求体、元数据
配置
- 创建一个普通类,使用 @Bean 注解注入指定级别的
Logger.Level
对象 - 局部生效:
@FeignClient(value = "item-service", configuration = 创建的普通类.class)
- 全局生效:
@EnableFeignClients(defaultConfiguration = 创建的普通类.class)
5、支持 Multipart 格式传参
- 引入
feign-form
和feign-form-spring
依赖 - 导入配置类,注入
Encoder
对象 - 在 feign 接口的注解上添加:
@FeignClient(value = "服务名",configuration = 配置类.class)
6、项目中传递用户信息
- 在配置类中注入
RequestInterceptor
Bean 接口对象 - 在匿名内部类方法中实现用户信息的传递
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate template) {
// 获取登录用户,在网关有通过全局过滤器到服务的前置拦截器传递过来
Long userId = UserContext.getUser();
if(userId == null) {
// 如果为空则直接跳过
return;
}
// 如果不为空则放入请求头中,传递给下游微服务
template.header("user-info", userId.toString());
}
};
}
四、网关 Gateway
Spring Cloud Gateway,网关就是网络的关口,负责请求的路由、身份校验、权限控制、限流
1、功能
- 权限控制: 网关作为微服务入口,需要校验用户是是否有请求资格,如果没有则进行拦截
- 路由和负载均衡: 一切请求都必须先经过 gateway,但网关不处理业务,而是根据某种规则,把请求转发到某个微服务,这个过程叫做路由。当然路由的目标服务有多个时,还需要做负载均衡
- 限流: 当请求流量过高时,在网关中按照下流的微服务能够接受的速度来放行请求,避免服务压力过大
2、项目使用
- 创建服务 gateway,引入依赖
spring-cloud-starter-gateway
与spring-cloud-starter-loadbalancer
- 在 yml 配置文件中编写基础配置和路由规则
- 启动网关服务进行测试:访问 http://网关地址/user/1时,符合 /user/** 规则,请求转发到 uri:http://userservice/user/1 上
spring:
gateway:
routes: # 网关路由配置:将符合 Path 规则的一切请求,都代理到 uri 参数指定的地址
- id: user-service # 路由id,自定义,只要唯一即可
uri: lb://userservice # 路由的目标地址,lb就是负载均衡,后面跟服务名称
predicates: # 路由断言,也就是判断请求是否符合路由规则的条件
- Path=/user/** # 这个是按照路径匹配,只要以/user/开头就符合要求
#filters: # 过滤器
#- AddRequestHeader=Truth, XTL! # 添加请求头
3、断言工厂
名称 | 说明 | 示例 |
---|---|---|
Path | 请求路径必须符合指定规则 | - Path=/red/{segment},/user/** |
After | 是某个时间点后的请求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Between | 是某两个时间点之前的请求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Header | 请求必须包含某些header | - Header=X-Request-Id,\d+ |
Method | 请求方式必须是指定方式 | - Method=GET,POST |
Query | 请求参数必须包含指定参数 | - Query=name |
4、过滤器工厂
GatewayFilter 是网关中提供的一种过滤器,可以对进入网关的请求和微服务返回的响应做处理
1、使用
- 在 yml 配置文件中添加
spring:cloud:gateway:routes:filters: - 过滤器名称=过滤值
- filters 只会对当前服务生效,想要全局生效,需要使用默认过滤器
default-filters
,它与 routes 同级
2、种类
Spring 提供了 33 种不同的路由过滤器工厂
名称 | 说明 |
---|---|
AddRequestHeader | 给当前请求添加一个请求头 |
RemoveRequestHeader | 移除请求中的一个请求头 |
AddResponseHeader | 给响应结果中添加一个响应头 |
RemoveResponseHeader | 从响应结果中移除有一个响应头 |
RequestRateLimiter | 限制请求的流量 |
3、自定义过滤器工厂
- 编写组件类继承
AbstractGatewayFilterFactory
,重写apply
方法,该类的名称一定要以GatewayFilterFactory
为后缀 - apply 方法中返回
GatewayFilter
匿名内部类对象,内部类实现filter
方法 - 在 filter 方法中实现自己的逻辑
- 在 yml 配置文件中的 default-filters 或者 filter 中指明才能使用,名字是类名称的前缀,然后该自定义过滤器就生效了
- 想要实现参数传递,需要配置 Config 内部类来定义属性充当参数,再重写 shortcutFieldOrder 方法实现参数传递
//实现参数传递的自定义过滤器工程
@Component
public class PrintAnyGatewayFilterFactory // 父类泛型是内部类的Config类型
extends AbstractGatewayFilterFactory<PrintAnyGatewayFilterFactory.Config> {
@Override
public GatewayFilter apply(Config config) {
// OrderedGatewayFilter是GatewayFilter的子类,包含两个参数:
// - GatewayFilter:过滤器
// - int order值:值越小,过滤器执行优先级越高
return new OrderedGatewayFilter(new GatewayFilter() {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 获取config值
String a = config.getA();
String b = config.getB();
String c = config.getC();