如何通过企业网站源码实现变现?
摘要:网站平台建设方案,企业网站源码变现方法,个人主页网站制作教程,中秋节网页设计实训报告文章目录 什么是网关?搭建实验项目demo-servicegateway-service尝试简单上手 路由&#
网站平台建设方案,企业网站源码变现方法,个人主页网站制作教程,中秋节网页设计实训报告文章目录 什么是网关#xff1f;搭建实验项目demo-servicegateway-service尝试简单上手 路由#xff08;Route#xff09;断言#xff08;Predicate#xff09;和断言工厂#xff08;Predicate Factory#xff09;gateway自带的断言工厂After#xff08;请求必须在某个… 文章目录 什么是网关搭建实验项目demo-servicegateway-service尝试简单上手 路由Route断言Predicate和断言工厂Predicate Factorygateway自带的断言工厂After请求必须在某个时间点之后Before请求必须在某个时间点之前Between请求必须在某两个时间点之间Cookie请求必须包含某些cookieHeader请求必须包含某些headerHost请求的主机名或IP地址必须符合规则Method请求方式必须是指定的方式Path请求路径必须符合指定规则Query请求参数必须包含指定参数 RemoteAddr请求者的IP必须符合规则Weight权重XForwarded Remote Addr请求者的IP必须符合规则考虑代理服务器影响 疑问当一个路由下有多个断言是只需要满足其中任意一个还是必须都满足当有多个路由都匹配的情况会选择哪一个路由 自定义断言工厂 过滤器Filter局部过滤器Gateway FilterAddRequestHeaderAddRequestParameterAddResponseHeader自定义局部过滤器 全局过滤器Global Filter自定义全局过滤器 跨域问题解决与nacos集成负载均衡自定义负载均衡 请求过程源码解析 什么是网关
当我们的微服务越来越多外部需要访问为了安全我们需要做身份认证认证通过才能访问我们的服务而当请求过来我们需要根据请求的不同路由到不同的服务中而一个服务会有多个实例请求过来还需要做负载均衡还有当请求过多的时候我们需要做限流以上这些都需要我们的网关来实现。目前网关有 Zuul 和 Gateway因为 Zuul 是阻塞式编程而 Gateway 是基于 Spring webFlux 的响应式编程所以 Gateway 的吞吐会更好下面我们就来学习 Gateway
搭建实验项目
为了学习 Gateway我们先搭建两个项目一个是业务服务项目demo-service一个是 Gateway 服务项目gateway-service初始配置如下
demo-service
pom.xml 配置如下
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.7.13/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.victor.demo/groupIdartifactIddemo-service/artifactIdversion0.0.1-SNAPSHOT/versionname样例服务/namedescription样例服务/descriptionpropertiesjava.version17/java.version/propertiesdependencies!-- Spring Boot --dependencygroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-web/artifactId/dependency/dependencies/projectapplication.yml 配置如下
server:#端口号改成8081port: 8081spring:application:name: demogateway-service
pom.xml 配置如下
?xml version1.0 encodingUTF-8?
project xmlnshttp://maven.apache.org/POM/4.0.0 xmlns:xsihttp://www.w3.org/2001/XMLSchema-instancexsi:schemaLocationhttp://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsdmodelVersion4.0.0/modelVersionparentgroupIdorg.springframework.boot/groupIdartifactIdspring-boot-starter-parent/artifactIdversion2.7.13/versionrelativePath/ !-- lookup parent from repository --/parentgroupIdcom.victor.gateway/groupIdartifactIdgateway-service/artifactIdversion0.0.1-SNAPSHOT/versionnamegateway网关/namedescriptiongateway网关/descriptionpropertiesjava.version17/java.version/propertiesdependencies!-- Gateway 网关 --dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-gateway/artifactIdversion3.1.8/version/dependency/dependencies/projectapplication.yml 配置如下
spring:application:name: gatewaygateway没有修改端口号默认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效果如下 下面我们加入 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:- StripPrefix1在浏览器中输入 http://127.0.0.1:8080/demo-service/hello效果如下 可以达到一样的效果成功gateway 里的配置起作用了首先我们的请求匹配的上 /demo-service/** 然后StripPrefix1 会跳过开头的 /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请求必须在某个时间点之后- After2023-09-15T16:00:00.00008:00[Asia/Shanghai]Before请求必须在某个时间点之前- Before2023-09-15T17:00:00.00008:00[Asia/Shanghai]Between请求必须在某两个时间点之间- Between2023-09-15T16:00:00.00008:00[Asia/Shanghai], 2023-09-15T17:00:00.00008:00[Asia/Shanghai]Cookie请求必须包含某些cookie- Cookiechocolate, ch.pHeader请求必须包含某些header- HeaderX-Request-Id, \dHost请求的主机名或IP地址必须符合规则- Host127.0.0.1:8080Method请求方式必须是指定的方式- MethodPOSTPath请求路径必须符合指定规则- Path/demo-service/helloQuery请求参数必须包含指定参数- QuerygreenRemoteAddr请求者的ip必须符合规则- RemoteAddr127.0.0.1Weight权重- Weightgroup, 5XForwardedRemoteAddr请求者的ip必须符合规则考虑代理服务器影响- XForwardedRemoteAddr192.168.33.33
gateway自带的断言工厂
After请求必须在某个时间点之后
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- After2023-09-15T16:00:00.00008:00[Asia/Shanghai]表示在 2023-09-15T16:00:00.00008:00[Asia/Shanghai] 这个时间之后的请求才会被匹配到这个路由
现在时间是 2023-09-15 16:05我们用postman试下 可以的我们改下配置的时间为16号2023-09-16T16:00:00.00008:00[Asia/Shanghai] 可以看到报404
Before请求必须在某个时间点之前
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- Before2023-09-15T17:00:00.00008:00[Asia/Shanghai]表示在 2023-09-15T17:00:00.00008:00[Asia/Shanghai] 这个时间之前的请求才会被匹配到这个路由
和 After 类似就不贴例子了可以自己试试
Between请求必须在某两个时间点之间
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- Between2023-09-15T16:00:00.00008:00[Asia/Shanghai], 2023-09-15T17:00:00.00008:00[Asia/Shanghai]表示在 2023-09-15T16:00:00.00008:00[Asia/Shanghai] 和 2023-09-15T17:00:00.00008:00[Asia/Shanghai] 两个时间点之间个路由
和 After 、Before类似就不贴例子了可以自己试试
Cookie请求必须包含某些cookie
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- Cookiechocolate, ch.p表示 cookie 中包含 chocolate 参数并且值是 ch.p 的请求才会被匹配到这个路由
我们先不添加 cookie 试一下 不出所料404然后我们添加一下cookie 成功了
也可以配置多个像这样
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- Cookiechocolate, ch.p- Cookieabc, 123这样必须包含两个 cookie 才行
Header请求必须包含某些header
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- HeaderX-Request-Id, \d表示请求头里有一个 key 是 X-Request-Id值需要匹配到正则表达式 [\d] 才会匹配到这个路由
我们先不添加头信息 没错404
然后再加上头信息 成功
这里也可以这么写 - HeaderX-Request-Id表示只要有这个参数就可以不在乎值是多少
当然也可以像 cookie 那样配置多个
Host请求的主机名或IP地址必须符合规则
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- Host127.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:- MethodPOST如果是 POST 请求才会被匹配到这个路由
这里我的接口是 GET就会报404如果要既支持GET又支持POST就用逗号分割像这样- MethodGET,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:- Querycolor, red请求中必须包含参数 color 并且值是 red 才会被匹配到这个路由 这里也可以这么写 - Querycolor表示只要有这个参数就可以不在乎值是多少和 Header 相似也可以添加多个
RemoteAddr请求者的IP必须符合规则
修改 application.yml 配置文件
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- RemoteAddr127.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:- Weightgroup, 8filters:- AddRequestParametercolor, red- id: demo-service2uri: http://127.0.0.1:8081predicates:- Weightgroup, 2filters:- AddRequestParametercolor, blue接收两个参数一个 group一个 weight80%的请求会被发送至 demo-service20%的请求会被发送至demo-service2
配置文件里增加 filters 是为了方便验证我们再 给 demo-service 增加个 controller
RequestMapping(/color)
public String hello(RequestParam(color) String color) {return this color is color;
}好来试下 会发现有时是 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:- XForwardedRemoteAddr192.168.33.33请求者的 IP 必须是 192.168.33.33 才会被匹配到这个路由
和 RemoteAddr 不一样的是因为 gateway 前面可能会有代理服务器使用 RemoteAddr 去匹配的是代理服务器的 IP所以就有了这个断言这个断言会通过从头信息中的 X-Forwarded-For 参数去获取客户端原始 IP 地址然后去匹配。
来试试 果然可以
疑问
当一个路由下有多个断言是只需要满足其中任意一个还是必须都满足
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- After2023-09-15T16:00:00.00008:00[Asia/Shanghai]- Cookiechocolate, ch.p结论当一个路由下有多个断言时所有条件都必须满足才能匹配此路由
当有多个路由都匹配的情况会选择哪一个路由
首先我们配置两个路由条件一致
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- MethodGET,POSTfilters:- AddRequestParametercolor, red- id: demo-service2uri: http://127.0.0.1:8081predicates:- MethodGET,POSTfilters:- AddRequestParametercolor, blue添加参数是为了好区分还是使用color接口
RequestMapping(/color)
public String hello(RequestParam(color) String color) {return this color is color;
}结果如下 始终走的第一个路由 demo-service
我们改动一下把 demo-service2 的 order 改成 -1试下
spring:application:name: gatewaycloud:gateway:routes:- id: demo-serviceuri: http://127.0.0.1:8081predicates:- MethodGET,POSTfilters:- AddRequestParametercolor, red- id: demo-service2uri: http://127.0.0.1:8081order: -1predicates:- MethodGET,POSTfilters:- AddRequestParametercolor, blue发现走第二个路由 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 AbstractRoutePredicateFactoryDayOfWeekRoutePredicateFactory.Config {public static final String DAY_OF_WEEK_KEY dayOfWeek;public DayOfWeekRoutePredicateFactory() {super(Config.class);}Overridepublic ListString shortcutFieldOrder() {return Arrays.asList(DAY_OF_WEEK_KEY);}Overridepublic PredicateServerWebExchange 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:- DayOfWeekMONDAY,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:- AddRequestHeaderX-Request-red, blue所有匹配到这个路由的请求的请求头都添加 名字是 Header X-Request-red值是 blue
AddRequestParameter
spring:cloud:gateway:routes:- id: add_request_parameter_routeuri: https://example.orgfilters:- AddRequestParameterred, blue所有匹配到这个路由的请求都添加一个参数 名字是 red值是 blue
AddResponseHeader
spring:cloud:gateway:routes:- id: add_response_header_routeuri: https://example.orgfilters:- AddResponseHeaderX-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:- After2023-09-15T16:00:00.00008: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请求路由到目标 URLGatewayMetricsFilter收集和记录网关的度量指标
但是关于有些强业务的过滤器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 MonoVoid 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 中获取上下文需要的各类信息如 request、response 等等然后从 request、response 里又可以获取 header、cookie、uripath 等信息过滤器也是需要分先后执行的这时候就可以实现 Ordered 接口重写 getOrder 方法返回一个 int 值这个值越小优先级就越高。也可以通过添加 Order 注解实现
不知道怎么用的可以参考上面 Gateway 自带的那些过滤器。
跨域问题解决
在配置文件里如下配置
spring:cloud:gateway:globalcors:add-to-simple-url-handler-mapping: true #解决options请求被拦截问题cors-configurations:[/**]:allowedOrigins: #允许哪些网站的跨域请求- https://docs.spring.ioallowedMethods: #允许的跨域ajax请求方式- GET- POSTallowedHeaders: * #允许请求头里携带的信息allowCredentials: true #是否允许携带cookiemaxAge: 360000 #这次跨域检测有效期与nacos集成
nacos 是既可以做注册中心又可以做配置中心的这边只讲做注册中心有机会单独写篇文章讲配置中心
首先本地起一个 nacos默认端口8848这个教程网上很多这里就不讲了
第一步引入依赖
给 gateway-service、demo-service 分别引入 nacos 的依赖注意还需要引入 loadbalancer 依赖还有就是 nacos 的配置我们一般放在 bootstrap.yml 文件里因为 bootstrap.yml 加载顺序早于 application.yml用 nacos 做配置中心时需要先加载 nacos 的配置所以还需要引入 bootstrap 依赖如下
!-- 负载均衡 --
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-loadbalancer/artifactIdversion3.1.7/version
/dependency!-- Nacos 服务发现依赖 --
dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-discovery/artifactIdversion2021.0.5.0/version
/dependency!-- Nacos 配置中心依赖 --
!--dependencygroupIdcom.alibaba.cloud/groupIdartifactIdspring-cloud-starter-alibaba-nacos-config/artifactIdversion2021.0.5.0/version
/dependency--!-- bootstrap --
dependencygroupIdorg.springframework.cloud/groupIdartifactIdspring-cloud-starter-bootstrap/artifactIdversion3.1.7/version
/dependency用作配置中心的时候还需要引入 spring-cloud-starter-alibaba-nacos-config这里先注释掉了
第二步添加 EnableDiscoveryClient 注解
一般我们把这个注解写在启动类上在 gateway-service、demo-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-service、demo-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:- StripPrefix1第四步启动
因为我们配了 nacos 的 namespace 是 dev所以先要在 nacos 的页面配置这个命名空间注意 ID 写 dev然后启动 nacosgateway-servicedemo-service打开 nacos 的页面 http://localhost:8848/nacos 在服务列表里选择 dev 命名空间如何没配的话会在 public 下可以看到我们注册进来的两个服务
再试着用 postman 调用下接口是可以的 负载均衡
Gateway 路由配置里 uri 配置了 lb:// 开头就会走负载均衡的逻辑具体代码在 ReactiveLoadBalancerClientFilter 的 filter 方法里ReactiveLoadBalancerClientFilter 是个全局过滤器上面讲过了 重点看choose方法 最终会走到 ReactorLoadBalancer 接口的 choose 方法 ReactorLoadBalancer 接口有3个实现默认走的是 RoundRobinLoadBalancer 负载均衡的代码就在 getInstanceResponse 方法里
如果我们要切换成 NacosLoadBalancer 怎么操作呢只需要在配置类上加上这么一句 LoadBalancerClients(defaultConfiguration NacosLoadBalancerClientConfiguration.class) 自定义负载均衡
如果我们想自己实现一个负载均衡逻辑怎么操作呢比如我们要写一个灰度负载均衡根据请求头信息里的版本号去匹配对应的服务怎么做
第一步给 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 ObjectProviderServiceInstanceListSupplier 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(ObjectProviderServiceInstanceListSupplier serviceInstanceListSupplierProvider,String serviceId, NacosDiscoveryProperties nacosDiscoveryProperties) {this.serviceId serviceId;this.serviceInstanceListSupplierProvider serviceInstanceListSupplierProvider;this.nacosDiscoveryProperties nacosDiscoveryProperties;}SuppressWarnings(rawtypes)Overridepublic MonoResponseServiceInstance choose(Request request) {ServiceInstanceListSupplier supplier serviceInstanceListSupplierProvider.getIfAvailable(NoopServiceInstanceListSupplier::new);return supplier.get(request).next().map(serviceInstances - processInstanceResponse(supplier, serviceInstances, request));}private ResponseServiceInstance processInstanceResponse(ServiceInstanceListSupplier supplier, ListServiceInstance serviceInstances, Request request) {ResponseServiceInstance serviceInstanceResponse getInstanceResponse(serviceInstances, request);if (supplier instanceof SelectedInstanceCallback serviceInstanceResponse.hasServer()) {((SelectedInstanceCallback) supplier).selectedServiceInstance(serviceInstanceResponse.getServer());}return serviceInstanceResponse;}private ResponseServiceInstance getInstanceResponse(ListServiceInstance 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)) {ListServiceInstance 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 {BeanConditionalOnMissingBeanpublic ReactorLoadBalancerServiceInstance 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 处理请求的入口方法是 DispatcherHandler 的 handle 方法我们从这里开始 看到第一个行的 handlerMappings我就愣住了这个东西有点眼熟啊看看在哪里赋值的 这个方法名也眼熟handlerAdapter 也眼熟在 SpringMVC 的源码里有见过原来 Spring webFlux 和 SpringMVC 在设计上有一些相似之处
框架Spring webFluxSpringMVC分发DispatcherHandlerDispatcherServlet映射HandlerMappingHandlerMapping适配HandlerAdapterHandlerAdapter处理WebHandlerHandler
关于 SpringMVC 请求处理流程的源码可以看我的另一篇文章SpringMVC源码学习笔记之请求处理流程
说回 Gateway 我们继续看 DispatcherHandler 的 handle 方法
//DispatcherHandler
Override
public MonoVoid 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 MonoObject 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 的子类 RoutePredicateHandlerMapping 的 getHandlerInternal 方法
//RoutePredicateHandlerMapping
Override
protected Mono? getHandlerInternal(ServerWebExchange exchange) {// dont 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((FunctionRoute, 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 MonoRoute 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.Routeempty().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
继续看下面我这边再贴一次 DispatcherHandler 的 handle 方法 继续看 invokeHandler 方法
//DispatcherHandler
private MonoHandlerResult 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 来找到匹配 webHandler 的 HandlerAdapter 可以看到是 SimpleHandlerAdapter 我们跟进去看
Override
public MonoHandlerResult handle(ServerWebExchange exchange, Object handler) {WebHandler webHandler (WebHandler) handler;MonoVoid mono webHandler.handle(exchange);return mono.then(Mono.empty());
}上面我们知道返回的 webHandler 类型是 FilteringWebHandler所以这里我们继续进到 FilteringWebHandler 的 handle 方法
Override
public MonoVoid handle(ServerWebExchange exchange) {//从上下文中获取匹配的路由Route route exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR);//获取路由中的局部过滤器ListGatewayFilter gatewayFilters route.getFilters();//全局过滤器ListGatewayFilter 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 接口为什么可以放入到 ListGatewayFilter combined 集合里于是看了下 this.globalFilters 是怎么赋值的
private final ListGatewayFilter globalFilters;public FilteringWebHandler(ListGlobalFilter globalFilters) {this.globalFilters loadFilters(globalFilters);
}private static ListGatewayFilter loadFilters(ListGlobalFilter 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 MonoVoid 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
