当前位置: 首页 > news >正文

Gateway学习和源码解析

文章目录

  • 什么是网关?
  • 搭建实验项目
    • demo-service
    • gateway-service
    • 尝试简单上手
  • 路由(Route)
  • 断言(Predicate)和断言工厂(Predicate Factory)
    • gateway自带的断言工厂
      • After(请求必须在某个时间点之后)
      • Before(请求必须在某个时间点之前)
      • Between(请求必须在某两个时间点之间)
      • Cookie(请求必须包含某些cookie)
      • Header(请求必须包含某些header)
      • Host(请求的主机名或IP地址必须符合规则)
      • Method(请求方式必须是指定的方式)
      • Path(请求路径必须符合指定规则)
      • Query(请求参数必须包含指定参数 )
      • RemoteAddr(请求者的IP必须符合规则)
      • Weight(权重)
      • XForwarded Remote Addr(请求者的IP必须符合规则(考虑代理服务器影响))
    • 疑问
      • 当一个路由下有多个断言,是只需要满足其中任意一个,还是必须都满足?
      • 当有多个路由都匹配的情况,会选择哪一个路由?
    • 自定义断言工厂
  • 过滤器(Filter)
    • 局部过滤器(Gateway Filter)
      • AddRequestHeader
      • AddRequestParameter
      • AddResponseHeader
      • 自定义局部过滤器
    • 全局过滤器(Global Filter)
      • 自定义全局过滤器
  • 跨域问题解决
  • 与nacos集成
  • 负载均衡
    • 自定义负载均衡
  • 请求过程源码解析

什么是网关?

当我们的微服务越来越多,外部需要访问,为了安全,我们需要做身份认证,认证通过,才能访问我们的服务,而当请求过来,我们需要根据请求的不同,路由到不同的服务中,而一个服务会有多个实例,请求过来还需要做负载均衡,还有当请求过多的时候,我们需要做限流,以上这些都需要我们的网关来实现。目前网关有 ZuulGateway,因为 Zuul 是阻塞式编程,而 Gateway 是基于 Spring webFlux 的响应式编程,所以 Gateway 的吞吐会更好,下面我们就来学习 Gateway

搭建实验项目

为了学习 Gateway,我们先搭建两个项目,一个是业务服务项目(demo-service),一个是 Gateway 服务项目(gateway-service),初始配置如下:

demo-service

pom.xml 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.13</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.victor.demo</groupId><artifactId>demo-service</artifactId><version>0.0.1-SNAPSHOT</version><name>样例服务</name><description>样例服务</description><properties><java.version>17</java.version></properties><dependencies><!-- Spring Boot --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency></dependencies></project>

application.yml 配置如下:

server:#端口号改成8081port: 8081spring:application:name: demo

gateway-service

pom.xml 配置如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.13</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>com.victor.gateway</groupId><artifactId>gateway-service</artifactId><version>0.0.1-SNAPSHOT</version><name>gateway网关</name><description>gateway网关</description><properties><java.version>17</java.version></properties><dependencies><!-- Gateway 网关 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId><version>3.1.8</version></dependency></dependencies></project>

application.yml 配置如下:

spring:application:name: gateway

gateway没有修改端口号,默认8080

尝试简单上手

我们先不使用 gateway,直接在 demo-service 里添加 controller,启动服务

package com.victor.demo.controller;import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class DemoController {@RequestMapping("/hello")public String hello() {return "hello gateway";}
}

在浏览器中输入 http://127.0.0.1:8081/hello,效果如下:

image-20230915144713655

下面我们加入 gateway,通过 gateway 路由到 demo-service 服务,首先修改下 gateway 的application.yml配置文件,如下所示:

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Path=/demo-service/**filters:- StripPrefix=1

在浏览器中输入 http://127.0.0.1:8080/demo-service/hello,效果如下:

image-20230915144646140

可以达到一样的效果,成功!gateway 里的配置起作用了,首先我们的请求匹配的上 /demo-service/** ,然后StripPrefix=1 会跳过开头的 /demo-service,所以 http://127.0.0.1:8080/demo-service/hello 就被路由到了 http://127.0.0.1:8081/hello 上。

下面我们就来学习 gateway 的配置, spring.could.gateway 是 gateway 的标准配置前缀,我们重点看 routes,这是一个复数,可以包含多个 route,我们可以配置多个路由转发

路由(Route)

路由下面包含了以下参数(我们可以看看 route 的源码,是一个叫 RouteDefinition 的类):

  • id:路由的唯一标识符
  • predicates:断言,用于判断请求是否符合条件,符合就转发路由目的地,可以配置多个
  • filters:过滤器,用于处理请求和响应,可以配置多个
  • uri:指定目标服务的 uri,也就是最终转发到的服务,支持 http 和 lb(负载均衡) 两种方式
  • metadata:元数据,key-value 键值对
  • order:多个路由之间排序,数值越小匹配优先级越高

下面结合官方文档 https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html,具体实际操作一下断言和过滤器的功能

断言(Predicate)和断言工厂(Predicate Factory)

断言(Predicate)用于定义路由规则中的条件匹配逻辑,它会根据请求的属性进行判断,决定是否应用这个路由规则,我们在配置文件里写的断言(Predicate)只是个字符串,最终这个字符串会被一个叫断言工厂(Predicate Factory)的东西解析,转换成真正的判断条件,上面我们实验的例子用到了 Path 方式的断言,其对应的断言工厂类是 PathRoutePredicateFactory,(其实就是配置文件里key,后面 + RoutePredicateFactory)除了这个,Gateway 还提供了许多预定义的断言工厂,如下:

名称说明样例
After请求必须在某个时间点之后- After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]
Before请求必须在某个时间点之前- Before=2023-09-15T17:00:00.000+08:00[Asia/Shanghai]
Between请求必须在某两个时间点之间- Between=2023-09-15T16:00:00.000+08:00[Asia/Shanghai], 2023-09-15T17:00:00.000+08:00[Asia/Shanghai]
Cookie请求必须包含某些cookie- Cookie=chocolate, ch.p
Header请求必须包含某些header- Header=X-Request-Id, \d+
Host请求的主机名或IP地址必须符合规则- Host=127.0.0.1:8080
Method请求方式必须是指定的方式- Method=POST
Path请求路径必须符合指定规则- Path=/demo-service/hello
Query请求参数必须包含指定参数- Query=green
RemoteAddr请求者的ip必须符合规则- RemoteAddr=127.0.0.1
Weight权重- Weight=group, 5
XForwardedRemoteAddr请求者的ip必须符合规则(考虑代理服务器影响)- XForwardedRemoteAddr=192.168.33.33

gateway自带的断言工厂

After(请求必须在某个时间点之后)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]

表示在 2023-09-15T16:00:00.000+08:00[Asia/Shanghai] 这个时间之后的请求才会被匹配到这个路由

现在时间是 2023-09-15 16:05,我们用postman试下

image-20230915160824051

可以的,我们改下配置的时间为16号,2023-09-16T16:00:00.000+08:00[Asia/Shanghai]

image-20230915160956609

可以看到报404

Before(请求必须在某个时间点之前)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Before=2023-09-15T17:00:00.000+08:00[Asia/Shanghai]

表示在 2023-09-15T17:00:00.000+08:00[Asia/Shanghai] 这个时间之前的请求才会被匹配到这个路由

After 类似,就不贴例子了,可以自己试试

Between(请求必须在某两个时间点之间)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Between=2023-09-15T16:00:00.000+08:00[Asia/Shanghai], 2023-09-15T17:00:00.000+08:00[Asia/Shanghai]

表示在 2023-09-15T16:00:00.000+08:00[Asia/Shanghai]2023-09-15T17:00:00.000+08:00[Asia/Shanghai] 两个时间点之间个路由

AfterBefore类似,就不贴例子了,可以自己试试

Cookie(请求必须包含某些cookie)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Cookie=chocolate, ch.p

表示 cookie 中包含 chocolate 参数,并且值是 ch.p 的请求才会被匹配到这个路由

我们先不添加 cookie 试一下

image-20230915163822441

不出所料404,然后我们添加一下cookie

image-20230915163927810

image-20230915165229612

成功了!

也可以配置多个,像这样

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Cookie=chocolate, ch.p- Cookie=abc, 123

这样必须包含两个 cookie 才行

Header(请求必须包含某些header)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Header=X-Request-Id, \d+

表示请求头里有一个 key 是 X-Request-Id,值需要匹配到正则表达式 [\d+] 才会匹配到这个路由

我们先不添加头信息

image-20230915170750191

没错,404

然后再加上头信息

image-20230915170831018

成功!

这里也可以这么写 - Header=X-Request-Id,表示只要有这个参数就可以,不在乎值是多少

当然也可以像 cookie 那样配置多个

Host(请求的主机名或IP地址必须符合规则)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Host=127.0.0.1:8080

请求的主机名或IP地址必须是 127.0.0.1:8080 才会被匹配到这个路由

按照上面的配置请求是成功的,但凡换个别的配置,就404,就不贴图了

Method(请求方式必须是指定的方式)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Method=POST

如果是 POST 请求才会被匹配到这个路由

这里我的接口是 GET,就会报404,如果要既支持GET,又支持POST,就用逗号分割,像这样- Method=GET,POST

Path(请求路径必须符合指定规则)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Path=/hello

请求路径中能匹配到 /hello 才会被匹配到这个路由

这个其实是我们最常用的,一般用来根据匹配不同的服务,把请求路由到不同的服务那边

Query(请求参数必须包含指定参数 )

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Query=color, red

请求中必须包含参数 color 并且值是 red 才会被匹配到这个路由

image-20230915174613650

这里也可以这么写 - Query=color,表示只要有这个参数就可以,不在乎值是多少,和 Header 相似,也可以添加多个

RemoteAddr(请求者的IP必须符合规则)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- RemoteAddr=127.0.0.1

请求者的 IP 必须是 127.0.0.1 才会被匹配到这个路由

Weight(权重)

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Weight=group, 8filters:- AddRequestParameter=color, red-   id: demo-service2uri: http://127.0.0.1:8081predicates:- Weight=group, 2filters:- AddRequestParameter=color, blue

接收两个参数,一个 group,一个 weight,80%的请求会被发送至 demo-service,20%的请求会被发送至demo-service2

配置文件里增加 filters 是为了方便验证,我们再 给 demo-service 增加个 controller

@RequestMapping("/color")
public String hello(@RequestParam("color") String color) {return "this color is " + color;
}

好,来试下

image-20230918084531677

image-20230918084705304

会发现有时是 this color is red 有时是 this color is blue,但是前者次数更多,因为权重大

XForwarded Remote Addr(请求者的IP必须符合规则(考虑代理服务器影响))

修改 application.yml 配置文件

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- XForwardedRemoteAddr=192.168.33.33

请求者的 IP 必须是 192.168.33.33 才会被匹配到这个路由

RemoteAddr 不一样的是,因为 gateway 前面可能会有代理服务器,使用 RemoteAddr 去匹配的是代理服务器的 IP,所以就有了这个断言,这个断言会通过从头信息中的 X-Forwarded-For 参数去获取客户端原始 IP 地址,然后去匹配。

来试试

image-20230915180543152

果然可以!

疑问

当一个路由下有多个断言,是只需要满足其中任意一个,还是必须都满足?

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]- Cookie=chocolate, ch.p

结论:当一个路由下有多个断言时,所有条件都必须满足才能匹配此路由

当有多个路由都匹配的情况,会选择哪一个路由?

首先我们配置两个路由,条件一致

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Method=GET,POSTfilters:- AddRequestParameter=color, red-   id: demo-service2uri: http://127.0.0.1:8081predicates:- Method=GET,POSTfilters:- AddRequestParameter=color, blue

添加参数是为了好区分,还是使用color接口

@RequestMapping("/color")
public String hello(@RequestParam("color") String color) {return "this color is " + color;
}

结果如下:

image-20230918093242651

始终走的第一个路由 demo-service

我们改动一下,把 demo-service2 的 order 改成 -1,试下

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- Method=GET,POSTfilters:- AddRequestParameter=color, red-   id: demo-service2uri: http://127.0.0.1:8081order: -1predicates:- Method=GET,POSTfilters:- AddRequestParameter=color, blue

image-20230918093354680

发现走第二个路由 demo-service2 了

结论:当一个请求满足多个路由的断言(Predicate)条件时,order较小的路由会生效(如果不配置默认是0,如果order一致,排在前面的生效)

自定义断言工厂

gateway 提供了这么多断言工厂,也可能不满足我们的实际应用场景,这时候,就需要我们自己写断言工厂了,我们发现在配置文件里的 key + RoutePredicateFactory 就是断言工厂的名字,所以我们写自定义的断言工厂,也要遵循这个规则,参考了 AfterRoutePredicateFactory 的源码,我决定写一个,通过配置文件中配置星期几,来控制路由的断言工厂

先写个断言工厂类(DayOfWeekRoutePredicateFactory):

package com.victor.gateway.config;import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;import javax.validation.constraints.NotNull;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.util.Arrays;
import java.util.List;
import java.util.function.Predicate;@Component
public class DayOfWeekRoutePredicateFactory extends AbstractRoutePredicateFactory<DayOfWeekRoutePredicateFactory.Config> {public static final String DAY_OF_WEEK_KEY = "dayOfWeek";public DayOfWeekRoutePredicateFactory() {super(Config.class);}@Overridepublic List<String> shortcutFieldOrder() {return Arrays.asList(DAY_OF_WEEK_KEY);}@Overridepublic Predicate<ServerWebExchange> apply(Config config) {return new GatewayPredicate() {@Overridepublic boolean test(ServerWebExchange serverWebExchange) {final DayOfWeek currentDayOfWeek = DayOfWeek.from(LocalDate.now());return currentDayOfWeek.equals(config.getDayOfWeek());}@Overridepublic Object getConfig() {return config;}@Overridepublic String toString() {return String.format("DayOfWeek: %s", config.getDayOfWeek());}};}public static class Config {@NotNullprivate DayOfWeek dayOfWeek;public DayOfWeek getDayOfWeek() {return dayOfWeek;}public void setDayOfWeek(DayOfWeek dayOfWeek) {this.dayOfWeek = dayOfWeek;}}
}

然后修改配置文件:

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- DayOfWeek=MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY

意思就是当工作日的时候,会匹配此路由

过滤器(Filter)

在 Gateway中,过滤器是用于在请求进入网关和离开网关之间执行一些逻辑的组件。分为局部过滤器(Gateway Filter)全局过滤器(Global Filter)

局部过滤器(Gateway Filter)

局部过滤器只对特定的路由起作用,Gateway 内部提供了30多种 Gateway Filter,这里就简单举几个例子,等需要用到的时候,查阅官方文档:https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#gatewayfilter-factories,下面取自官方文档中的例子:

AddRequestHeader

spring:cloud:gateway:routes:- id: add_request_header_routeuri: https://example.orgfilters:- AddRequestHeader=X-Request-red, blue

所有匹配到这个路由的请求的请求头都添加: 名字是 Header X-Request-red,值是 blue

AddRequestParameter

spring:cloud:gateway:routes:- id: add_request_parameter_routeuri: https://example.orgfilters:- AddRequestParameter=red, blue

所有匹配到这个路由的请求都添加一个参数: 名字是 red,值是 blue

AddResponseHeader

spring:cloud:gateway:routes:- id: add_response_header_routeuri: https://example.orgfilters:- AddResponseHeader=X-Response-Red, Blue

所有匹配到这个路由的请求的响应头添加: 名字是 X-Response-Red,值是 Blue

自定义局部过滤器

命名和断言工厂一样,是配置文件里的 key + GatewayFilterFactory ,可以实现接口 GatewayFilterFactory,也可以继承抽象类 AbstractGatewayFilterFactory

package com.victor.gateway.config;import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;@Component
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory {@Overridepublic GatewayFilter apply(Object config) {return (exchange, chain) -> {//----------------处理业务逻辑start----------------System.out.println("自定义过滤器");//----------------处理业务逻辑end----------------return chain.filter(exchange);};}
}

配置文件改下

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: http://127.0.0.1:8081predicates:- After=2023-09-15T16:00:00.000+08:00[Asia/Shanghai]filters:- Custom

因为没有参数,就直接写成 - Custom

全局过滤器(Global Filter)

全局过滤器对所有请求进行过滤,同样的 Gateway 内部提供了一些全局过滤器,查阅官方文档:https://docs.spring.io/spring-cloud-gateway/docs/3.1.8/reference/html/#global-filters,大致如下:

名称作用
ForwardRoutingFilter请求转发
ReactiveLoadBalancerClientFilter客户端负载均衡
WebClientHttpRoutingFilter将HTTP请求通过 WebClient 进行路由
NettyWriteResponseFilter代理响应写回网关的客户端
RouteToRequestUrlFilter请求路由到目标 URL
GatewayMetricsFilter收集和记录网关的度量指标

但是,关于有些强业务的过滤器,Gateway本身没办法帮我们实现,这时候就需要我们自己写自定义的全局过滤器

自定义全局过滤器

全局过滤器的命名就没有那么多讲究了,只要实现了 GlobalFilter 接口就可以了,这里写了一个过滤器,校验请求头中是否包含了一个名为 token 值是 123456,如果包含就通过,否则返回 401 错误码

package com.victor.gateway.config;import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;@Component
public class CustomGlobalFilter implements GlobalFilter, Ordered {/**** @param exchange 上下文信息,可以获取request、response等信息* @param chain 过滤器链,用来把请求委托给下一个过滤器* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//----------------处理业务逻辑start----------------if (!"123456".equals(exchange.getRequest().getHeaders().getFirst("token"))) {exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);return exchange.getResponse().setComplete();}//----------------处理业务逻辑end----------------return chain.filter(exchange);}@Overridepublic int getOrder() {return -1;}
}

可以从 exchange 中获取上下文需要的各类信息,如 requestresponse 等等,然后从 requestresponse 里又可以获取 headercookieuripath 等信息,过滤器也是需要分先后执行的,这时候就可以实现 Ordered 接口,重写 getOrder 方法,返回一个 int 值,这个值越小,优先级就越高。也可以通过添加 @Order 注解实现

不知道怎么用的,可以参考上面 Gateway 自带的那些过滤器。

跨域问题解决

在配置文件里如下配置:

spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: true #解决options请求被拦截问题cors-configurations:'[/**]':allowedOrigins: #允许哪些网站的跨域请求- "https://docs.spring.io"allowedMethods: #允许的跨域ajax请求方式- GET- POSTallowedHeaders: "*" #允许请求头里携带的信息allowCredentials: true #是否允许携带cookiemaxAge: 360000 #这次跨域检测有效期

与nacos集成

nacos 是既可以做注册中心,又可以做配置中心的,这边只讲做注册中心,有机会单独写篇文章讲配置中心

首先本地起一个 nacos,默认端口8848,这个教程网上很多,这里就不讲了

第一步,引入依赖

gateway-servicedemo-service 分别引入 nacos 的依赖,注意还需要引入 loadbalancer 依赖,还有就是 nacos 的配置,我们一般放在 bootstrap.yml 文件里(因为 bootstrap.yml 加载顺序早于 application.yml,用 nacos 做配置中心时,需要先加载 nacos 的配置),所以还需要引入 bootstrap 依赖,如下:

<!-- 负载均衡 -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-loadbalancer</artifactId><version>3.1.7</version>
</dependency><!-- Nacos 服务发现依赖 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>2021.0.5.0</version>
</dependency><!-- Nacos 配置中心依赖 -->
<!--<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId><version>2021.0.5.0</version>
</dependency>--><!-- bootstrap -->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId><version>3.1.7</version>
</dependency>

用作配置中心的时候还需要引入 spring-cloud-starter-alibaba-nacos-config,这里先注释掉了

第二步,添加 @EnableDiscoveryClient 注解

一般我们把这个注解写在启动类上,在 gateway-servicedemo-service 的启动类上增加 @EnableDiscoveryClient 注解

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

第三步,修改配置文件

gateway-servicedemo-service 添加 bootstrap.yml,并增加 nacos 配置

spring:cloud:# Nacos配置nacos:discovery:server-addr: 127.0.0.1:8848namespace: devconfig:enabled: false

然后,需要额外改下 gateway 配置中的 uri 改成 lb://demo

spring:application:name: gatewaycloud:gateway:routes:-   id: demo-serviceuri: lb://demo #修改成负载均衡的方式predicates:- Path=/demo-service/**filters:- StripPrefix=1

第四步,启动

因为我们配了 nacosnamespacedev,所以先要在 nacos 的页面配置这个命名空间,注意 ID 写 dev,然后启动 nacosgateway-servicedemo-service,打开 nacos 的页面 http://localhost:8848/nacos

image-20230920151315274

在服务列表里选择 dev 命名空间(如何没配的话,会在 public 下),可以看到我们注册进来的两个服务

再试着用 postman 调用下接口,是可以的

image-20230920151814476

负载均衡

Gateway 路由配置里 uri 配置了 lb:// 开头,就会走负载均衡的逻辑,具体代码在 ReactiveLoadBalancerClientFilterfilter 方法里(ReactiveLoadBalancerClientFilter 是个全局过滤器,上面讲过了)

image-20230921102919833

重点看choose方法

image-20230921103532298

最终会走到 ReactorLoadBalancer 接口的 choose 方法

image-20230921103752859

ReactorLoadBalancer 接口有3个实现,默认走的是 RoundRobinLoadBalancer

image-20230921104106915

负载均衡的代码就在 getInstanceResponse 方法里

如果我们要切换成 NacosLoadBalancer 怎么操作呢,只需要在配置类上加上这么一句 @LoadBalancerClients(defaultConfiguration = NacosLoadBalancerClientConfiguration.class)

image-20230921104352562

自定义负载均衡

如果我们想自己实现一个负载均衡逻辑,怎么操作呢,比如我们要写一个灰度负载均衡,根据请求头信息里的版本号去匹配对应的服务,怎么做?

第一步,给 demo-service 服务配上版本号元数据(gray-version)

spring:cloud:# Nacos配置nacos:discovery:server-addr: 127.0.0.1:8848namespace: devmetadata:gray-version: 1.0.0config:enabled: false

第二步,在 gateway-service 服务里模仿 RoundRobinLoadBalancer 或者 NacosLoadBalancer 写一个 GrayLoadBalancer

public class GrayLoadBalancer implements ReactorServiceInstanceLoadBalancer {private static final Log log = LogFactory.getLog(RandomLoadBalancer.class);private static final String GRAY_VERSION = "gray-version";private final String serviceId;private ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider;private final NacosDiscoveryProperties nacosDiscoveryProperties;/*** @param serviceInstanceListSupplierProvider a provider of*                                            {@link ServiceInstanceListSupplier} that will be used to get available instances* @param serviceId                           id of the service for which to choose an instance*/public GrayLoadBalancer(ObjectProvider<ServiceInstanceListSupplier> serviceInstanceListSupplierProvider,String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {this.serviceId = serviceId;this.serviceInstanceListSupplierProvider = serviceInstanceListSupplierProvider;this.nacosDiscoveryProperties = nacosDiscoveryProperties;}@SuppressWarnings("rawtypes")@Overridepublic Mono<Response<ServiceInstance>> choose(Request request) {ServiceInstanceListSupplier supplier = serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);return supplier.get(request).next().map(serviceInstances -> processInstanceResponse(supplier, serviceInstances, request));}private Response<ServiceInstance> processInstanceResponse(ServiceInstanceListSupplier supplier, List<ServiceInstance> serviceInstances, Request request) {Response<ServiceInstance> serviceInstanceResponse = getInstanceResponse(serviceInstances, request);if (supplier instanceof SelectedInstanceCallback && serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());}return serviceInstanceResponse;}private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> instances, Request request) {if (instances.isEmpty()) {if (log.isWarnEnabled()) {log.warn("No servers available for service: " + serviceId);}return new EmptyResponse();}//具体的选择逻辑ServiceInstance instance;RequestDataContext context = (RequestDataContext) request.getContext();String grayVersion = context.getClientRequest().getHeaders().getFirst(GRAY_VERSION);if (StringUtils.isNotEmpty(grayVersion)) {List<ServiceInstance> instancesToChoose = instances.stream().filter(i -> grayVersion.equals(i.getMetadata().get(GRAY_VERSION))).collect(Collectors.toList());instance = NacosBalancer.getHostByRandomWeight3(instancesToChoose);} else {instance = NacosBalancer.getHostByRandomWeight3(instances);}return new DefaultResponse(instance);}
}

第三步,在 gateway-service 服务里写一个 GrayLoadBalancerClientConfiguration 注册 GrayLoadBalancer

@ConditionalOnDiscoveryEnabled
public class GrayLoadBalancerClientConfiguration {@Bean@ConditionalOnMissingBeanpublic ReactorLoadBalancer<ServiceInstance> grayLoadBalancer(Environment environment, LoadBalancerClientFactory loadBalancerClientFactory,NacosDiscoveryProperties nacosDiscoveryProperties) {String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME);return new GrayLoadBalancer(loadBalancerClientFactory.getLazyProvider(name, ServiceInstanceListSupplier.class), name, nacosDiscoveryProperties);}
}

第四步,在 gateway-service 服务启动类上增加注解 @LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfiguration.class)

@SpringBootApplication
@EnableDiscoveryClient
@LoadBalancerClients(defaultConfiguration = GrayLoadBalancerClientConfiguration.class)
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}}

这样就可以了,然后我们请求的时候,在头信息中增加 gray-version,值是 1.0.0,就会在配置了元数据 gray-version 等于1.0.0 的服务里选择一个

请求过程源码解析

我们知道 Gateway 用的是 webFlux 响应式编程,webFlux 处理请求的入口方法是 DispatcherHandlerhandle 方法,我们从这里开始

image-20230919112344389

看到第一个行的 handlerMappings,我就愣住了,这个东西有点眼熟啊,看看在哪里赋值的

image-20230919130122971

这个方法名也眼熟,handlerAdapter 也眼熟,在 SpringMVC 的源码里有见过,原来 Spring webFlux 和 SpringMVC 在设计上有一些相似之处

框架Spring webFluxSpringMVC
分发DispatcherHandlerDispatcherServlet
映射HandlerMappingHandlerMapping
适配HandlerAdapterHandlerAdapter
处理WebHandlerHandler

关于 SpringMVC 请求处理流程的源码可以看我的另一篇文章:SpringMVC源码学习笔记之请求处理流程

说回 Gateway 我们继续看 DispatcherHandlerhandle 方法

//DispatcherHandler
@Override
public Mono<Void> handle(ServerWebExchange exchange) {if (this.handlerMappings == null) {return createNotFoundError();}if (CorsUtils.isPreFlightRequest(exchange.getRequest())) {return handlePreFlight(exchange);}return Flux.fromIterable(this.handlerMappings) //遍历handlerMappings.concatMap(mapping -> mapping.getHandler(exchange)) //找到对应的WebHandler.next().switchIfEmpty(createNotFoundError()).flatMap(handler -> invokeHandler(exchange, handler)) //找到适配的HandlerAdapter处理WebHandler.flatMap(result -> handleResult(exchange, result));//找到对应的HandlerResultHandler处理result
}

整体流程大概就是,遍历所有的 handlerMappings,然后找到对应的 WebHandler,再为 WebHandler 找到适配的 HandlerAdapter,用这个 HandlerAdapter 处理 WebHandler,最后再为结果找到对应的 HandlerResultHandler 处理 result

我们先看 mapping.getHandler,来到 AbstractHandlerMapping (HandlerMapping 接口的抽象实现)类的 getHandler 方法

//AbstractHandlerMapping
@Override
public Mono<Object> getHandler(ServerWebExchange exchange) {//获取匹配的handlerreturn getHandlerInternal(exchange).map(handler -> {if (logger.isDebugEnabled()) {logger.debug(exchange.getLogPrefix() + "Mapped to " + handler);}ServerHttpRequest request = exchange.getRequest();//有配置跨域相关配置的处理if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {CorsConfiguration config = (this.corsConfigurationSource != null ?this.corsConfigurationSource.getCorsConfiguration(exchange) : null);CorsConfiguration handlerConfig = getCorsConfiguration(handler, exchange);config = (config != null ? config.combine(handlerConfig) : handlerConfig);if (config != null) {config.validateAllowCredentials();}if (!this.corsProcessor.process(config, exchange) || CorsUtils.isPreFlightRequest(request)) {return NO_OP_HANDLER;}}return handler;});
}

重点看 getHandlerInternal 方法是怎么获取匹配的 webHandler 的,来到 AbstractHandlerMapping 的子类 RoutePredicateHandlerMappinggetHandlerInternal 方法

//RoutePredicateHandlerMapping
@Override
protected Mono<?> getHandlerInternal(ServerWebExchange exchange) {// don't handle requests on management port if set and different than server portif (this.managementPortType == DIFFERENT && this.managementPort != null&& exchange.getRequest().getLocalAddress() != null&& exchange.getRequest().getLocalAddress().getPort() == this.managementPort) {return Mono.empty();}exchange.getAttributes().put(GATEWAY_HANDLER_MAPPER_ATTR, getSimpleName());return lookupRoute(exchange)// .log("route-predicate-handler-mapping", Level.FINER) //name this.flatMap((Function<Route, Mono<?>>) r -> {exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);if (logger.isDebugEnabled()) {logger.debug("Mapping [" + getExchangeDesc(exchange) + "] to " + r);}//把路由放入到上下文中exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r);return Mono.just(webHandler);}).switchIfEmpty(Mono.empty().then(Mono.fromRunnable(() -> {exchange.getAttributes().remove(GATEWAY_PREDICATE_ROUTE_ATTR);if (logger.isTraceEnabled()) {logger.trace("No RouteDefinition found for [" + getExchangeDesc(exchange) + "]");}})));
}

重点是 lookupRoute 方法,这个方法会过滤出符合请求的路由,并通过 exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, r); 方法把匹配到的路由放入上下文中,后续有用,我们进去看看 lookupRoute 方法

//RoutePredicateHandlerMapping
protected Mono<Route> lookupRoute(ServerWebExchange exchange) {return this.routeLocator.getRoutes()// individually filter routes so that filterWhen error delaying is not a// problem.concatMap(route -> Mono.just(route).filterWhen(r -> {// add the current route we are testingexchange.getAttributes().put(GATEWAY_PREDICATE_ROUTE_ATTR, r.getId());return r.getPredicate().apply(exchange);})// instead of immediately stopping main flux due to error, log and// swallow it.doOnError(e -> logger.error("Error applying predicate for route: " + route.getId(), e)).onErrorResume(e -> Mono.empty()))// .defaultIfEmpty() put a static Route not found// or .switchIfEmpty()// .switchIfEmpty(Mono.<Route>empty().log("noroute")).next()// TODO: error handling.map(route -> {if (logger.isDebugEnabled()) {logger.debug("Route matched: " + route.getId());}validateRoute(route, exchange);return route;});/** TODO: trace logging if (logger.isTraceEnabled()) {* logger.trace("RouteDefinition did not match: " + routeDefinition.getId()); }*/
}

this.routeLocator.getRoutes() 先获取所有 yml 文件中的路由信息,包含下面的断言等信息,并且按照 order 排好序返回,然后调用 Mono.just(route).filterWhen(r -> {...}),根据路由中断言条件来判断是否当前请求是否匹配这个路由的断言规则,next() 会获取到第一个匹配的路由,最后返回这个路由,找到后,返回上一个方法里,会返回一个FilteringWebHandler 类型的 webHandler ,到这 getHandler 结束了,找到了匹配的 webHandler

继续看下面,我这边再贴一次 DispatcherHandlerhandle 方法

image-20230919112344389

继续看 invokeHandler 方法

//DispatcherHandler
private Mono<HandlerResult> invokeHandler(ServerWebExchange exchange, Object handler) {if (ObjectUtils.nullSafeEquals(exchange.getResponse().getStatusCode(), HttpStatus.FORBIDDEN)) {return Mono.empty();  // CORS rejection}if (this.handlerAdapters != null) {for (HandlerAdapter handlerAdapter : this.handlerAdapters) {if (handlerAdapter.supports(handler)) {return handlerAdapter.handle(exchange, handler);}}}return Mono.error(new IllegalStateException("No HandlerAdapter: " + handler));
}

可以看到这里循环所有的 handlerAdapters 来找到匹配 webHandlerHandlerAdapter

image-20230919151050830

可以看到是 SimpleHandlerAdapter ,我们跟进去看

@Override
public Mono<HandlerResult> handle(ServerWebExchange exchange, Object handler) {WebHandler webHandler = (WebHandler) handler;Mono<Void> mono = webHandler.handle(exchange);return mono.then(Mono.empty());
}

上面我们知道返回的 webHandler 类型是 FilteringWebHandler,所以这里我们继续进到 FilteringWebHandlerhandle 方法

@Override
public Mono<Void> handle(ServerWebExchange exchange) {//从上下文中获取匹配的路由Route route = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);//获取路由中的局部过滤器List<GatewayFilter> gatewayFilters = route.getFilters();//全局过滤器List<GatewayFilter> combined = new ArrayList<>(this.globalFilters);//把局部过滤器和全局过滤器合并combined.addAll(gatewayFilters);// TODO: needed or cached?//把所有的过滤器排序AnnotationAwareOrderComparator.sort(combined);if (logger.isDebugEnabled()) {logger.debug("Sorted gatewayFilterFactories: " + combined);}//执行过滤器链中的每一个过滤器方法return new DefaultGatewayFilterChain(combined).filter(exchange);
}

这个方法,先从上下文中取出之前放入的路由信息,然后从路由信息里取出局部过滤器,和全局过滤器合并,然后排序,最后执行每一个过滤器方法

这里有点好奇,我们记得全局过滤器实现的是 GlobalFilter 接口,为什么可以放入到 List<GatewayFilter> combined 集合里,于是看了下 this.globalFilters 是怎么赋值的

private final List<GatewayFilter> globalFilters;public FilteringWebHandler(List<GlobalFilter> globalFilters) {this.globalFilters = loadFilters(globalFilters);
}private static List<GatewayFilter> loadFilters(List<GlobalFilter> filters) {return filters.stream().map(filter -> {GatewayFilterAdapter gatewayFilter = new GatewayFilterAdapter(filter);if (filter instanceof Ordered) {int order = ((Ordered) filter).getOrder();return new OrderedGatewayFilter(gatewayFilter, order);}return gatewayFilter;}).collect(Collectors.toList());
}

发现是在实例化 FilteringWebHandler 的时候,通过 loadFilters 方法赋值,这个方法,把 GlobalFilter 转成了 GatewayFilter,可以看到这里对 GlobalFilter 做了一层包装,包装成了 GatewayFilterAdapter,如果是排序的 GlobalFilter ,还要再包一层 OrderedGatewayFilter ,我们看下 GatewayFilterAdapter 的代码

private static class GatewayFilterAdapter implements GatewayFilter {private final GlobalFilter delegate;GatewayFilterAdapter(GlobalFilter delegate) {this.delegate = delegate;}@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {return this.delegate.filter(exchange, chain);}@Overridepublic String toString() {final StringBuilder sb = new StringBuilder("GatewayFilterAdapter{");sb.append("delegate=").append(delegate);sb.append('}');return sb.toString();}}

这里用到了适配器模式,用一个适配器类(GatewayFilterAdapter)实现 GatewayFilter 接口,构造函数接收一个 GlobalFilter 对象,把它包装成 GatewayFilter

相关文章:

Gateway学习和源码解析

文章目录 什么是网关&#xff1f;搭建实验项目demo-servicegateway-service尝试简单上手 路由&#xff08;Route&#xff09;断言&#xff08;Predicate&#xff09;和断言工厂&#xff08;Predicate Factory&#xff09;gateway自带的断言工厂After&#xff08;请求必须在某个…...

移动机器人运动规划 --- 基于图搜索的Dijkstra算法

移动机器人运动规划 --- 基于图搜索的Dijkstra算法 Dijkstra 算法Dijkstra 算法 伪代码流程Dijkstra 算法步骤示例Dijkstra算法的优劣分析 Dijkstra 算法 Dijkstra 算法与BFS算法的区别就是 : 从容器中弹出接下来要访问的节点的规则不同 BFS 弹出: 层级最浅的原则&#xff0c…...

Mybatis SQL构建器

上一篇我们介绍了在Mybatis映射器中使用SelectProvider、InsertProvider、UpdateProvider、DeleteProvider进行对数据的增删改查操作&#xff1b;本篇我们介绍如何使用SQL构建器在Provider中优雅的构建SQL语句。 如果您对在Mybatis映射器中使用SelectProvider、InsertProvider…...

怎么将几张图片做成pdf合在一起

怎么将几张图片做成pdf合在一起&#xff1f;在我们平时的工作中&#xff0c;图片和pdf都是非常重要的电脑文件&#xff0c;使用也非常频繁&#xff0c;图片能够更为直观的展示内容&#xff0c;而pdf则更加的正规&#xff0c;很多重要文件大多会做成pdf格式的。在职场人的日常工…...

关于JPA +SpringBoot 遇到的一些问题及解决方法

关于JPA SpringBoot 遇到的一些问题及解决方法&#xff08;可能会有你正在遇到的&#xff09; 一、JpaRepository相关 1.1 org.springframework.dao.InvalidDataAccessResourceUsageException: Named parameter not bound : id; nested exception is org.hibernate.QueryEx…...

​全国馆藏《乡村振兴战略下传统村落文化旅游设计》许少辉八一著作——2023学生开学季辉少许

​全国馆藏《乡村振兴战略下传统村落文化旅游设计》许少辉八一著作——2023学生开学季辉少许...

linux升级glibc-2.28

1.准备工作 1.1升级gcc到gcc8 # 安装devtoolset-8-gcc yum install centos-release-scl yum install devtoolset-8 scl enable devtoolset-8 -- bash# 启用工具 source /opt/rh/devtoolset-8/enable # 安装GCC-8 yum install -y devtoolset-8-gcc devtoolset-8-gcc-c devtoolse…...

[Go疑难杂症]为什么nil不等于nil

现象 在日常开发中&#xff0c;可能一不小心就会掉进 Go 语言的某些陷阱里&#xff0c;而本文要介绍的 nil ≠ nil 问题&#xff0c;便是其中一个&#xff0c;初看起来会让人觉得很诡异&#xff0c;摸不着头脑。 先来看个例子&#xff1a; type CustomizedError struct {Err…...

C#60个常见的问题和答案

在本文中,我将帮助你准备好在下一次面试中解决这些与C# 编程语言相关的问题。同时,你可能想练习一些C# 项目。这 60 个基本的 C#面试问题和答案将帮助你了解该语言的技术概念。 目录 什么是 C#? 1.什么是类? 2.面向对象编程的主要概念是什么?...

11:STM32---spl通信

目录 一:SPL通信 1:简历 2:硬件电路 3:移动数据图 4:SPI时序基本单元 A : 开/ 终条件 B:SPI时序基本单元 A:模式0 B:模式1 C:模式2 D:模式3 C:SPl时序 A:发送指令 B: 指定地址写 C:指定地址读 二: W25Q64 1:简历 2: 硬件电路 3:W25Q64框图 4: Flash操作注意…...

kafka的 ack 应答机制

目录 一 ack 应答机制 二 ISR 集合 一 ack 应答机制 kafka 为用户提供了三种应答级别&#xff1a; all&#xff0c;leader&#xff0c;0 acks &#xff1a;0 这一操作提供了一个最低的延迟&#xff0c;partition的leader接收到消息还没有写入磁盘就已经返回ack&#x…...

Django系列:Django开发环境配置与第一个Django项目

Django系列 Django开发环境配置与第一个Django项目 作者&#xff1a;李俊才 &#xff08;jcLee95&#xff09;&#xff1a;https://blog.csdn.net/qq_28550263 邮箱 &#xff1a;291148484163.com 本文地址&#xff1a;https://blog.csdn.net/qq_28550263/article/details/1328…...

iPad协议/微信协议最新版

一、了解微信的协议 在开发微信协议之前&#xff0c;需要先了解微信的协议。微信的协议包括登录协议、消息传输协议、文件传输协议、数据同步协议等。其中&#xff0c;登录协议是最重要的协议之一&#xff0c;包括登录验证、登录认证等。消息传输协议则是微信最核心的功能之一…...

URL字符解码

将网页编码文字还原&#xff1a; 例如&#xff1a;https%3A%2F%2Fwww.example.com%2F%3Fparam%3Dvalue%26key%3D%E4%B8%AD%E6%96%87 解码&#xff1a; https: // www.example.com/?paramvalue&key中文 代码&#xff1a; char hexValue(char ch) {if (isdigit(ch)){re…...

uni-app进行表单效验

Uni-app内置了一些表单验证方法&#xff0c;可以帮助我们对表单进行有效的验证。以下是一些常用的验证方法&#xff1a; 非空验证&#xff1a; if(!this.formData.name){uni.showToast({title: 请输入姓名,icon: none});return false; }手机号码验证&#xff1a; const phon…...

IO流内容总结

IO流作用 对文件或者网络中的数据进行读写操作。 简单记&#xff1a;输入流读数据&#xff0c;输出流写数据。 Java的输出流主要以OutputStream和Writer作为基类&#xff0c;输入流主要是以InputStream和Reader作为基类。 按处理数据单元分类 字节流 字节输入流&#xff…...

MySQL的进阶篇1-MySQL的存储引擎简介

存储引擎 MySQL的体系结构 0、客户端连机器【java、Python、JDBC等】 1、【MySQL服务器-连接层】认证&#xff0c;授权&#xff0c;连接池 2、【MySQL服务器-服务层】 {SQL接口&#xff08;DML、DDL、存储过程、触发器&#xff09;、解析器、查询优化器、缓存} 3、【MySQL…...

九芯电子丨语音智能风扇,助您畅享智慧生活

回忆童年时期的传统机械风扇&#xff0c;那“古老”的扇叶连摆动看起来是那么吃力。在一个闷热的夏夜&#xff0c;风扇的噪音往往令人印象深刻。但在今天&#xff0c;静音家用风扇已取代了传统的机械风扇。与此同时&#xff0c;随着智能化的发展&#xff0c;智能家居已逐渐成为…...

2101. 引爆最多的炸弹;752. 打开转盘锁;1234. 替换子串得到平衡字符串

2101. 引爆最多的炸弹 核心思想&#xff1a;枚举BFS。枚举每个炸弹最多引爆多少个炸弹&#xff0c;对每个炸弹进行dfs&#xff0c;一个炸弹能否引爆另一个炸弹是两个炸弹的圆心距离在第一个炸弹的半径之内。 752. 打开转盘锁 核心思想:典型BFS&#xff0c;就像水源扩散一样&a…...

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著

​校园学习《乡村振兴战略下传统村落文化旅游设计》许少辉八一新著...

UOS服务器操作系统搭建离线yum仓库

UOS服务器操作系统搭建离线yum仓库 1050e版本操作系统&#xff08;适用ARM64和AMD64&#xff09;1、挂载everything镜像并同步2、配置本地仓库3、配置nginx发布离线源 1050e版本操作系统&#xff08;适用ARM64和AMD64&#xff09; 首先需要有everything镜像文件 服务端操作流…...

C# 实现数独游戏

1.数独单元 public struct SudokuCell{public SudokuCell() : this(0, 0, 0){}public SudokuCell(int x, int y, int number){X x; Y y; Number number;}public int X { get; set; }public int Y { get; set; }public int Number { get; set; }} 2.数独创建 public class …...

vscode + conda+ ffmpeg + numpy 的安装方式

Windows 搭建 环境 遇到的错误点&#xff1a; 解决&#xff0c;使用conda init conda activate myenv usage: conda-script.py [-h] [–no-plugins] [-V] COMMAND … conda-script.py: error: argument COMMAND: invalid choice: ‘activate’ (choose from ‘clean’, ‘comp…...

Python Union联合类型注解

视频版教程 Python3零基础7天入门实战视频教程 我们看下如下的示例&#xff1a; my_list2: list[int] [1, 2, 3, 4] my_dict2: dict[str, float] {"python222": 3.14, "java1234": 4.35} l1 [1, "python222", True] # 如何注解多种元素类型…...

提高接口自动化测试效率:使用 JMESPath 实现断言和数据提取!

前言 做接口自动化&#xff0c;断言是比不可少的。如何快速巧妙的提取断言数据就成了关键&#xff0c;当然也可以提高用例的编写效率。笔者在工作中接触到了JMESPath&#xff0c;那到底该如何使用呢&#xff1f;带着疑惑一起往下看。 JMESPath是啥&#xff1f; JMESPath 是一…...

【Linux操作系统教程】用户管理与权限管理你真的懂了吗(三)

&#x1f604;作者简介&#xff1a; 小曾同学.com,一个致力于测试开发的博主⛽️&#xff0c;主要职责&#xff1a;测试开发、CI/CD 如果文章知识点有错误的地方&#xff0c;还请大家指正&#xff0c;让我们一起学习&#xff0c;一起进步。&#x1f60a; 座右铭&#xff1a;不想…...

华为全联接大会2023 | 尚宇亮:携手启动O3社区发布

2023年9月20日&#xff0c;在华为全联接大会2023上&#xff0c;华为正式发布“联接全球服务工程师&#xff0c;聚合用户服务经验”的知识经验平台&#xff0c;以“Online 在线、Open 开放、Orchestration 协同”为理念&#xff0c;由华为、伙伴和客户携手&#xff0c;共同构建知…...

MySQL数据库查缺补漏——基础篇

MySQL数据库查缺补漏-基础篇 基础篇 net start mysql80[服务名] net stop mysql80 create database pshdhx default charset utf8mb4; 为什么不使用utf8&#xff1f;因为其字符占用三个字节&#xff0c;有四个字节的字符&#xff0c;所有需要设置为utf8mb4; 数值类型&…...

ESP8266 WiFi物联网智能插座—电能计量

目录 1、芯片功能 2、性能指标 3、寄存器说明 4、UART通信协议 4.1、写操作帧格式和时序 4.2、读操作帧格式和时序 4.3、读取全电参数数据包 4.4、配置波特率 4.5、UART保护机制 5、功能说明 5.1、电流电压瞬态波形计量 5.2、有功功率 5.3、有功功率防潜动 5.4、电能计量 5.5、…...

“智慧”北京,人工智能引领“新风尚”

原创 | 文 BFT机器人 北京时间&#xff0c;9月15日&#xff0c;北京人工智能产业峰会暨中关村科学城科创大赛颁奖典礼在北京中关村举行&#xff0c;同时惠阳还举行了“中关村人工智能大模型产业集聚区”启动建设的揭牌仪式。 此次大会围绕北京AI产业的建设与发展&#xff0c;各…...

wordpress 更新url/网站快速排名

1、PNP逻辑数据库。 LOOP获取信息类型数据。 TABLES: PERNR . INFOTYPES: 0000, 0001 .START-OF-SELECTION.GET PERNR .LOOP AT P0000 WHERE ......ENDLOOP.LOOP AT P0001 WHERE ......ENDLOOP. 宏获取信息类型数据。 TABLES: PERNR, T001P. INFOTYPES: 0000, 0001 .GET PERNR …...

做h5最好的网站/企业seo顾问服务阿亮

我正在使用PHP的“simplexml_load_file”从Flickr获取一些数据.我的目标是获取照片网址.我能够得到以下值(分配给PHP变量)&#xff1a;codewrecker posted a photo:我怎样才能提取它的这一部分&#xff1f;http://farm3.static.flickr.com/2298/2302759205_4fb109f367_m.jpg万一…...

湖南seo网站策划/网络推广方案模板

vs2017有独立的python环境&#xff1b;所以想在vs2017开发python并使用第三方包&#xff0c;需要在vs2017中操作&#xff0c;完成第三方包的安装。 一&#xff0c;查看vs2017有哪些版本的python&#xff0c;当前使用的是哪个版本 工具--》Python--》Python环境 可以看到vs2017包…...

哈尔滨网站开发需要多少钱/最近的国际新闻热点

前言 本文是在笔者做深度学习相关研究的时候需要高算力GPU去运行代码因而选择去租借GPU云服务器&#xff0c;这里记下自己所接触的一些GPU服务器网站和使用技巧 一、推荐站点 1、极链AI云&#xff0c;首当其冲的一定是这家&#xff0c;这家是我用过认为较为好用而且相对价格…...

电子网站有哪些/网络营销推广公司名称

前言Redis提供了5种数据类型&#xff1a;String(字符串)、Hash(哈希)、List(列表)、Set(集合)、Zset(有序集合)&#xff0c;理解每种数据类型的特点对于redis的开发和运维非常重要。Redis中的list是我们经常使用到的一种数据类型&#xff0c;根据使用方式的不同&#xff0c;可以…...

郑州做的比较好网站公司吗/搜索引擎免费登录入口

【100个】计算机理论英文参考文献供您参考,希望能解决毕业生们的计算机理论论文参考文献哪里找相关问题,整理好参考文献那就开始写计算机理论论文吧&#xff01;一、计算机理论论文参考文献范文[1]抑制OFDM信号峰均比的PTS算法分析与优化.胡茂凯.陈西宏.刘强&#xff0c;2011陕…...