commit
c855763625
@ -1,35 +1,38 @@ |
|||||||
* [首页](/?t=1574408534264) |
* [首页](/?t=1579512230184) |
||||||
* 开发文档 |
* 开发文档 |
||||||
* [快速体验](files/10010_快速体验.md?t=1574408534265) |
* [快速体验](files/10010_快速体验.md?t=1579512230186) |
||||||
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1574408534281) |
* [项目接入到SOP](files/10011_项目接入到SOP.md?t=1579512230207) |
||||||
* [新增接口](files/10020_新增接口.md?t=1574408534281) |
* [新增接口](files/10020_新增接口.md?t=1579512230208) |
||||||
* [开发流程](files/10021_开发流程.md?t=1574408534281) |
* [开发流程](files/10021_开发流程.md?t=1579512230208) |
||||||
* [业务参数校验](files/10030_业务参数校验.md?t=1574408534281) |
* [业务参数校验](files/10030_业务参数校验.md?t=1579512230208) |
||||||
* [错误处理](files/10040_错误处理.md?t=1574408534281) |
* [错误处理](files/10040_错误处理.md?t=1579512230208) |
||||||
* [编写文档](files/10041_编写文档.md?t=1574408534282) |
* [编写文档](files/10041_编写文档.md?t=1579512230208) |
||||||
* [接口交互详解](files/10050_接口交互详解.md?t=1574408534282) |
* [接口交互详解](files/10050_接口交互详解.md?t=1579512230208) |
||||||
* [easyopen支持](files/10070_easyopen支持.md?t=1574408534282) |
* [easyopen支持](files/10070_easyopen支持.md?t=1579512230208) |
||||||
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1574408534282) |
* [使用签名校验工具](files/10080_使用签名校验工具.md?t=1579512230209) |
||||||
* [ISV管理](files/10085_ISV管理.md?t=1574408534282) |
* [ISV管理](files/10085_ISV管理.md?t=1579512230209) |
||||||
* [自定义返回结果](files/10087_自定义返回结果.md?t=1574408534282) |
* [自定义返回结果](files/10087_自定义返回结果.md?t=1579512230209) |
||||||
* [自定义过滤器](files/10088_自定义过滤器.md?t=1574408534282) |
* [自定义过滤器](files/10088_自定义过滤器.md?t=1579512230209) |
||||||
* [自定义校验token](files/10089_自定义校验token.md?t=1574408534282) |
* [自定义校验token](files/10089_自定义校验token.md?t=1579512230209) |
||||||
* [路由授权](files/10090_路由授权.md?t=1574408534282) |
* [路由授权](files/10090_路由授权.md?t=1579512230209) |
||||||
* [接口限流](files/10092_接口限流.md?t=1574408534282) |
* [接口限流](files/10092_接口限流.md?t=1579512230209) |
||||||
* [监控日志](files/10093_监控日志.md?t=1574408534282) |
* [监控日志](files/10093_监控日志.md?t=1579512230209) |
||||||
* [SDK开发](files/10095_SDK开发.md?t=1574408534283) |
* [SDK开发](files/10095_SDK开发.md?t=1579512230210) |
||||||
* [使用SpringCloudGateway](files/10096_使用SpringCloudGateway.md?t=1574408534283) |
* [使用SpringCloudGateway](files/10096_使用SpringCloudGateway.md?t=1579512230210) |
||||||
* [应用授权](files/10097_应用授权.md?t=1574408534283) |
* [应用授权](files/10097_应用授权.md?t=1579512230210) |
||||||
* [提供restful接口](files/10100_提供restful接口.md?t=1574408534283) |
* [提供restful接口](files/10100_提供restful接口.md?t=1579512230210) |
||||||
* [文件上传](files/10104_文件上传.md?t=1574408534283) |
* [文件上传](files/10104_文件上传.md?t=1579512230210) |
||||||
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1574408534283) |
* [配置Sleuth链路追踪](files/10109_配置Sleuth链路追踪.md?t=1579512230210) |
||||||
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1574408534283) |
* [预发布灰度发布](files/10110_预发布灰度发布.md?t=1579512230211) |
||||||
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1574408534283) |
* [动态修改请求参数](files/10111_动态修改请求参数.md?t=1579512230211) |
||||||
* [使用eureka](files/10112_使用eureka.md?t=1574408534283) |
* [使用eureka](files/10112_使用eureka.md?t=1579512230211) |
||||||
* [扩展其它注册中心](files/10113_扩展其它注册中心.md?t=1574408534283) |
* [扩展其它注册中心](files/10113_扩展其它注册中心.md?t=1579512230211) |
||||||
* 原理分析 |
* 原理分析 |
||||||
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1574408534283) |
* [原理分析之@ApiMapping](files/90010_原理分析之@ApiMapping.md?t=1579512230211) |
||||||
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1574408534283) |
* [原理分析之如何存储路由](files/90011_原理分析之如何存储路由.md?t=1579512230211) |
||||||
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1574408534283) |
* [原理分析之如何路由](files/90012_原理分析之如何路由.md?t=1579512230211) |
||||||
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1574408534284) |
* [原理分析之文档归纳](files/90013_原理分析之文档归纳.md?t=1579512230211) |
||||||
* [常见问题](files/90100_常见问题.md?t=1574408534284) |
* [原理分析之预发布灰度发布](files/90014_原理分析之预发布灰度发布.md?t=1579512230211) |
||||||
|
* [2.x升3.x注意事项](files/90099_2.x升3.x注意事项.md?t=1579512230212) |
||||||
|
* [常见问题](files/90100_常见问题.md?t=1579512230212) |
||||||
|
* [网关性能测试](files/90999_网关性能测试.md?t=1579512230212) |
||||||
|
@ -1,92 +1,16 @@ |
|||||||
# 使用SpringCloudGateway |
# 使用SpringCloudGateway |
||||||
|
|
||||||
SOP默认网关是使用Spring Cloud Zuul,您也可以切换成Spring Cloud Gateway,完整代码见`spring-cloud-gateway`分支。 |
修改`sop-gateway/pom.xml`配置,artifactId部分改成`sop-bridge-gateway`即可 |
||||||
|
|
||||||
如果您的系统并发量不大,建议使用zuul,因为zuul的功能更全面,有新功能会优先实现在zuul上。 |
|
||||||
|
|
||||||
- SOP中 Spring Cloud Zuul 和 Spring Cloud Gateway功能对比 |
|
||||||
|
|
||||||
| 功能 | Spring Cloud Zuul | Spring Cloud Gateway | |
|
||||||
| ----- | ---- | ----------------------- | |
|
||||||
| 签名验证|√ | √ | |
|
||||||
| 统一异常处理|√ | √ | |
|
||||||
| 统一返回内容|√ | √ | |
|
||||||
| session管理|√ | √ | |
|
||||||
| 秘钥管理|√ | √ | |
|
||||||
| 微服务端自动验证(JSR-303)|√ | √ | |
|
||||||
| 文件上传|√ | √ | |
|
||||||
| 文件下载|√ | √ | |
|
||||||
| 接口限流|√ | √ | |
|
||||||
| 文档整合|√ | √ | |
|
||||||
| 应用授权|√ | √ | |
|
||||||
| 监控日志|√ | √ | |
|
||||||
| 支持nacos|√ | √ | |
|
||||||
| 网关动态修改参数|√ | √ | |
|
||||||
|
|
||||||
使用Spring Cloud Gateway步骤如下: |
|
||||||
|
|
||||||
- 打开sop-gateway/pom.xml,注释spring cloud zuul依赖,打开Spring Cloud Gateway依赖 |
|
||||||
|
|
||||||
```xml |
```xml |
||||||
<!-- ↓↓↓ 使用spring cloud zuul ↓↓↓ |
|
||||||
<dependency> |
|
||||||
<groupId>org.springframework.cloud</groupId> |
|
||||||
<artifactId>spring-cloud-starter-netflix-zuul</artifactId> |
|
||||||
</dependency> |
|
||||||
--> |
|
||||||
<!-- ↑↑↑ 使用spring cloud zuul ↑↑↑ --> |
|
||||||
|
|
||||||
|
|
||||||
<!-- ↓↓↓ 使用spring cloud gateway,处于beta阶段,推荐使用zuul ↓↓↓ --> |
|
||||||
<dependency> |
|
||||||
<groupId>org.springframework.cloud</groupId> |
|
||||||
<artifactId>spring-cloud-starter-gateway</artifactId> |
|
||||||
</dependency> |
|
||||||
<dependency> |
<dependency> |
||||||
<groupId>org.springframework.boot</groupId> |
<groupId>com.gitee.sop</groupId> |
||||||
<artifactId>spring-boot-starter-webflux</artifactId> |
<!-- 使用zuul作为网关 --> |
||||||
|
<!--<artifactId>sop-bridge-zuul</artifactId>--> |
||||||
|
<!-- 使用spring cloud gateway作为网关 --> |
||||||
|
<artifactId>sop-bridge-gateway</artifactId> |
||||||
|
<version>对应版本</version> |
||||||
</dependency> |
</dependency> |
||||||
<dependency> |
|
||||||
<groupId>org.springframework.boot</groupId> |
|
||||||
<artifactId>spring-boot-starter-actuator</artifactId> |
|
||||||
</dependency> |
|
||||||
|
|
||||||
<!-- ↑↑↑ 使用spring cloud gateway ↑↑↑ --> |
|
||||||
``` |
|
||||||
|
|
||||||
- 打开启动类`SopGatewayApplication.java`, 注释zuul相关注解 |
|
||||||
|
|
||||||
```java |
|
||||||
package com.gitee.sop.gateway; |
|
||||||
|
|
||||||
import org.springframework.boot.SpringApplication; |
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication; |
|
||||||
//import org.springframework.cloud.netflix.zuul.EnableZuulProxy; |
|
||||||
|
|
||||||
// 开启网关功能 |
|
||||||
//@EnableZuulProxy |
|
||||||
@SpringBootApplication |
|
||||||
public class SopGatewayApplication { |
|
||||||
|
|
||||||
public static void main(String[] args) { |
|
||||||
SpringApplication.run(SopGatewayApplication.class, args); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
``` |
|
||||||
|
|
||||||
- 禁用ZuulConfig类,注释掉@Configuration注解即可 |
|
||||||
|
|
||||||
```java |
|
||||||
//@Configuration |
|
||||||
public class ZuulConfig extends AlipayZuulConfiguration {...} |
|
||||||
``` |
|
||||||
|
|
||||||
- 启用GatewayConfig类,打开@Configuration注释 |
|
||||||
|
|
||||||
```java |
|
||||||
@Configuration |
|
||||||
public class GatewayConfig extends AlipayGatewayConfiguration {...} |
|
||||||
``` |
``` |
||||||
|
|
||||||
修改完毕,重启sop-gateway |
修改完毕,重启sop-gateway |
||||||
|
@ -0,0 +1,92 @@ |
|||||||
|
# 原理分析之预发布灰度发布 |
||||||
|
|
||||||
|
SOP网关采用`自定义负载均衡策略`来实现对预发布/灰度发布服务器实例的选择。 |
||||||
|
|
||||||
|
spring cloud gateway默认的负载均衡实现类在:`org.springframework.cloud.gateway.filter.LoadBalancerClientFilter.java`中 |
||||||
|
|
||||||
|
这个类主要做了几件事情: |
||||||
|
|
||||||
|
1. 解析出请求路径中的scheme |
||||||
|
2. 如果scheme不是以`lb`协议开头直接跳过 |
||||||
|
3. 如果scheme以`lb`协议开头,则说明需要进行负载均衡,选出一台微服务实例 |
||||||
|
4. 将`lb`协议解析成`http://ip:port`,继续向下请求 |
||||||
|
|
||||||
|
其中第4步是由`org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient`来完成的, |
||||||
|
我们只要分别继承`LoadBalancerClientFilter`和`RibbonLoadBalancerClient`,然后重写其中的方法就能完成自定义负载均衡。 |
||||||
|
|
||||||
|
SOP中的重写类是:`SopLoadBalancerClientFilter`和`SopLoadBalancerClient`,核心代码委托给了`LoadBalanceServerChooser`处理,核心代码如下: |
||||||
|
|
||||||
|
```java |
||||||
|
/** |
||||||
|
* 选择服务器 |
||||||
|
* |
||||||
|
* @param serviceId serviceId,仅gateway网关有作用 |
||||||
|
* @param exchange 请求上下文 |
||||||
|
* @param loadBalancer loadBalancer |
||||||
|
* @param superChooser 父类默认的选择 |
||||||
|
* @param serverChooserFunction 执行选择操作 |
||||||
|
* @return 返回服务器实例,没有选到则返回null |
||||||
|
*/ |
||||||
|
public R choose( |
||||||
|
String serviceId |
||||||
|
, T exchange |
||||||
|
, ILoadBalancer loadBalancer |
||||||
|
, Supplier<R> superChooser |
||||||
|
, Function<List<Server>, R> serverChooserFunction) { |
||||||
|
// 获取所有服务实例 |
||||||
|
List<Server> servers = loadBalancer.getReachableServers(); |
||||||
|
|
||||||
|
// 存放预发服务器 |
||||||
|
List<Server> preServers = new ArrayList<>(4); |
||||||
|
// 存放灰度发布服务器 |
||||||
|
List<Server> grayServers = new ArrayList<>(4); |
||||||
|
// 存放非预发服务器 |
||||||
|
List<Server> notPreServers = new ArrayList<>(4); |
||||||
|
|
||||||
|
for (Server server : servers) { |
||||||
|
// 获取实例metadata |
||||||
|
Map<String, String> metadata = getMetadata(serviceId, server); |
||||||
|
// 是否开启了预发模式 |
||||||
|
if (this.isPreServer(metadata)) { |
||||||
|
preServers.add(server); |
||||||
|
} else if (this.isGrayServer(metadata)) { |
||||||
|
grayServers.add(server); |
||||||
|
} else { |
||||||
|
notPreServers.add(server); |
||||||
|
} |
||||||
|
} |
||||||
|
notPreServers.addAll(grayServers); |
||||||
|
// 如果没有开启预发布服务和灰度发布,直接用默认的方式 |
||||||
|
if (preServers.isEmpty() && grayServers.isEmpty()) { |
||||||
|
return superChooser.get(); |
||||||
|
} |
||||||
|
// 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器 |
||||||
|
if (this.isRequestFromPreDomain(exchange)) { |
||||||
|
return serverChooserFunction.apply(preServers); |
||||||
|
} |
||||||
|
// 如果是灰度请求,则认为是灰度用户,选出灰度服务器 |
||||||
|
if (this.isRequestGrayServer(exchange)) { |
||||||
|
return serverChooserFunction.apply(grayServers); |
||||||
|
} |
||||||
|
|
||||||
|
// 到这里说明不能访问预发/灰度服务器,则需要路由到非预发服务器 |
||||||
|
// 注意:这里允许走灰度服务器,如果不允许走,注释notPreServers.addAll(grayServers);这行 |
||||||
|
return serverChooserFunction.apply(notPreServers); |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
其业务逻辑如下: |
||||||
|
|
||||||
|
1. 选出`serviceId`对应的所有服务器实例 |
||||||
|
2. 将服务器实例进行分类,分别放进`预发布List`,`灰度List`,`非预发布List`中 |
||||||
|
3. 如果`预发布List`,`灰度List`都为空,表示没有开启任何预发/灰度服务,直接使用父类的负载均衡策略 |
||||||
|
4. 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器 |
||||||
|
5. 如果是灰度请求,则认为是灰度用户,选出灰度服务器 |
||||||
|
6. 最后剩下的是正常用户,正常用户不能走预发环境 |
||||||
|
|
||||||
|
## 参考类 |
||||||
|
|
||||||
|
- com.gitee.sop.gatewaycommon.gateway.filter.SopLoadBalancerClientFilter |
||||||
|
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient |
||||||
|
- com.gitee.sop.gatewaycommon.gateway.loadbalancer.GatewayLoadBalanceServerChooser |
||||||
|
|
@ -0,0 +1,23 @@ |
|||||||
|
# 2.x升3.x注意事项 |
||||||
|
|
||||||
|
升级到3.x后`本地访问接口`方式会有不同。 |
||||||
|
|
||||||
|
```java |
||||||
|
@ApiMapping(value = "alipay.story.get") |
||||||
|
public StoryResult getStory(StoryParam param) { |
||||||
|
StoryResult story = new StoryResult(); |
||||||
|
story.setId(1L); |
||||||
|
story.setName("海底小纵队(alipay.story.get1.0), port:" + environment.getProperty("server.port") + ", param:" + param); |
||||||
|
return story; |
||||||
|
} |
||||||
|
``` |
||||||
|
|
||||||
|
- 2.x版本访问方式: |
||||||
|
|
||||||
|
`http://localhost:2222/alipay.story.get/?name=Jim&version=1.0` |
||||||
|
|
||||||
|
- 3.x版本访问方式: |
||||||
|
|
||||||
|
`http://localhost:2222/alipay.story.get/1.0/?name=Jim` |
||||||
|
|
||||||
|
3.x版本中把版本号融合在了url中,如果这个功能没有用到,可以放心升级。 |
@ -0,0 +1,116 @@ |
|||||||
|
# 网关性能测试 |
||||||
|
|
||||||
|
**测试环境** |
||||||
|
|
||||||
|
> 注意:记得关闭限流功能 |
||||||
|
|
||||||
|
- 测试工具:[wrk](https://github.com/wg/wrk),[安装教程](https://www.cnblogs.com/quanxiaoha/p/10661650.html) |
||||||
|
- 服务器:CentOS7(虚拟机,宿主机:macbookpro),内存:2G,CPU:1,核数:2核 |
||||||
|
- 运行环境:Java8、Mysql-5.7、Nacos-1.1.3 |
||||||
|
- 运行参数: |
||||||
|
|
||||||
|
``` |
||||||
|
-verbose:gc -XX:+PrintGCDetails -XX:+PrintHeapAtGC -Xloggc:gc-zuul.log \ |
||||||
|
-XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=128m -Xms1024m -Xmx1024m -Xmn512m -Xss256k -XX:SurvivorRatio=8 \ |
||||||
|
-XX:+UseConcMarkSweepGC |
||||||
|
``` |
||||||
|
|
||||||
|
- zuul配置: |
||||||
|
|
||||||
|
```properties |
||||||
|
# 不校验时间,这样一个链接可以一直进行测试 |
||||||
|
sop.api-config.timeout-seconds=0 |
||||||
|
sop.restful.enable=true |
||||||
|
|
||||||
|
logging.level.com.gitee=info |
||||||
|
|
||||||
|
# zuul调优 |
||||||
|
zuul.host.max-per-route-connections=5000 |
||||||
|
zuul.host.max-total-connections=5000 |
||||||
|
zuul.semaphore.max-semaphores=5000 |
||||||
|
|
||||||
|
ribbon.ReadTimeout=5000 |
||||||
|
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=13000 |
||||||
|
|
||||||
|
logging.file=sop-gateway.log |
||||||
|
``` |
||||||
|
|
||||||
|
以上配置仅针对zuul,Spring Cloud Gateway没有做优化配置 |
||||||
|
|
||||||
|
CentOS允许最大连接数 |
||||||
|
``` |
||||||
|
$ ulimit -n |
||||||
|
65535 |
||||||
|
``` |
||||||
|
|
||||||
|
## 调用开放接口 |
||||||
|
|
||||||
|
- wrk命令: |
||||||
|
|
||||||
|
``` |
||||||
|
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/?charset=utf-8&biz_content=%7B%22name%22%3A%22%E8%91%AB%E8%8A%A6%E5%A8%83%22%2C%22id%22%3A%221%22%7D&method=alipay.story.get&format=json&sign=RjK%2FThnzAJQOw%2BfoVLS18PgWZAR%2B25SI2XdONFhS%2BmS8vsv2jNT3rygFoh%2ByX1AJbMgIEfcBzkQyqrs29jjd5dcwHVkcf7vxXshyfcEgl0fbMF6Ihycnz7rqSqkW3lzAWx4NuWUfkPnTX8Ffuf%2BhYRaI0NCpNv%2FV300HvsvmUjS6ZzS4YHaT1peSq0agfUhwRPd97aYMnUwRZDzxNfc5wuXA7OQ1o%2FPYIuIb%2FajVfwNP5ysitc%2FKtYEqt9rNAuzkcFmsw71d2RRnrPLsDN%2BuBXnIEh482f%2FbMj2Rj4%2FMq%2B0PEtlTRbg3rYnxyfowymfX%2BNmI4gNRUt70D4a%2FL3Qiug%3D%3D&app_id=2019032617262200001&sign_type=RSA2&version=1.0×tamp=2020-01-19+13%3A34%3A12" |
||||||
|
``` |
||||||
|
|
||||||
|
- Spring Cloud Gateway测试结果 |
||||||
|
|
||||||
|
``` |
||||||
|
8 threads and 200 connections |
||||||
|
Thread Stats Avg Stdev Max +/- Stdev |
||||||
|
Latency 139.74ms 69.39ms 617.14ms 69.82% |
||||||
|
Req/Sec 182.12 55.74 343.00 66.24% |
||||||
|
43391 requests in 30.09s, 11.96MB read |
||||||
|
Requests/sec: 1441.96 |
||||||
|
Transfer/sec: 406.96KB |
||||||
|
``` |
||||||
|
|
||||||
|
- Spring Cloud Zuul测试结果 |
||||||
|
|
||||||
|
``` |
||||||
|
8 threads and 200 connections |
||||||
|
Thread Stats Avg Stdev Max +/- Stdev |
||||||
|
Latency 230.14ms 331.27ms 2.00s 86.98% |
||||||
|
Req/Sec 141.69 51.04 323.00 66.99% |
||||||
|
33945 requests in 30.09s, 9.88MB read |
||||||
|
Socket errors: connect 0, read 0, write 0, timeout 385 |
||||||
|
Requests/sec: 1128.05 |
||||||
|
Transfer/sec: 336.15KB |
||||||
|
``` |
||||||
|
|
||||||
|
## 调用restful请求 |
||||||
|
|
||||||
|
- wrk命令: |
||||||
|
|
||||||
|
``` |
||||||
|
wrk -t8 -c200 -d30s "http://10.1.31.227:8081/rest/story-service/food/getFoodById?id=2" |
||||||
|
``` |
||||||
|
|
||||||
|
线程数为 8,模拟 200 个并发请求,持续 30 秒 |
||||||
|
|
||||||
|
- Spring Cloud Gateway测试结果 |
||||||
|
|
||||||
|
``` |
||||||
|
8 threads and 200 connections |
||||||
|
Thread Stats Avg Stdev Max +/- Stdev |
||||||
|
Latency 120.14ms 58.30ms 513.85ms 67.47% |
||||||
|
Req/Sec 210.47 54.26 770.00 69.37% |
||||||
|
50301 requests in 30.10s, 7.53MB read |
||||||
|
Requests/sec: 1670.97 |
||||||
|
Transfer/sec: 256.21KB |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
- Spring Cloud Zuul测试结果 |
||||||
|
|
||||||
|
``` |
||||||
|
8 threads and 200 connections |
||||||
|
Thread Stats Avg Stdev Max +/- Stdev |
||||||
|
Latency 185.86ms 285.65ms 1.99s 88.55% |
||||||
|
Req/Sec 167.75 55.60 460.00 68.05% |
||||||
|
40070 requests in 30.09s, 6.65MB read |
||||||
|
Socket errors: connect 0, read 0, write 0, timeout 466 |
||||||
|
Requests/sec: 1331.81 |
||||||
|
Transfer/sec: 226.50KB |
||||||
|
``` |
||||||
|
|
||||||
|
|
||||||
|
综上所述,Spring Cloud Gateway在没有优化的情况下,压测表现比zuul好,但zuul的数据表现也不差,但是出现超时现象,总的来说还是Spring Cloud Gateway具有优势。 |
@ -1,23 +0,0 @@ |
|||||||
use sop; |
|
||||||
|
|
||||||
DROP TABLE IF EXISTS `config_service_route`; |
|
||||||
|
|
||||||
CREATE TABLE `config_service_route` ( |
|
||||||
`id` varchar(128) NOT NULL DEFAULT '' COMMENT '路由id', |
|
||||||
`service_id` varchar(128) NOT NULL DEFAULT '', |
|
||||||
`name` varchar(128) NOT NULL DEFAULT '' COMMENT '接口名', |
|
||||||
`version` varchar(64) NOT NULL DEFAULT '' COMMENT '版本号', |
|
||||||
`predicates` varchar(256) DEFAULT NULL COMMENT '路由断言(SpringCloudGateway专用)', |
|
||||||
`filters` varchar(256) DEFAULT NULL COMMENT '路由过滤器(SpringCloudGateway专用)', |
|
||||||
`uri` varchar(128) NOT NULL DEFAULT '' COMMENT '路由规则转发的目标uri', |
|
||||||
`path` varchar(128) NOT NULL DEFAULT '' COMMENT 'uri后面跟的path', |
|
||||||
`order` int(11) NOT NULL DEFAULT '0' COMMENT '路由执行的顺序', |
|
||||||
`ignore_validate` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否忽略验证,业务参数验证除外', |
|
||||||
`status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态,0:待审核,1:启用,2:禁用', |
|
||||||
`merge_result` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否合并结果', |
|
||||||
`permission` tinyint(4) NOT NULL DEFAULT '0' COMMENT '是否需要授权才能访问', |
|
||||||
`gmt_create` datetime DEFAULT CURRENT_TIMESTAMP, |
|
||||||
`gmt_modified` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, |
|
||||||
PRIMARY KEY (`id`), |
|
||||||
KEY `idx_serviceid` (`service_id`) USING BTREE |
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='路由配置'; |
|
@ -1 +0,0 @@ |
|||||||
ALTER TABLE `sop`.`config_limit` ADD COLUMN `duration_seconds` INT NOT NULL DEFAULT 1 COMMENT '限流持续时间,默认1秒,即每durationSeconds秒允许多少请求(当limit_type=1时有效)' AFTER `exec_count_per_second`; |
|
@ -1 +0,0 @@ |
|||||||
ALTER TABLE `sop`.`config_service_route` ADD COLUMN `need_token` TINYINT NOT NULL DEFAULT 0 COMMENT '是否需要token' AFTER `permission`; |
|
@ -0,0 +1,40 @@ |
|||||||
|
package com.gitee.sop.adminserver.config; |
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication; |
||||||
|
import org.springframework.boot.env.EnvironmentPostProcessor; |
||||||
|
import org.springframework.core.env.ConfigurableEnvironment; |
||||||
|
import org.springframework.core.env.PropertiesPropertySource; |
||||||
|
import org.springframework.core.env.PropertySource; |
||||||
|
import org.springframework.core.io.ClassPathResource; |
||||||
|
import org.springframework.core.io.Resource; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Properties; |
||||||
|
|
||||||
|
/** |
||||||
|
* 自定义环境处理,在运行SpringApplication之前加载任意配置文件到Environment环境中 |
||||||
|
* https://www.jianshu.com/p/be6c818fe6ff
|
||||||
|
*/ |
||||||
|
public class SopAdminEnvironmentPostProcessor implements EnvironmentPostProcessor { |
||||||
|
|
||||||
|
private final Properties properties = new Properties(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
||||||
|
Resource resource = new ClassPathResource("META-INF/sop-admin.properties"); |
||||||
|
// 加载成PropertySource对象,并添加到Environment环境中
|
||||||
|
environment.getPropertySources().addLast(loadProfiles(resource)); |
||||||
|
} |
||||||
|
|
||||||
|
private PropertySource<?> loadProfiles(Resource resource) { |
||||||
|
if (resource == null || !resource.exists()) { |
||||||
|
throw new IllegalArgumentException("资源" + resource + "不存在"); |
||||||
|
} |
||||||
|
try { |
||||||
|
properties.load(resource.getInputStream()); |
||||||
|
return new PropertiesPropertySource(resource.getFilename(), properties); |
||||||
|
} catch (IOException ex) { |
||||||
|
throw new IllegalStateException("加载配置文件失败" + resource, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,32 @@ |
|||||||
|
# 这里的配置不用改,如果要改在application-xx.properties中改 |
||||||
|
# 在application-xx.properties中配置会覆盖这里的值 |
||||||
|
# 参考:com.gitee.sop.adminserver.config.SopAdminEnvironmentPostProcessor |
||||||
|
|
||||||
|
spring.application.name=sop-admin |
||||||
|
|
||||||
|
# session过期时间,分钟 |
||||||
|
admin.access-token.timeout-minutes=30 |
||||||
|
# 签名方式,rsa:支付宝开放平台签名方式,md5:淘宝开放平台签名方式 |
||||||
|
sop.sign-type=rsa |
||||||
|
|
||||||
|
# nacos配置 |
||||||
|
nacos.config.server-addr=${nacos.url} |
||||||
|
nacos.discovery.server-addr=${nacos.url} |
||||||
|
|
||||||
|
# 数据库配置 |
||||||
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver |
||||||
|
spring.datasource.url=jdbc:mysql://${mysql.host}/sop?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai |
||||||
|
spring.datasource.username=${mysql.username} |
||||||
|
spring.datasource.password=${mysql.password} |
||||||
|
|
||||||
|
# 固定不用改 |
||||||
|
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml |
||||||
|
easyopen.show-doc=false |
||||||
|
easyopen.ignore-validate=true |
||||||
|
|
||||||
|
# 不用改 |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillInsert=gmt_create |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillUpdate=gmt_modified |
||||||
|
|
||||||
|
# 不用改,如果要改,请全局替换修改 |
||||||
|
sop.secret=MZZOUSTua6LzApIWXCwEgbBmxSzpzC |
@ -0,0 +1,2 @@ |
|||||||
|
# 自定义自动配置类,如果有多个类,使用逗号(,)分隔,\正斜杠标示换行还可以读取到指定的类 |
||||||
|
org.springframework.boot.env.EnvironmentPostProcessor=com.gitee.sop.adminserver.config.SopAdminEnvironmentPostProcessor |
@ -1 +1 @@ |
|||||||
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.c6dfb7ee.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function n(n){for(var r,c,a=n[0],f=n[1],i=n[2],d=0,l=[];d<a.length;d++)c=a[d],u[c]&&l.push(u[c][0]),u[c]=0;for(r in f)Object.prototype.hasOwnProperty.call(f,r)&&(e[r]=f[r]);h&&h(n);while(l.length)l.shift()();return o.push.apply(o,i||[]),t()}function t(){for(var e,n=0;n<o.length;n++){for(var t=o[n],r=!0,c=1;c<t.length;c++){var a=t[c];0!==u[a]&&(r=!1)}r&&(o.splice(n--,1),e=f(f.s=t[0]))}return e}var r={},c={runtime:0},u={runtime:0},o=[];function a(e){return f.p+"static/js/"+({}[e]||e)+"."+{"chunk-0faf9ba2":"88458f58","chunk-25908fca":"66819987","chunk-2c1f2e8f":"f092c0a0","chunk-2d2085ef":"91d75f3c","chunk-2d221c34":"20057287","chunk-4de1c2b6":"e74e3d03","chunk-6f78c9fe":"3ac83b41","chunk-73b2dcec":"60c5d8e9","chunk-9b31c83a":"52bc6b2c","chunk-9f479afe":"0d0ed757","chunk-c3ce42fe":"9517b588"}[e]+".js"}function f(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var n=[],t={"chunk-0faf9ba2":1,"chunk-25908fca":1,"chunk-2c1f2e8f":1,"chunk-4de1c2b6":1,"chunk-73b2dcec":1,"chunk-9b31c83a":1,"chunk-c3ce42fe":1};c[e]?n.push(c[e]):0!==c[e]&&t[e]&&n.push(c[e]=new Promise(function(n,t){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-0faf9ba2":"ed1510dd","chunk-25908fca":"a66354ec","chunk-2c1f2e8f":"0314067f","chunk-2d2085ef":"31d6cfe0","chunk-2d221c34":"31d6cfe0","chunk-4de1c2b6":"a37cd815","chunk-6f78c9fe":"31d6cfe0","chunk-73b2dcec":"ed391cc5","chunk-9b31c83a":"3b12267b","chunk-9f479afe":"31d6cfe0","chunk-c3ce42fe":"6b789903"}[e]+".css",u=f.p+r,o=document.getElementsByTagName("link"),a=0;a<o.length;a++){var i=o[a],d=i.getAttribute("data-href")||i.getAttribute("href");if("stylesheet"===i.rel&&(d===r||d===u))return n()}var l=document.getElementsByTagName("style");for(a=0;a<l.length;a++){i=l[a],d=i.getAttribute("data-href");if(d===r||d===u)return n()}var h=document.createElement("link");h.rel="stylesheet",h.type="text/css",h.onload=n,h.onerror=function(n){var r=n&&n.target&&n.target.src||u,o=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");o.code="CSS_CHUNK_LOAD_FAILED",o.request=r,delete c[e],h.parentNode.removeChild(h),t(o)},h.href=u;var s=document.getElementsByTagName("head")[0];s.appendChild(h)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)n.push(r[2]);else{var o=new Promise(function(n,t){r=u[e]=[n,t]});n.push(r[2]=o);var i,d=document.createElement("script");d.charset="utf-8",d.timeout=120,f.nc&&d.setAttribute("nonce",f.nc),d.src=a(e),i=function(n){d.onerror=d.onload=null,clearTimeout(l);var t=u[e];if(0!==t){if(t){var r=n&&("load"===n.type?"missing":n.type),c=n&&n.target&&n.target.src,o=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");o.type=r,o.request=c,t[1](o)}u[e]=void 0}};var l=setTimeout(function(){i({type:"timeout",target:d})},12e4);d.onerror=d.onload=i,document.head.appendChild(d)}return Promise.all(n)},f.m=e,f.c=r,f.d=function(e,n,t){f.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,n){if(1&n&&(e=f(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)f.d(t,r,function(n){return e[n]}.bind(null,r));return t},f.n=function(e){var n=e&&e.__esModule?function(){return e["default"]}:function(){return e};return f.d(n,"a",n),n},f.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},f.p="",f.oe=function(e){throw console.error(e),e};var i=window["webpackJsonp"]=window["webpackJsonp"]||[],d=i.push.bind(i);i.push=n,i=i.slice();for(var l=0;l<i.length;l++)n(i[l]);var h=d;t()})([]);</script><script src=static/js/chunk-elementUI.298ac98c.js></script><script src=static/js/chunk-libs.75deb05f.js></script><script src=static/js/app.f05d6d01.js></script></body></html> |
<!DOCTYPE html><html><head><meta charset=utf-8><meta http-equiv=X-UA-Compatible content="IE=edge,chrome=1"><meta name=viewport content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no"><link rel=icon href=favicon.ico><title>SOP Admin</title><link href=static/css/chunk-elementUI.81cf475c.css rel=stylesheet><link href=static/css/chunk-libs.3dfb7769.css rel=stylesheet><link href=static/css/app.c6dfb7ee.css rel=stylesheet></head><body><noscript><strong>We're sorry but SOP Admin doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id=app></div><script>(function(e){function n(n){for(var r,c,a=n[0],f=n[1],i=n[2],d=0,l=[];d<a.length;d++)c=a[d],u[c]&&l.push(u[c][0]),u[c]=0;for(r in f)Object.prototype.hasOwnProperty.call(f,r)&&(e[r]=f[r]);h&&h(n);while(l.length)l.shift()();return o.push.apply(o,i||[]),t()}function t(){for(var e,n=0;n<o.length;n++){for(var t=o[n],r=!0,c=1;c<t.length;c++){var a=t[c];0!==u[a]&&(r=!1)}r&&(o.splice(n--,1),e=f(f.s=t[0]))}return e}var r={},c={runtime:0},u={runtime:0},o=[];function a(e){return f.p+"static/js/"+({}[e]||e)+"."+{"chunk-0faf9ba2":"88458f58","chunk-25908fca":"66819987","chunk-2c1f2e8f":"f092c0a0","chunk-2d2085ef":"91d75f3c","chunk-2d221c34":"20057287","chunk-4de1c2b6":"e74e3d03","chunk-6f78c9fe":"3ac83b41","chunk-73b2dcec":"60c5d8e9","chunk-9b31c83a":"52bc6b2c","chunk-9f479afe":"b0599ada","chunk-c3ce42fe":"9517b588"}[e]+".js"}function f(n){if(r[n])return r[n].exports;var t=r[n]={i:n,l:!1,exports:{}};return e[n].call(t.exports,t,t.exports,f),t.l=!0,t.exports}f.e=function(e){var n=[],t={"chunk-0faf9ba2":1,"chunk-25908fca":1,"chunk-2c1f2e8f":1,"chunk-4de1c2b6":1,"chunk-73b2dcec":1,"chunk-9b31c83a":1,"chunk-c3ce42fe":1};c[e]?n.push(c[e]):0!==c[e]&&t[e]&&n.push(c[e]=new Promise(function(n,t){for(var r="static/css/"+({}[e]||e)+"."+{"chunk-0faf9ba2":"ed1510dd","chunk-25908fca":"a66354ec","chunk-2c1f2e8f":"0314067f","chunk-2d2085ef":"31d6cfe0","chunk-2d221c34":"31d6cfe0","chunk-4de1c2b6":"a37cd815","chunk-6f78c9fe":"31d6cfe0","chunk-73b2dcec":"ed391cc5","chunk-9b31c83a":"3b12267b","chunk-9f479afe":"31d6cfe0","chunk-c3ce42fe":"6b789903"}[e]+".css",u=f.p+r,o=document.getElementsByTagName("link"),a=0;a<o.length;a++){var i=o[a],d=i.getAttribute("data-href")||i.getAttribute("href");if("stylesheet"===i.rel&&(d===r||d===u))return n()}var l=document.getElementsByTagName("style");for(a=0;a<l.length;a++){i=l[a],d=i.getAttribute("data-href");if(d===r||d===u)return n()}var h=document.createElement("link");h.rel="stylesheet",h.type="text/css",h.onload=n,h.onerror=function(n){var r=n&&n.target&&n.target.src||u,o=new Error("Loading CSS chunk "+e+" failed.\n("+r+")");o.code="CSS_CHUNK_LOAD_FAILED",o.request=r,delete c[e],h.parentNode.removeChild(h),t(o)},h.href=u;var s=document.getElementsByTagName("head")[0];s.appendChild(h)}).then(function(){c[e]=0}));var r=u[e];if(0!==r)if(r)n.push(r[2]);else{var o=new Promise(function(n,t){r=u[e]=[n,t]});n.push(r[2]=o);var i,d=document.createElement("script");d.charset="utf-8",d.timeout=120,f.nc&&d.setAttribute("nonce",f.nc),d.src=a(e),i=function(n){d.onerror=d.onload=null,clearTimeout(l);var t=u[e];if(0!==t){if(t){var r=n&&("load"===n.type?"missing":n.type),c=n&&n.target&&n.target.src,o=new Error("Loading chunk "+e+" failed.\n("+r+": "+c+")");o.type=r,o.request=c,t[1](o)}u[e]=void 0}};var l=setTimeout(function(){i({type:"timeout",target:d})},12e4);d.onerror=d.onload=i,document.head.appendChild(d)}return Promise.all(n)},f.m=e,f.c=r,f.d=function(e,n,t){f.o(e,n)||Object.defineProperty(e,n,{enumerable:!0,get:t})},f.r=function(e){"undefined"!==typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},f.t=function(e,n){if(1&n&&(e=f(e)),8&n)return e;if(4&n&&"object"===typeof e&&e&&e.__esModule)return e;var t=Object.create(null);if(f.r(t),Object.defineProperty(t,"default",{enumerable:!0,value:e}),2&n&&"string"!=typeof e)for(var r in e)f.d(t,r,function(n){return e[n]}.bind(null,r));return t},f.n=function(e){var n=e&&e.__esModule?function(){return e["default"]}:function(){return e};return f.d(n,"a",n),n},f.o=function(e,n){return Object.prototype.hasOwnProperty.call(e,n)},f.p="",f.oe=function(e){throw console.error(e),e};var i=window["webpackJsonp"]=window["webpackJsonp"]||[],d=i.push.bind(i);i.push=n,i=i.slice();for(var l=0;l<i.length;l++)n(i[l]);var h=d;t()})([]);</script><script src=static/js/chunk-elementUI.298ac98c.js></script><script src=static/js/chunk-libs.75deb05f.js></script><script src=static/js/app.f05d6d01.js></script></body></html> |
File diff suppressed because one or more lines are too long
@ -0,0 +1,36 @@ |
|||||||
|
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<artifactId>sop-common</artifactId> |
||||||
|
<groupId>com.gitee.sop</groupId> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
<relativePath>../pom.xml</relativePath> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
|
||||||
|
<artifactId>sop-bridge-gateway</artifactId> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>com.gitee.sop</groupId> |
||||||
|
<artifactId>sop-gateway-common</artifactId> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.cloud</groupId> |
||||||
|
<artifactId>spring-cloud-starter-gateway</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.boot</groupId> |
||||||
|
<artifactId>spring-boot-starter-actuator</artifactId> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
</project> |
@ -0,0 +1,18 @@ |
|||||||
|
package com.gitee.sop.bridge; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.config.BaseGatewayAutoConfiguration; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.configuration.AlipayGatewayConfiguration; |
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore; |
||||||
|
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.context.annotation.Import; |
||||||
|
|
||||||
|
/** |
||||||
|
* https://blog.csdn.net/seashouwang/article/details/80299571
|
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
@Import(AlipayGatewayConfiguration.class) |
||||||
|
@AutoConfigureBefore(RibbonAutoConfiguration.class) |
||||||
|
public class SopGatewayAutoConfiguration extends BaseGatewayAutoConfiguration { |
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
# 固定不变,不能改 |
||||||
|
spring.application.name=sop-gateway |
||||||
|
# 不用改,如果要改,请全局替换修改 |
||||||
|
sop.secret=MZZOUSTua6LzApIWXCwEgbBmxSzpzC |
||||||
|
|
||||||
|
# 网关入口 |
||||||
|
sop.gateway-index-path=/ |
||||||
|
|
||||||
|
# nacos cloud配置 |
||||||
|
#spring.cloud.nacos.discovery.server-addr=${nacos.url} |
||||||
|
|
||||||
|
# 数据库配置 |
||||||
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver |
||||||
|
spring.datasource.url=jdbc:mysql://${mysql.host}/sop?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai |
||||||
|
spring.datasource.username=${mysql.username} |
||||||
|
spring.datasource.password=${mysql.password} |
||||||
|
|
||||||
|
# https://blog.csdn.net/qq_36872046/article/details/81058045 |
||||||
|
# 路由转发超时时间,毫秒,默认值1000,详见:RibbonClientConfiguration.DEFAULT_READ_TIMEOUT。 |
||||||
|
# 如果微服务端 处理时间过长,会导致ribbon read超时,解决办法将这个值调大一点 |
||||||
|
ribbon.ReadTimeout=2000 |
||||||
|
# 设置为true(默认false),则所有请求都重试,默认只支持get请求重试 |
||||||
|
# 请谨慎设置,因为post请求大多都是写入请求,如果要支持重试,确保服务的幂等性 |
||||||
|
ribbon.OkToRetryOnAllOperations=false |
||||||
|
|
||||||
|
#spring.cloud.gateway.discovery.locator.lower-case-service-id=true |
||||||
|
#spring.cloud.gateway.discovery.locator.enabled=true |
||||||
|
|
||||||
|
# 不用改 |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillInsert=gmt_create |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillUpdate=gmt_modified |
||||||
|
|
||||||
|
# 文件上传配置 |
||||||
|
spring.servlet.multipart.enabled=true |
||||||
|
# 这里设置大一点没关系,真实大小由upload.max-file-size控制 |
||||||
|
spring.servlet.multipart.max-file-size=50MB |
||||||
|
|
||||||
|
# 允许上传文件大小,不能超过这个值,单位:B,KB,MB |
||||||
|
upload.max-file-size=2MB |
@ -0,0 +1,32 @@ |
|||||||
|
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd"> |
||||||
|
<parent> |
||||||
|
<artifactId>sop-common</artifactId> |
||||||
|
<groupId>com.gitee.sop</groupId> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
<relativePath>../pom.xml</relativePath> |
||||||
|
</parent> |
||||||
|
<modelVersion>4.0.0</modelVersion> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
|
||||||
|
<artifactId>sop-bridge-zuul</artifactId> |
||||||
|
|
||||||
|
<dependencies> |
||||||
|
<dependency> |
||||||
|
<groupId>com.gitee.sop</groupId> |
||||||
|
<artifactId>sop-gateway-common</artifactId> |
||||||
|
<version>3.0.0-SNAPSHOT</version> |
||||||
|
</dependency> |
||||||
|
|
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.cloud</groupId> |
||||||
|
<artifactId>spring-cloud-starter-netflix-zuul</artifactId> |
||||||
|
</dependency> |
||||||
|
<dependency> |
||||||
|
<groupId>org.springframework.retry</groupId> |
||||||
|
<artifactId>spring-retry</artifactId> |
||||||
|
</dependency> |
||||||
|
</dependencies> |
||||||
|
</project> |
@ -0,0 +1,24 @@ |
|||||||
|
package com.gitee.sop.bridge; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.config.BaseGatewayAutoConfiguration; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.configuration.AlipayZuulConfiguration; |
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore; |
||||||
|
import org.springframework.boot.autoconfigure.web.servlet.error.ErrorMvcAutoConfiguration; |
||||||
|
import org.springframework.cloud.netflix.zuul.EnableZuulProxy; |
||||||
|
import org.springframework.context.annotation.Configuration; |
||||||
|
import org.springframework.context.annotation.Import; |
||||||
|
|
||||||
|
/** |
||||||
|
* https://blog.csdn.net/seashouwang/article/details/80299571
|
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Configuration |
||||||
|
@EnableZuulProxy |
||||||
|
@Import(AlipayZuulConfiguration.class) |
||||||
|
// 在ErrorMvcAutoConfiguration之前加载
|
||||||
|
// 如果不加会出现basicErrorController和zuulErrorController冲突
|
||||||
|
// zuulErrorController是SOP中的,提前加载后basicErrorController就不会加载
|
||||||
|
@AutoConfigureBefore({ErrorMvcAutoConfiguration.class}) |
||||||
|
public class SopGatewayAutoConfiguration extends BaseGatewayAutoConfiguration { |
||||||
|
} |
||||||
|
|
@ -0,0 +1,44 @@ |
|||||||
|
# 固定不变,不能改 |
||||||
|
spring.application.name=sop-gateway |
||||||
|
# 入口地址,不用改,默认是/zuul |
||||||
|
zuul.servlet-path=/api |
||||||
|
# 禁用默认的过滤器,不能删,不用改 |
||||||
|
zuul.FormBodyWrapperFilter.pre.disable=true |
||||||
|
zuul.Servlet30WrapperFilter.pre.disable=true |
||||||
|
# 不用改,如果要改,请全局替换修改 |
||||||
|
sop.secret=MZZOUSTua6LzApIWXCwEgbBmxSzpzC |
||||||
|
|
||||||
|
# zuul优化配置 |
||||||
|
zuul.host.max-per-route-connections=1000 |
||||||
|
zuul.host.max-total-connections=1000 |
||||||
|
zuul.semaphore.max-semaphores=1000 |
||||||
|
|
||||||
|
# nacos cloud配置 |
||||||
|
#spring.cloud.nacos.discovery.server-addr=${nacos.url} |
||||||
|
|
||||||
|
# 数据库配置 |
||||||
|
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver |
||||||
|
spring.datasource.url=jdbc:mysql://${mysql.host}/sop?useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai |
||||||
|
spring.datasource.username=${mysql.username} |
||||||
|
spring.datasource.password=${mysql.password} |
||||||
|
|
||||||
|
# https://blog.csdn.net/qq_36872046/article/details/81058045 |
||||||
|
# 路由转发超时时间,毫秒,默认值1000,详见:RibbonClientConfiguration.DEFAULT_READ_TIMEOUT。 |
||||||
|
# 如果微服务端 处理时间过长,会导致ribbon read超时,解决办法将这个值调大一点 |
||||||
|
ribbon.ReadTimeout=5000 |
||||||
|
# 设置为true(默认false),则所有请求都重试,默认只支持get请求重试 |
||||||
|
# 请谨慎设置,因为post请求大多都是写入请求,如果要支持重试,确保服务的幂等性 |
||||||
|
ribbon.OkToRetryOnAllOperations=false |
||||||
|
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000 |
||||||
|
|
||||||
|
# 不用改 |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillInsert=gmt_create |
||||||
|
mybatis.fill.com.gitee.fastmybatis.core.support.DateFillUpdate=gmt_modified |
||||||
|
|
||||||
|
# 文件上传配置 |
||||||
|
spring.servlet.multipart.enabled=true |
||||||
|
# 这里设置大一点没关系,真实大小由upload.max-file-size控制 |
||||||
|
spring.servlet.multipart.max-file-size=50MB |
||||||
|
|
||||||
|
# 允许上传文件大小,不能超过这个值,单位:B,KB,MB |
||||||
|
upload.max-file-size=2MB |
@ -0,0 +1,10 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.bean; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface ApiParamAware<T> { |
||||||
|
ApiParam getApiParam(T t); |
||||||
|
} |
@ -0,0 +1,48 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.config; |
||||||
|
|
||||||
|
import lombok.Data; |
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
@ConfigurationProperties(prefix = "sop.api-config") |
||||||
|
public class ApiConfigProperties { |
||||||
|
|
||||||
|
private List<String> i18nModules = new ArrayList<>(); |
||||||
|
|
||||||
|
/** |
||||||
|
* 忽略验证,设置true,则所有接口不会进行签名校验 |
||||||
|
*/ |
||||||
|
private boolean ignoreValidate; |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否对结果进行合并。<br> |
||||||
|
* 默认情况下是否合并结果由微服务端决定,一旦指定该值,则由该值决定,不管微服务端如何设置。 |
||||||
|
*/ |
||||||
|
private Boolean mergeResult; |
||||||
|
|
||||||
|
/** |
||||||
|
* 请求超时时间,默认5分钟,即允许在5分钟内重复请求 |
||||||
|
*/ |
||||||
|
private int timeoutSeconds = 300; |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否开启限流功能 |
||||||
|
*/ |
||||||
|
private boolean openLimit = true; |
||||||
|
|
||||||
|
/** |
||||||
|
* 显示返回sign |
||||||
|
*/ |
||||||
|
private boolean showReturnSign = true; |
||||||
|
|
||||||
|
/** |
||||||
|
* 保存错误信息容器的容量 |
||||||
|
*/ |
||||||
|
private int storeErrorCapacity = 20; |
||||||
|
} |
@ -0,0 +1,29 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.config; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.BeanUtils; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties; |
||||||
|
|
||||||
|
import javax.annotation.PostConstruct; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
@EnableConfigurationProperties(ApiConfigProperties.class) |
||||||
|
public class BaseGatewayAutoConfiguration { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private ApiConfigProperties apiConfigProperties; |
||||||
|
|
||||||
|
@PostConstruct |
||||||
|
public void after() { |
||||||
|
log.info("网关基本配置:{}", JSON.toJSONString(apiConfigProperties)); |
||||||
|
ApiConfig apiConfig = ApiConfig.getInstance(); |
||||||
|
BeanUtils.copyProperties(apiConfigProperties, apiConfig); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,39 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.config; |
||||||
|
|
||||||
|
import org.springframework.boot.SpringApplication; |
||||||
|
import org.springframework.boot.env.EnvironmentPostProcessor; |
||||||
|
import org.springframework.core.env.ConfigurableEnvironment; |
||||||
|
import org.springframework.core.env.PropertiesPropertySource; |
||||||
|
import org.springframework.core.env.PropertySource; |
||||||
|
import org.springframework.core.io.ClassPathResource; |
||||||
|
import org.springframework.core.io.Resource; |
||||||
|
|
||||||
|
import java.io.IOException; |
||||||
|
import java.util.Properties; |
||||||
|
|
||||||
|
/** |
||||||
|
* 自定义环境处理,在运行SpringApplication之前加载任意配置文件到Environment环境中 |
||||||
|
*/ |
||||||
|
public class SopGatewayEnvironmentPostProcessor implements EnvironmentPostProcessor { |
||||||
|
|
||||||
|
private final Properties properties = new Properties(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { |
||||||
|
Resource resource = new ClassPathResource("sop-bridge.properties"); |
||||||
|
// 加载成PropertySource对象,并添加到Environment环境中
|
||||||
|
environment.getPropertySources().addLast(loadProfiles(resource)); |
||||||
|
} |
||||||
|
|
||||||
|
private PropertySource<?> loadProfiles(Resource resource) { |
||||||
|
if (resource == null || !resource.exists()) { |
||||||
|
throw new IllegalArgumentException("资源" + resource + "不存在"); |
||||||
|
} |
||||||
|
try { |
||||||
|
properties.load(resource.getInputStream()); |
||||||
|
return new PropertiesPropertySource(resource.getFilename(), properties); |
||||||
|
} catch (IOException ex) { |
||||||
|
throw new IllegalStateException("加载配置文件失败" + resource, ex); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,75 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.controller; |
||||||
|
|
||||||
|
import com.alibaba.fastjson.JSON; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.GatewayPushDTO; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.NacosConfigs; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SpringContext; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.ChannelMsgProcessor; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.EnvGrayManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.IPBlacklistManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.IsvRoutePermissionManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.LimitConfigManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteConfigManager; |
||||||
|
import com.gitee.sop.gatewaycommon.secret.IsvManager; |
||||||
|
import com.gitee.sop.gatewaycommon.util.RequestUtil; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.factory.annotation.Value; |
||||||
|
import org.springframework.web.bind.annotation.PostMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import java.util.HashMap; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
@RestController |
||||||
|
public class ConfigChannelController { |
||||||
|
|
||||||
|
private static Map<String, Class<? extends ChannelMsgProcessor>> processorMap = new HashMap<>(16); |
||||||
|
|
||||||
|
static { |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_GRAY, EnvGrayManager.class); |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_IP_BLACKLIST, IPBlacklistManager.class); |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_ISV, IsvManager.class); |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_ROUTE_PERMISSION, IsvRoutePermissionManager.class); |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_LIMIT_CONFIG, LimitConfigManager.class); |
||||||
|
processorMap.put(NacosConfigs.GROUP_CHANNEL + NacosConfigs.DATA_ID_ROUTE_CONFIG, RouteConfigManager.class); |
||||||
|
} |
||||||
|
|
||||||
|
@Value("${sop.secret}") |
||||||
|
private String secret; |
||||||
|
|
||||||
|
@PostMapping("/sop/configChannelMsg") |
||||||
|
public Mono<String> configChannel(ServerWebExchange exchange) { |
||||||
|
ServerRequest serverRequest = ServerWebExchangeUtil.createReadBodyRequest(exchange); |
||||||
|
// 读取请求体中的内容
|
||||||
|
return serverRequest.bodyToMono(String.class) |
||||||
|
.flatMap(requestJson -> { |
||||||
|
String sign = exchange.getRequest().getHeaders().getFirst("sign"); |
||||||
|
try { |
||||||
|
// 签名验证
|
||||||
|
RequestUtil.checkResponseBody(requestJson, sign, secret); |
||||||
|
} catch (Exception e) { |
||||||
|
log.error("configChannelMsg错误", e); |
||||||
|
return Mono.just(e.getMessage()); |
||||||
|
} |
||||||
|
GatewayPushDTO gatewayPushDTO = JSON.parseObject(requestJson, GatewayPushDTO.class); |
||||||
|
ChannelMsgProcessor channelMsgProcessor = getChannelMsgProcessor(gatewayPushDTO); |
||||||
|
channelMsgProcessor.process(gatewayPushDTO.getChannelMsg()); |
||||||
|
return Mono.just("ok"); |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private ChannelMsgProcessor getChannelMsgProcessor(GatewayPushDTO gatewayPushDTO) { |
||||||
|
String key = gatewayPushDTO.getGroupId() + gatewayPushDTO.getDataId(); |
||||||
|
Class<? extends ChannelMsgProcessor> aClass = processorMap.get(key); |
||||||
|
return SpringContext.getBean(aClass); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,21 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.controller; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseErrorLogController; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@RestController |
||||||
|
public class ErrorLogController extends BaseErrorLogController<ServerWebExchange> { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ApiParam getApiParam(ServerWebExchange request) { |
||||||
|
Map<String, String> params = request.getRequest().getQueryParams().toSingleValueMap(); |
||||||
|
return ApiParam.build(params); |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,42 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.controller; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiContext; |
||||||
|
import com.gitee.sop.gatewaycommon.exception.ApiException; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.message.ErrorEnum; |
||||||
|
import com.gitee.sop.gatewaycommon.result.ResultExecutor; |
||||||
|
import org.springframework.web.bind.annotation.RequestMapping; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@RestController |
||||||
|
public class GatewayController { |
||||||
|
|
||||||
|
/** |
||||||
|
* 处理签名错误返回 |
||||||
|
* |
||||||
|
* @param exchange exchange |
||||||
|
* @return 返回最终结果 |
||||||
|
*/ |
||||||
|
@RequestMapping("/sop/validateError") |
||||||
|
public Mono<String> validateError(ServerWebExchange exchange) { |
||||||
|
Throwable throwable = ServerWebExchangeUtil.getThrowable(exchange); |
||||||
|
// 合并微服务传递过来的结果,变成最终结果
|
||||||
|
ResultExecutor<ServerWebExchange, String> resultExecutor = ApiContext.getApiConfig().getGatewayResultExecutor(); |
||||||
|
String gatewayResult = resultExecutor.buildErrorResult(exchange, throwable); |
||||||
|
return Mono.just(gatewayResult); |
||||||
|
} |
||||||
|
|
||||||
|
@RequestMapping("/sop/unknown") |
||||||
|
public Mono<String> unknown(ServerWebExchange exchange) { |
||||||
|
ApiException exception = ErrorEnum.ISV_INVALID_METHOD.getErrorMeta().getException(); |
||||||
|
ResultExecutor<ServerWebExchange, String> resultExecutor = ApiContext.getApiConfig().getGatewayResultExecutor(); |
||||||
|
String gatewayResult = resultExecutor.buildErrorResult(exchange, exception); |
||||||
|
return Mono.just(gatewayResult); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,156 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.filter; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.exception.ApiException; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.gateway.route.GatewayForwardChooser; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.EnvironmentKeys; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.route.ForwardInfo; |
||||||
|
import com.gitee.sop.gatewaycommon.validate.Validator; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
import org.springframework.beans.factory.annotation.Value; |
||||||
|
import org.springframework.cloud.gateway.support.BodyInserterContext; |
||||||
|
import org.springframework.cloud.gateway.support.CachedBodyOutputMessage; |
||||||
|
import org.springframework.core.Ordered; |
||||||
|
import org.springframework.core.annotation.Order; |
||||||
|
import org.springframework.core.io.buffer.DataBuffer; |
||||||
|
import org.springframework.http.HttpHeaders; |
||||||
|
import org.springframework.http.HttpMethod; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequest; |
||||||
|
import org.springframework.http.server.reactive.ServerHttpRequestDecorator; |
||||||
|
import org.springframework.web.reactive.function.BodyInserter; |
||||||
|
import org.springframework.web.reactive.function.BodyInserters; |
||||||
|
import org.springframework.web.reactive.function.server.ServerRequest; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
import org.springframework.web.server.WebFilter; |
||||||
|
import org.springframework.web.server.WebFilterChain; |
||||||
|
import reactor.core.publisher.Flux; |
||||||
|
import reactor.core.publisher.Mono; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
import java.util.Objects; |
||||||
|
|
||||||
|
/** |
||||||
|
* 入口 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
@Order(Ordered.HIGHEST_PRECEDENCE) |
||||||
|
public class IndexFilter implements WebFilter { |
||||||
|
|
||||||
|
private static final String REST_PATH_PREFIX = "/rest"; |
||||||
|
private static final String SOP_PATH_PREFIX = "/sop"; |
||||||
|
|
||||||
|
@Value("${sop.gateway-index-path:/}") |
||||||
|
private String indexPath; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private Validator validator; |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private GatewayForwardChooser gatewayForwardChooser; |
||||||
|
|
||||||
|
@Override |
||||||
|
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { |
||||||
|
ServerHttpRequest request = exchange.getRequest(); |
||||||
|
String path = request.getURI().getPath(); |
||||||
|
// SOP路径,直接放行
|
||||||
|
if (path.startsWith(SOP_PATH_PREFIX)) { |
||||||
|
return chain.filter(exchange); |
||||||
|
} |
||||||
|
// 如果是restful请求,直接转发
|
||||||
|
if (path.startsWith(REST_PATH_PREFIX)) { |
||||||
|
String sopRestfulEnableValue = EnvironmentKeys.SOP_RESTFUL_ENABLE.getValue(); |
||||||
|
if (!Objects.equals("true", sopRestfulEnableValue)) { |
||||||
|
log.error("尝试调用restful请求,但sop.restful.enable未开启"); |
||||||
|
return ServerWebExchangeUtil.forwardUnknown(exchange, chain); |
||||||
|
} |
||||||
|
ServerWebExchange newExchange = ServerWebExchangeUtil.getRestfulExchange(exchange, path); |
||||||
|
return chain.filter(newExchange); |
||||||
|
} |
||||||
|
if (Objects.equals(path, indexPath)) { |
||||||
|
if (request.getMethod() == HttpMethod.POST) { |
||||||
|
ServerRequest serverRequest = ServerWebExchangeUtil.createReadBodyRequest(exchange); |
||||||
|
// 读取请求体中的内容
|
||||||
|
Mono<?> modifiedBody = serverRequest.bodyToMono(String.class) |
||||||
|
.flatMap(body -> { |
||||||
|
// 构建ApiParam
|
||||||
|
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange, body); |
||||||
|
// 签名验证
|
||||||
|
doValidate(exchange, apiParam); |
||||||
|
return Mono.just(body); |
||||||
|
}); |
||||||
|
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, (Class) String.class); |
||||||
|
HttpHeaders headers = new HttpHeaders(); |
||||||
|
headers.putAll(exchange.getRequest().getHeaders()); |
||||||
|
|
||||||
|
// the new content type will be computed by bodyInserter
|
||||||
|
// and then set in the request decorator
|
||||||
|
headers.remove(HttpHeaders.CONTENT_LENGTH); |
||||||
|
|
||||||
|
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage( |
||||||
|
exchange, headers); |
||||||
|
return bodyInserter.insert(outputMessage, new BodyInserterContext()) |
||||||
|
.then(Mono.defer(() -> { |
||||||
|
ForwardInfo forwardInfo = gatewayForwardChooser.getForwardInfo(exchange); |
||||||
|
ServerHttpRequest decorator = decorate(exchange, headers, outputMessage); |
||||||
|
ServerWebExchange newExchange = exchange.mutate().request(decorator).build(); |
||||||
|
ServerWebExchange forwardExchange = ServerWebExchangeUtil.getForwardExchange(newExchange, forwardInfo); |
||||||
|
return chain.filter(forwardExchange); |
||||||
|
})); |
||||||
|
|
||||||
|
} else { |
||||||
|
URI uri = exchange.getRequest().getURI(); |
||||||
|
// 原始参数
|
||||||
|
String originalQuery = uri.getRawQuery(); |
||||||
|
// 构建ApiParam
|
||||||
|
ApiParam apiParam = ServerWebExchangeUtil.getApiParam(exchange, originalQuery); |
||||||
|
// 签名验证
|
||||||
|
doValidate(exchange, apiParam); |
||||||
|
|
||||||
|
ForwardInfo forwardInfo = gatewayForwardChooser.getForwardInfo(exchange); |
||||||
|
ServerWebExchange forwardExchange = ServerWebExchangeUtil.getForwardExchange(exchange, forwardInfo); |
||||||
|
return chain.filter(forwardExchange); |
||||||
|
} |
||||||
|
} else { |
||||||
|
return ServerWebExchangeUtil.forwardUnknown(exchange, chain); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private void doValidate(ServerWebExchange exchange, ApiParam apiParam) { |
||||||
|
try { |
||||||
|
validator.validate(apiParam); |
||||||
|
} catch (ApiException e) { |
||||||
|
log.error("验证失败,ip:{}, params:{}, errorMsg:{}", apiParam.fetchIp(), apiParam.toJSONString(), e.getMessage()); |
||||||
|
ServerWebExchangeUtil.setThrowable(exchange, e); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private ServerHttpRequestDecorator decorate( |
||||||
|
ServerWebExchange exchange |
||||||
|
, HttpHeaders headers |
||||||
|
, CachedBodyOutputMessage outputMessage |
||||||
|
) { |
||||||
|
return new ServerHttpRequestDecorator(exchange.getRequest()) { |
||||||
|
@Override |
||||||
|
public HttpHeaders getHeaders() { |
||||||
|
long contentLength = headers.getContentLength(); |
||||||
|
HttpHeaders httpHeaders = new HttpHeaders(); |
||||||
|
httpHeaders.putAll(super.getHeaders()); |
||||||
|
if (contentLength > 0) { |
||||||
|
httpHeaders.setContentLength(contentLength); |
||||||
|
} else { |
||||||
|
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked"); |
||||||
|
} |
||||||
|
return httpHeaders; |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Flux<DataBuffer> getBody() { |
||||||
|
return outputMessage.getBody(); |
||||||
|
} |
||||||
|
}; |
||||||
|
} |
||||||
|
} |
@ -1,58 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.gateway.filter; |
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
|
||||||
import com.gitee.sop.gatewaycommon.util.RouteUtil; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter; |
|
||||||
import org.springframework.cloud.gateway.route.Route; |
|
||||||
import org.springframework.core.Ordered; |
|
||||||
import org.springframework.util.StringUtils; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
import org.springframework.web.util.UriComponentsBuilder; |
|
||||||
import reactor.core.publisher.Mono; |
|
||||||
|
|
||||||
import java.net.URI; |
|
||||||
|
|
||||||
import static org.springframework.cloud.gateway.filter.LoadBalancerClientFilter.LOAD_BALANCER_CLIENT_FILTER_ORDER; |
|
||||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; |
|
||||||
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; |
|
||||||
|
|
||||||
/** |
|
||||||
* 在LoadBalancerClientFilter后面处理,从Route中找到具体的path,然后插入到uri的path中 |
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Slf4j |
|
||||||
public class LoadBalancerClientExtFilter implements GlobalFilter, Ordered { |
|
||||||
|
|
||||||
@Override |
|
||||||
public int getOrder() { |
|
||||||
return LOAD_BALANCER_CLIENT_FILTER_ORDER + 1; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { |
|
||||||
Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR); |
|
||||||
String path = this.findPath(exchange, route); |
|
||||||
if (StringUtils.hasLength(path)) { |
|
||||||
URI url = exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR); |
|
||||||
UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.fromUri(url); |
|
||||||
uriComponentsBuilder.path(path); |
|
||||||
URI requestUrl = uriComponentsBuilder.build(true).toUri(); |
|
||||||
exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUrl); |
|
||||||
} |
|
||||||
return chain.filter(exchange); |
|
||||||
} |
|
||||||
|
|
||||||
protected String findPath(ServerWebExchange exchange, Route route) { |
|
||||||
String path = exchange.getAttribute(SopConstants.REDIRECT_PATH_KEY); |
|
||||||
if (path != null) { |
|
||||||
return path; |
|
||||||
} |
|
||||||
URI routeUri = route.getUri(); |
|
||||||
String uriStr = routeUri.toString(); |
|
||||||
return RouteUtil.findPath(uriStr); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,32 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.filter; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.gateway.loadbalancer.SopLoadBalancerClient; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient; |
||||||
|
import org.springframework.cloud.gateway.config.LoadBalancerProperties; |
||||||
|
import org.springframework.cloud.gateway.filter.LoadBalancerClientFilter; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
import java.net.URI; |
||||||
|
|
||||||
|
import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; |
||||||
|
|
||||||
|
/** |
||||||
|
* 扩展负载均衡过滤器 |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class SopLoadBalancerClientFilter extends LoadBalancerClientFilter { |
||||||
|
public SopLoadBalancerClientFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) { |
||||||
|
super(loadBalancer, properties); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ServiceInstance choose(ServerWebExchange exchange) { |
||||||
|
if (loadBalancer instanceof SopLoadBalancerClient) { |
||||||
|
SopLoadBalancerClient sopLoadBalancerClient = (SopLoadBalancerClient)loadBalancer; |
||||||
|
return sopLoadBalancerClient.choose(((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost(), exchange); |
||||||
|
} else { |
||||||
|
return super.choose(exchange); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,48 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.gateway.filter; |
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.exception.ApiException; |
|
||||||
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
|
||||||
import com.gitee.sop.gatewaycommon.param.ApiParam; |
|
||||||
import com.gitee.sop.gatewaycommon.param.ParamBuilder; |
|
||||||
import com.gitee.sop.gatewaycommon.validate.Validator; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
import org.springframework.beans.factory.annotation.Autowired; |
|
||||||
import org.springframework.cloud.gateway.filter.GatewayFilterChain; |
|
||||||
import org.springframework.cloud.gateway.filter.GlobalFilter; |
|
||||||
import org.springframework.core.Ordered; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
import reactor.core.publisher.Mono; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Slf4j |
|
||||||
public class ValidateFilter implements GlobalFilter, Ordered { |
|
||||||
|
|
||||||
@Autowired |
|
||||||
private ParamBuilder<ServerWebExchange> paramBuilder; |
|
||||||
|
|
||||||
@Autowired |
|
||||||
private Validator validator; |
|
||||||
|
|
||||||
@Override |
|
||||||
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { |
|
||||||
// 解析参数
|
|
||||||
ApiParam param = paramBuilder.build(exchange); |
|
||||||
ServerWebExchangeUtil.setApiParam(exchange, param); |
|
||||||
// 验证操作,这里有负责验证签名参数
|
|
||||||
try { |
|
||||||
validator.validate(param); |
|
||||||
} catch (ApiException e) { |
|
||||||
log.error("验证失败,params:{}", param.toJSONString(), e); |
|
||||||
throw e; |
|
||||||
} |
|
||||||
return chain.filter(exchange); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public int getOrder() { |
|
||||||
// 最优先执行
|
|
||||||
return Orders.VALIDATE_FILTER_ORDER; |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,29 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.loadbalancer.LoadBalanceServerChooser; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class GatewayLoadBalanceServerChooser extends LoadBalanceServerChooser<ServerWebExchange, ServiceInstance> { |
||||||
|
|
||||||
|
public GatewayLoadBalanceServerChooser(SpringClientFactory clientFactory) { |
||||||
|
this.setClientFactory(clientFactory); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getHost(ServerWebExchange exchange) { |
||||||
|
return exchange.getRequest().getURI().getHost(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam getApiParam(ServerWebExchange exchange) { |
||||||
|
return ServerWebExchangeUtil.getApiParam(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.loadbalancer; |
||||||
|
|
||||||
|
import com.netflix.loadbalancer.Server; |
||||||
|
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer; |
||||||
|
import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; |
||||||
|
|
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class NacosServerIntrospector extends DefaultServerIntrospector { |
||||||
|
|
||||||
|
@Override |
||||||
|
public Map<String, String> getMetadata(Server server) { |
||||||
|
if (server instanceof NacosServer) { |
||||||
|
NacosServer discoveryServer = (NacosServer)server; |
||||||
|
return discoveryServer.getInstance().getMetadata(); |
||||||
|
} else { |
||||||
|
return super.getMetadata(server); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,107 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.loadbalancer.ServerChooserContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.netflix.client.config.IClientConfig; |
||||||
|
import com.netflix.loadbalancer.Server; |
||||||
|
import org.springframework.cloud.client.ServiceInstance; |
||||||
|
import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; |
||||||
|
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient; |
||||||
|
import org.springframework.cloud.netflix.ribbon.RibbonUtils; |
||||||
|
import org.springframework.cloud.netflix.ribbon.ServerIntrospector; |
||||||
|
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
import java.util.List; |
||||||
|
import java.util.concurrent.ThreadLocalRandom; |
||||||
|
|
||||||
|
/** |
||||||
|
* 重写负载均衡处理。 |
||||||
|
* 默认使用的是RibbonLoadBalancerClient类,详见org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration#loadBalancerClient() |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class SopLoadBalancerClient extends RibbonLoadBalancerClient implements ServerChooserContext<ServerWebExchange> { |
||||||
|
|
||||||
|
private final SpringClientFactory clientFactory; |
||||||
|
private GatewayLoadBalanceServerChooser loadBalanceServerChooser; |
||||||
|
|
||||||
|
public SopLoadBalancerClient(SpringClientFactory clientFactory) { |
||||||
|
super(clientFactory); |
||||||
|
this.clientFactory = clientFactory; |
||||||
|
this.loadBalanceServerChooser = new GatewayLoadBalanceServerChooser(clientFactory); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* New: Select a server using a 'key'. |
||||||
|
*/ |
||||||
|
@Override |
||||||
|
public ServiceInstance choose(String serviceId, Object hint) { |
||||||
|
return loadBalanceServerChooser.choose( |
||||||
|
serviceId |
||||||
|
, (ServerWebExchange) hint |
||||||
|
, this.getLoadBalancer(serviceId) |
||||||
|
, () -> super.choose(serviceId, hint) |
||||||
|
, (servers) -> getRibbonServer(serviceId, servers) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam getApiParam(ServerWebExchange exchange) { |
||||||
|
return ServerWebExchangeUtil.getApiParam(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getHost(ServerWebExchange exchange) { |
||||||
|
return exchange.getRequest().getURI().getHost(); |
||||||
|
} |
||||||
|
|
||||||
|
private RibbonServer getRibbonServer(String serviceId, List<Server> servers) { |
||||||
|
Server server = this.chooseRandomServer(servers); |
||||||
|
if (server == null) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
return new RibbonServer( |
||||||
|
serviceId |
||||||
|
, server |
||||||
|
, isSecure(server, serviceId) |
||||||
|
, serverIntrospector(serviceId).getMetadata(server) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 随机选取一台实例 |
||||||
|
* |
||||||
|
* @param servers 服务列表 |
||||||
|
* @return 返回实例,没有返回null |
||||||
|
*/ |
||||||
|
private Server chooseRandomServer(List<Server> servers) { |
||||||
|
if (servers.isEmpty()) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
int serverCount = servers.size(); |
||||||
|
// 随机选取一台实例
|
||||||
|
int index = chooseRandomInt(serverCount); |
||||||
|
return servers.get(index); |
||||||
|
} |
||||||
|
|
||||||
|
private int chooseRandomInt(int serverCount) { |
||||||
|
return ThreadLocalRandom.current().nextInt(serverCount); |
||||||
|
} |
||||||
|
|
||||||
|
private ServerIntrospector serverIntrospector(String serviceId) { |
||||||
|
ServerIntrospector serverIntrospector = this.clientFactory.getInstance(serviceId, |
||||||
|
ServerIntrospector.class); |
||||||
|
if (serverIntrospector == null) { |
||||||
|
serverIntrospector = new DefaultServerIntrospector(); |
||||||
|
} |
||||||
|
return serverIntrospector; |
||||||
|
} |
||||||
|
|
||||||
|
private boolean isSecure(Server server, String serviceId) { |
||||||
|
IClientConfig config = this.clientFactory.getClientConfig(serviceId); |
||||||
|
ServerIntrospector serverIntrospector = serverIntrospector(serviceId); |
||||||
|
return RibbonUtils.isSecure(config, serverIntrospector, server); |
||||||
|
} |
||||||
|
} |
@ -1,27 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.gateway.param; |
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
|
||||||
import com.gitee.sop.gatewaycommon.param.ApiParam; |
|
||||||
import com.gitee.sop.gatewaycommon.param.BaseParamBuilder; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public class GatewayParamBuilder extends BaseParamBuilder<ServerWebExchange> { |
|
||||||
|
|
||||||
@Override |
|
||||||
public ApiParam buildRequestParams(ServerWebExchange exchange) { |
|
||||||
return ServerWebExchangeUtil.getRequestParams(exchange); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public String getIP(ServerWebExchange ctx) { |
|
||||||
return ctx.getRequest().getRemoteAddress().getAddress().getHostAddress(); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public void setVersionInHeader(ServerWebExchange ctx, String headerName, String version) { |
|
||||||
ctx.getRequest().getHeaders().add(headerName, version); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,29 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.gateway.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.route.BaseForwardChooser; |
||||||
|
import com.gitee.sop.gatewaycommon.route.ForwardInfo; |
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class GatewayForwardChooser extends BaseForwardChooser<ServerWebExchange> { |
||||||
|
|
||||||
|
private static final String VALIDATE_ERROR_PATH = "/sop/validateError"; |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam getApiParam(ServerWebExchange exchange) { |
||||||
|
return ServerWebExchangeUtil.getApiParam(exchange); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ForwardInfo getForwardInfo(ServerWebExchange exchange) { |
||||||
|
// 如果有异常,直接跳转到异常处理
|
||||||
|
if (ServerWebExchangeUtil.getThrowable(exchange) != null) { |
||||||
|
return new ForwardInfo(VALIDATE_ERROR_PATH, ""); |
||||||
|
} |
||||||
|
return super.getForwardInfo(exchange); |
||||||
|
} |
||||||
|
} |
@ -1,105 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.gateway.route; |
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
|
||||||
import com.gitee.sop.gatewaycommon.gateway.ServerWebExchangeUtil; |
|
||||||
import com.gitee.sop.gatewaycommon.param.ParamNames; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; |
|
||||||
import org.springframework.util.AntPathMatcher; |
|
||||||
import org.springframework.util.CollectionUtils; |
|
||||||
import org.springframework.util.PathMatcher; |
|
||||||
import org.springframework.validation.annotation.Validated; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
|
|
||||||
import javax.validation.constraints.NotEmpty; |
|
||||||
import java.util.Arrays; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Map; |
|
||||||
import java.util.function.Predicate; |
|
||||||
|
|
||||||
/** |
|
||||||
* 此断言决定执行哪个路由 |
|
||||||
* |
|
||||||
* 使用地方: |
|
||||||
* @see com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache |
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Slf4j |
|
||||||
public class NameVersionRoutePredicateFactory extends AbstractRoutePredicateFactory<NameVersionRoutePredicateFactory.Config> { |
|
||||||
|
|
||||||
private static final String PARAM_KEY = "param"; |
|
||||||
private static final String REGEXP_KEY = "regexp"; |
|
||||||
|
|
||||||
private PathMatcher pathMatcher = new AntPathMatcher(); |
|
||||||
|
|
||||||
public NameVersionRoutePredicateFactory() { |
|
||||||
super(Config.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public List<String> shortcutFieldOrder() { |
|
||||||
return Arrays.asList(PARAM_KEY, REGEXP_KEY); |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* config.param为nameVersion,即路由id |
|
||||||
* |
|
||||||
* @param config |
|
||||||
* @return 返回断言 |
|
||||||
*/ |
|
||||||
@Override |
|
||||||
public Predicate<ServerWebExchange> apply(Config config) { |
|
||||||
|
|
||||||
return exchange -> { |
|
||||||
Map<String, ?> params = ServerWebExchangeUtil.getRequestParams(exchange); |
|
||||||
if (CollectionUtils.isEmpty(params)) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
String nameVersion = config.param; |
|
||||||
Object name = params.get(ParamNames.API_NAME); |
|
||||||
Object version = params.get(ParamNames.VERSION_NAME); |
|
||||||
if (name == null || version == null) { |
|
||||||
return false; |
|
||||||
} |
|
||||||
String key = name.toString() + version.toString(); |
|
||||||
// 如果是通配符
|
|
||||||
if (nameVersion.contains("{")) { |
|
||||||
boolean match = pathMatcher.match(nameVersion, key); |
|
||||||
if (match) { |
|
||||||
Map<String, Object> attributes = exchange.getAttributes(); |
|
||||||
attributes.put(SopConstants.REDIRECT_PATH_KEY, key); |
|
||||||
} |
|
||||||
return match; |
|
||||||
} else { |
|
||||||
return key.equals(nameVersion); |
|
||||||
} |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
@Validated |
|
||||||
public static class Config { |
|
||||||
@NotEmpty |
|
||||||
private String param; |
|
||||||
|
|
||||||
private String regexp; |
|
||||||
|
|
||||||
public String getParam() { |
|
||||||
return param; |
|
||||||
} |
|
||||||
|
|
||||||
public Config setParam(String param) { |
|
||||||
this.param = param; |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
public String getRegexp() { |
|
||||||
return regexp; |
|
||||||
} |
|
||||||
|
|
||||||
public Config setRegexp(String regexp) { |
|
||||||
this.regexp = regexp; |
|
||||||
return this; |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,202 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.gateway.route; |
|
||||||
|
|
||||||
import org.apache.commons.lang.StringUtils; |
|
||||||
import org.apache.commons.logging.Log; |
|
||||||
import org.apache.commons.logging.LogFactory; |
|
||||||
import org.springframework.cloud.gateway.handler.AsyncPredicate; |
|
||||||
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory; |
|
||||||
import org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory; |
|
||||||
import org.springframework.core.io.buffer.DataBuffer; |
|
||||||
import org.springframework.core.io.buffer.DataBufferUtils; |
|
||||||
import org.springframework.http.HttpMethod; |
|
||||||
import org.springframework.http.codec.HttpMessageReader; |
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequest; |
|
||||||
import org.springframework.http.server.reactive.ServerHttpRequestDecorator; |
|
||||||
import org.springframework.util.MultiValueMap; |
|
||||||
import org.springframework.web.reactive.function.server.HandlerStrategies; |
|
||||||
import org.springframework.web.reactive.function.server.ServerRequest; |
|
||||||
import org.springframework.web.server.ServerWebExchange; |
|
||||||
import reactor.core.publisher.Flux; |
|
||||||
import reactor.core.publisher.Mono; |
|
||||||
|
|
||||||
import java.util.ArrayList; |
|
||||||
import java.util.List; |
|
||||||
import java.util.Map; |
|
||||||
import java.util.Objects; |
|
||||||
import java.util.function.Predicate; |
|
||||||
|
|
||||||
import static org.springframework.cloud.gateway.filter.AdaptCachedBodyGlobalFilter.CACHED_REQUEST_BODY_KEY; |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取请求参数插件,兼容get,post,使用方式: |
|
||||||
* @Bean |
|
||||||
* ReadBodyRoutePredicateFactory readBodyRoutePredicateFactory() { |
|
||||||
* return new ReadBodyRoutePredicateFactory(); |
|
||||||
* } |
|
||||||
* |
|
||||||
* @see org.springframework.cloud.gateway.handler.predicate.ReadBodyPredicateFactory |
|
||||||
* 详见:https://blog.51cto.com/thinklili/2329184
|
|
||||||
* |
|
||||||
* 使用地方: |
|
||||||
* @see com.gitee.sop.gatewaycommon.gateway.route.GatewayRouteCache |
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public class ReadBodyRoutePredicateFactory extends AbstractRoutePredicateFactory<ReadBodyRoutePredicateFactory.Config> { |
|
||||||
|
|
||||||
protected static final Log LOGGER = LogFactory.getLog(ReadBodyPredicateFactory.class); |
|
||||||
|
|
||||||
private static final String TEST_ATTRIBUTE = "read_body_predicate_test_attribute"; |
|
||||||
private static final String CACHE_REQUEST_BODY_OBJECT_KEY = "cachedRequestBodyObject"; |
|
||||||
private static final List<HttpMessageReader<?>> HTTP_MESSAGE_READERS = HandlerStrategies.withDefaults().messageReaders(); |
|
||||||
|
|
||||||
|
|
||||||
public ReadBodyRoutePredicateFactory() { |
|
||||||
super(ReadBodyRoutePredicateFactory.Config.class); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public AsyncPredicate<ServerWebExchange> applyAsync(Config config) { |
|
||||||
return exchange -> { |
|
||||||
HttpMethod method = exchange.getRequest().getMethod(); |
|
||||||
if (method == HttpMethod.POST) { |
|
||||||
return this.applyForPost(exchange, config); |
|
||||||
} else { |
|
||||||
return this.applyForGet(exchange, config); |
|
||||||
} |
|
||||||
}; |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取post表单参数 |
|
||||||
* @param exchange |
|
||||||
* @param config |
|
||||||
* @return |
|
||||||
*/ |
|
||||||
protected Mono<Boolean> applyForPost(ServerWebExchange exchange, Config config) { |
|
||||||
Class inClass = config.getInClass(); |
|
||||||
|
|
||||||
Object cachedBody = exchange.getAttribute(CACHE_REQUEST_BODY_OBJECT_KEY); |
|
||||||
// We can only read the body from the request once, once that happens if we try to read the body again an
|
|
||||||
// exception will be thrown. The below if/else caches the body object as a request attribute in the ServerWebExchange
|
|
||||||
// so if this filter is run more than once (due to more than one route using it) we do not try to read the
|
|
||||||
// request body multiple times
|
|
||||||
if (cachedBody != null) { |
|
||||||
try { |
|
||||||
boolean test = config.getPredicate().test(cachedBody); |
|
||||||
exchange.getAttributes().put(TEST_ATTRIBUTE, test); |
|
||||||
return Mono.just(test); |
|
||||||
} catch (ClassCastException e) { |
|
||||||
if (LOGGER.isDebugEnabled()) { |
|
||||||
LOGGER.debug("Predicate test failed because class in predicate does not canVisit the cached body object", |
|
||||||
e); |
|
||||||
} |
|
||||||
} |
|
||||||
return Mono.just(false); |
|
||||||
} else { |
|
||||||
//Join all the DataBuffers so we have a single DataBuffer for the body
|
|
||||||
return DataBufferUtils.join(exchange.getRequest().getBody()) |
|
||||||
.flatMap(dataBuffer -> { |
|
||||||
//Update the retain counts so we can read the body twice, once to parse into an object
|
|
||||||
//that we can test the predicate against and a second time when the HTTP client sends
|
|
||||||
//the request downstream
|
|
||||||
//Note: if we end up reading the body twice we will run into a problem, but as of right
|
|
||||||
//now there is no good use case for doing this
|
|
||||||
DataBufferUtils.retain(dataBuffer); |
|
||||||
//Make a slice for each read so each read has its own read/write indexes
|
|
||||||
Flux<DataBuffer> cachedFlux = Flux.defer(() -> Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount()))); |
|
||||||
|
|
||||||
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(exchange.getRequest()) { |
|
||||||
@Override |
|
||||||
public Flux<DataBuffer> getBody() { |
|
||||||
return cachedFlux; |
|
||||||
} |
|
||||||
}; |
|
||||||
return ServerRequest.create(exchange.mutate().request(mutatedRequest).build(), HTTP_MESSAGE_READERS) |
|
||||||
.bodyToMono(inClass) |
|
||||||
.doOnNext(objectValue -> { |
|
||||||
exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue); |
|
||||||
exchange.getAttributes().put(CACHED_REQUEST_BODY_KEY, cachedFlux); |
|
||||||
}) |
|
||||||
.map(objectValue -> config.getPredicate().test(objectValue)); |
|
||||||
}); |
|
||||||
|
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* 获取GET请求参数 |
|
||||||
* @param exchange |
|
||||||
* @param config |
|
||||||
* @return |
|
||||||
*/ |
|
||||||
protected Mono<Boolean> applyForGet(ServerWebExchange exchange, Config config) { |
|
||||||
// 处理get请求
|
|
||||||
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams(); |
|
||||||
String objectValue = null; |
|
||||||
if (queryParams != null && queryParams.size() > 0) { |
|
||||||
List<String> params = new ArrayList<>(queryParams.size()); |
|
||||||
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) { |
|
||||||
params.add(entry.getKey() + "=" + entry.getValue().get(0)); |
|
||||||
} |
|
||||||
objectValue = StringUtils.join(params.toArray()); |
|
||||||
exchange.getAttributes().put(CACHE_REQUEST_BODY_OBJECT_KEY, objectValue); |
|
||||||
} |
|
||||||
return Mono.just(config.getPredicate().test(objectValue)); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
public Config newConfig() { |
|
||||||
Config config = super.newConfig(); |
|
||||||
config.setInClass(String.class); |
|
||||||
config.setPredicate(Objects::nonNull); |
|
||||||
return config; |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
@SuppressWarnings("unchecked") |
|
||||||
public Predicate<ServerWebExchange> apply(Config config) { |
|
||||||
throw new UnsupportedOperationException( |
|
||||||
"ReadBodyPredicateFactory is only async."); |
|
||||||
} |
|
||||||
|
|
||||||
public static class Config { |
|
||||||
private Class inClass; |
|
||||||
private Predicate predicate; |
|
||||||
private Map<String, Object> hints; |
|
||||||
|
|
||||||
public Class getInClass() { |
|
||||||
return inClass; |
|
||||||
} |
|
||||||
|
|
||||||
public Config setInClass(Class inClass) { |
|
||||||
this.inClass = inClass; |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
public Predicate getPredicate() { |
|
||||||
return predicate; |
|
||||||
} |
|
||||||
|
|
||||||
public <T> Config setPredicate(Class<T> inClass, Predicate<T> predicate) { |
|
||||||
setInClass(inClass); |
|
||||||
this.predicate = predicate; |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
public Config setPredicate(Predicate predicate) { |
|
||||||
this.predicate = predicate; |
|
||||||
return this; |
|
||||||
} |
|
||||||
|
|
||||||
public Map<String, Object> getHints() { |
|
||||||
return hints; |
|
||||||
} |
|
||||||
|
|
||||||
public Config setHints(Map<String, Object> hints) { |
|
||||||
this.hints = hints; |
|
||||||
return this; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,64 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.loadbalancer; |
|
||||||
|
|
||||||
import com.gitee.sop.gatewaycommon.bean.SpringContext; |
|
||||||
import com.gitee.sop.gatewaycommon.zuul.loadbalancer.BaseServerChooser; |
|
||||||
import com.netflix.loadbalancer.Server; |
|
||||||
import org.springframework.cloud.alibaba.nacos.ribbon.NacosServer; |
|
||||||
import org.springframework.core.env.Environment; |
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest; |
|
||||||
import java.util.Map; |
|
||||||
|
|
||||||
/** |
|
||||||
* 预发布、灰度环境选择,参考自:https://segmentfault.com/a/1190000017412946
|
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public class EnvironmentServerChooser extends BaseServerChooser { |
|
||||||
|
|
||||||
private static final String MEDATA_KEY_ENV = "env"; |
|
||||||
private static final String ENV_PRE_VALUE = "pre"; |
|
||||||
private static final String ENV_GRAY_VALUE = "gray"; |
|
||||||
|
|
||||||
/** |
|
||||||
* 预发布机器域名 |
|
||||||
*/ |
|
||||||
private static final String PRE_DOMAIN = "localhost"; |
|
||||||
|
|
||||||
@Override |
|
||||||
protected boolean isPreServer(Server server) { |
|
||||||
String env = getEnvValue(server); |
|
||||||
return ENV_PRE_VALUE.equals(env); |
|
||||||
} |
|
||||||
|
|
||||||
@Override |
|
||||||
protected boolean isGrayServer(Server server) { |
|
||||||
String env = getEnvValue(server); |
|
||||||
return ENV_GRAY_VALUE.equals(env); |
|
||||||
} |
|
||||||
|
|
||||||
private String getEnvValue(Server server) { |
|
||||||
// eureka存储的metadata
|
|
||||||
Map<String, String> metadata = getMetada(server); |
|
||||||
return metadata.get(MEDATA_KEY_ENV); |
|
||||||
} |
|
||||||
|
|
||||||
protected Map<String, String> getMetada(Server server) { |
|
||||||
return ((NacosServer) server).getMetadata(); |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
/** |
|
||||||
* 通过判断hostname来确定是否是预发布请求,可修改此方法实现自己想要的 |
|
||||||
* |
|
||||||
* @param request request |
|
||||||
* @return 返回true:可以进入到预发环境 |
|
||||||
*/ |
|
||||||
@Override |
|
||||||
protected boolean canVisitPre(Server server, HttpServletRequest request) { |
|
||||||
String serverName = request.getServerName(); |
|
||||||
String domain = SpringContext.getBean(Environment.class).getProperty("pre.domain", PRE_DOMAIN); |
|
||||||
return domain.equals(serverName); |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,16 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.loadbalancer; |
|
||||||
|
|
||||||
import com.netflix.loadbalancer.Server; |
|
||||||
import com.netflix.niws.loadbalancer.DiscoveryEnabledServer; |
|
||||||
|
|
||||||
import java.util.Map; |
|
||||||
|
|
||||||
/** |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
public class EurekaEnvironmentServerChooser extends EnvironmentServerChooser { |
|
||||||
@Override |
|
||||||
protected Map<String, String> getMetada(Server server) { |
|
||||||
return ((DiscoveryEnabledServer) server).getInstanceInfo().getMetadata(); |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,8 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class LoadBalanceConfig { |
||||||
|
|
||||||
|
} |
@ -0,0 +1,128 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.SopConstants; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.SpringContext; |
||||||
|
import com.netflix.loadbalancer.ILoadBalancer; |
||||||
|
import com.netflix.loadbalancer.Server; |
||||||
|
import org.springframework.cloud.netflix.ribbon.DefaultServerIntrospector; |
||||||
|
import org.springframework.cloud.netflix.ribbon.ServerIntrospector; |
||||||
|
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; |
||||||
|
|
||||||
|
import java.util.ArrayList; |
||||||
|
import java.util.List; |
||||||
|
import java.util.Map; |
||||||
|
import java.util.Objects; |
||||||
|
import java.util.function.Function; |
||||||
|
import java.util.function.Supplier; |
||||||
|
|
||||||
|
/** |
||||||
|
* 预发布、灰度发布服务器选择 |
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public abstract class LoadBalanceServerChooser<T, R> implements ServerChooserContext<T> { |
||||||
|
|
||||||
|
private SpringClientFactory clientFactory; |
||||||
|
|
||||||
|
/** |
||||||
|
* 选择服务器 |
||||||
|
* |
||||||
|
* @param serviceId serviceId,仅gateway网关有作用 |
||||||
|
* @param exchange 请求上下文 |
||||||
|
* @param loadBalancer loadBalancer |
||||||
|
* @param superChooser 父类默认的选择 |
||||||
|
* @param serverChooserFunction 执行选择操作 |
||||||
|
* @return 返回服务器实例,没有选到则返回null |
||||||
|
*/ |
||||||
|
public R choose( |
||||||
|
String serviceId |
||||||
|
, T exchange |
||||||
|
, ILoadBalancer loadBalancer |
||||||
|
, Supplier<R> superChooser |
||||||
|
, Function<List<Server>, R> serverChooserFunction) { |
||||||
|
// 获取所有服务实例
|
||||||
|
List<Server> servers = loadBalancer.getReachableServers(); |
||||||
|
|
||||||
|
// 存放预发服务器
|
||||||
|
List<Server> preServers = new ArrayList<>(4); |
||||||
|
// 存放灰度发布服务器
|
||||||
|
List<Server> grayServers = new ArrayList<>(4); |
||||||
|
// 存放非预发服务器
|
||||||
|
List<Server> notPreServers = new ArrayList<>(4); |
||||||
|
|
||||||
|
for (Server server : servers) { |
||||||
|
// 获取实例metadata
|
||||||
|
Map<String, String> metadata = getMetadata(serviceId, server); |
||||||
|
// 是否开启了预发模式
|
||||||
|
if (this.isPreServer(metadata)) { |
||||||
|
preServers.add(server); |
||||||
|
} else if (this.isGrayServer(metadata)) { |
||||||
|
grayServers.add(server); |
||||||
|
} else { |
||||||
|
notPreServers.add(server); |
||||||
|
} |
||||||
|
} |
||||||
|
notPreServers.addAll(grayServers); |
||||||
|
// 如果没有开启预发布服务和灰度发布,直接用默认的方式
|
||||||
|
if (preServers.isEmpty() && grayServers.isEmpty()) { |
||||||
|
return superChooser.get(); |
||||||
|
} |
||||||
|
// 如果是从预发布域名访问过来,则认为是预发布请求,选出预发服务器
|
||||||
|
if (this.isRequestFromPreDomain(exchange)) { |
||||||
|
return serverChooserFunction.apply(preServers); |
||||||
|
} |
||||||
|
// 如果是灰度请求,则认为是灰度用户,选出灰度服务器
|
||||||
|
if (this.isRequestGrayServer(exchange)) { |
||||||
|
return serverChooserFunction.apply(grayServers); |
||||||
|
} |
||||||
|
|
||||||
|
// 到这里说明不能访问预发/灰度服务器,则需要路由到非预发服务器
|
||||||
|
// 注意:这里允许走灰度服务器,如果不允许走,注释notPreServers.addAll(grayServers);这行
|
||||||
|
return serverChooserFunction.apply(notPreServers); |
||||||
|
} |
||||||
|
|
||||||
|
protected Map<String, String> getMetadata(String serviceId, Server server) { |
||||||
|
return serverIntrospector(serviceId).getMetadata(server); |
||||||
|
} |
||||||
|
|
||||||
|
protected SpringClientFactory getSpringClientFactory() { |
||||||
|
if (clientFactory == null) { |
||||||
|
clientFactory = SpringContext.getBean(SpringClientFactory.class); |
||||||
|
} |
||||||
|
return clientFactory; |
||||||
|
} |
||||||
|
|
||||||
|
public void setClientFactory(SpringClientFactory clientFactory) { |
||||||
|
this.clientFactory = clientFactory; |
||||||
|
} |
||||||
|
|
||||||
|
private ServerIntrospector serverIntrospector(String serviceId) { |
||||||
|
ServerIntrospector serverIntrospector = getSpringClientFactory().getInstance(serviceId, |
||||||
|
ServerIntrospector.class); |
||||||
|
if (serverIntrospector == null) { |
||||||
|
serverIntrospector = new DefaultServerIntrospector(); |
||||||
|
} |
||||||
|
return serverIntrospector; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否是预发布服务器 |
||||||
|
* |
||||||
|
* @param metadata metadata |
||||||
|
* @return true:是 |
||||||
|
*/ |
||||||
|
private boolean isPreServer(Map<String, String> metadata) { |
||||||
|
return Objects.equals(metadata.get(SopConstants.METADATA_ENV_KEY), SopConstants.METADATA_ENV_PRE_VALUE); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* 是否是灰度发布服务器 |
||||||
|
* |
||||||
|
* @param metadata metadata |
||||||
|
* @return true:是 |
||||||
|
*/ |
||||||
|
private boolean isGrayServer(Map<String, String> metadata) { |
||||||
|
return Objects.equals(metadata.get(SopConstants.METADATA_ENV_KEY), SopConstants.METADATA_ENV_GRAY_VALUE); |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,35 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiParamAware; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.EnvironmentKeys; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import org.apache.commons.lang.ArrayUtils; |
||||||
|
import org.springframework.util.StringUtils; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface ServerChooserContext<T> extends ApiParamAware<T> { |
||||||
|
|
||||||
|
/** |
||||||
|
* 通过判断hostname来确定是否是预发布请求 |
||||||
|
* |
||||||
|
* @param t t |
||||||
|
* @return 返回true:可以进入到预发环境 |
||||||
|
*/ |
||||||
|
default boolean isRequestFromPreDomain(T t) { |
||||||
|
String domain = EnvironmentKeys.PRE_DOMAIN.getValue(); |
||||||
|
if (StringUtils.isEmpty(domain)) { |
||||||
|
return false; |
||||||
|
} |
||||||
|
String[] domains = domain.split("\\,"); |
||||||
|
return ArrayUtils.contains(domains, getHost(t)); |
||||||
|
} |
||||||
|
|
||||||
|
default boolean isRequestGrayServer(T t) { |
||||||
|
ApiParam apiParam = getApiParam(t); |
||||||
|
return apiParam.isGrayRequest(); |
||||||
|
} |
||||||
|
|
||||||
|
String getHost(T t); |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer.builder; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class AppIdGrayUserBuilder implements GrayUserBuilder { |
||||||
|
|
||||||
|
@Override |
||||||
|
public String buildGrayUserKey(ApiParam apiParam) { |
||||||
|
return apiParam.fetchAppKey(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int order() { |
||||||
|
return 0; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer.builder; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface GrayUserBuilder { |
||||||
|
|
||||||
|
/** |
||||||
|
* 获取灰度用户key |
||||||
|
* |
||||||
|
* @param apiParam apiParam |
||||||
|
* @return 返回用户key |
||||||
|
*/ |
||||||
|
String buildGrayUserKey(ApiParam apiParam); |
||||||
|
|
||||||
|
/** |
||||||
|
* 优先级,数字小优先 |
||||||
|
* |
||||||
|
* @return 返回数字 |
||||||
|
*/ |
||||||
|
int order(); |
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.loadbalancer.builder; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class IpGrayUserBuilder implements GrayUserBuilder { |
||||||
|
|
||||||
|
@Override |
||||||
|
public String buildGrayUserKey(ApiParam apiParam) { |
||||||
|
return apiParam.fetchIp(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public int order() { |
||||||
|
return 1; |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.result; |
||||||
|
|
||||||
|
import org.springframework.web.server.ServerWebExchange; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface ResultExecutorForGateway extends ResultExecutor<ServerWebExchange, String> { |
||||||
|
} |
@ -0,0 +1,9 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.result; |
||||||
|
|
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface ResultExecutorForZuul extends ResultExecutor<RequestContext, String> { |
||||||
|
} |
@ -0,0 +1,66 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.route; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiConfig; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.RouteDefinition; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.ApiParamAware; |
||||||
|
import com.gitee.sop.gatewaycommon.bean.TargetRoute; |
||||||
|
import com.gitee.sop.gatewaycommon.loadbalancer.builder.GrayUserBuilder; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.EnvGrayManager; |
||||||
|
import com.gitee.sop.gatewaycommon.manager.RouteRepositoryContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import org.apache.commons.lang3.BooleanUtils; |
||||||
|
import org.springframework.beans.factory.annotation.Autowired; |
||||||
|
|
||||||
|
import javax.annotation.PostConstruct; |
||||||
|
import java.util.Comparator; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public abstract class BaseForwardChooser<T> implements ForwardChooser<T>, ApiParamAware<T> { |
||||||
|
|
||||||
|
@Autowired |
||||||
|
private EnvGrayManager envGrayManager; |
||||||
|
|
||||||
|
@Override |
||||||
|
public ForwardInfo getForwardInfo(T t) { |
||||||
|
ApiParam apiParam = getApiParam(t); |
||||||
|
String nameVersion = apiParam.fetchNameVersion(); |
||||||
|
TargetRoute targetRoute = RouteRepositoryContext.getRouteRepository().get(nameVersion); |
||||||
|
RouteDefinition routeDefinitionOrig = targetRoute.getRouteDefinition(); |
||||||
|
String path = routeDefinitionOrig.getPath(); |
||||||
|
String version = apiParam.fetchVersion(); |
||||||
|
String serviceId = targetRoute.getServiceRouteInfo().fetchServiceIdLowerCase(); |
||||||
|
// 如果服务在灰度阶段,返回一个灰度版本号
|
||||||
|
String grayVersion = envGrayManager.getVersion(serviceId, nameVersion); |
||||||
|
// 如果是灰度环境
|
||||||
|
if (grayVersion != null && isGrayUser(serviceId, apiParam)) { |
||||||
|
String newNameVersion = apiParam.fetchName() + grayVersion; |
||||||
|
TargetRoute targetRouteDest = RouteRepositoryContext.getRouteRepository().get(newNameVersion); |
||||||
|
if (targetRouteDest != null) { |
||||||
|
apiParam.setGrayRequest(true); |
||||||
|
if (BooleanUtils.toBoolean(routeDefinitionOrig.getCompatibleMode())) { |
||||||
|
version = grayVersion; |
||||||
|
} else { |
||||||
|
// 获取灰度接口
|
||||||
|
RouteDefinition routeDefinition = targetRouteDest.getRouteDefinition(); |
||||||
|
path = routeDefinition.getPath(); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
return new ForwardInfo(path, version); |
||||||
|
} |
||||||
|
|
||||||
|
protected boolean isGrayUser(String serviceId, ApiParam apiParam) { |
||||||
|
List<GrayUserBuilder> grayUserBuilders = ApiConfig.getInstance().getGrayUserBuilders(); |
||||||
|
for (GrayUserBuilder grayUserBuilder : grayUserBuilders) { |
||||||
|
String userKey = grayUserBuilder.buildGrayUserKey(apiParam); |
||||||
|
if (envGrayManager.containsKey(serviceId, userKey)) { |
||||||
|
return true; |
||||||
|
} |
||||||
|
} |
||||||
|
return false; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,16 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.route; |
||||||
|
|
||||||
|
/** |
||||||
|
* 转发选择 |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public interface ForwardChooser<T> { |
||||||
|
|
||||||
|
/** |
||||||
|
* 返回转发信息 |
||||||
|
* @param t 上下文 |
||||||
|
* @return 返回转发信息 |
||||||
|
*/ |
||||||
|
ForwardInfo getForwardInfo(T t); |
||||||
|
|
||||||
|
} |
@ -0,0 +1,19 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.route; |
||||||
|
|
||||||
|
import lombok.Data; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Data |
||||||
|
public class ForwardInfo { |
||||||
|
private String path; |
||||||
|
private String version; |
||||||
|
private String domain; |
||||||
|
|
||||||
|
public ForwardInfo(String path, String version) { |
||||||
|
this.path = path; |
||||||
|
this.version = version; |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,23 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.controller; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.bean.BaseErrorLogController; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.util.RequestUtil; |
||||||
|
import org.springframework.stereotype.Controller; |
||||||
|
import org.springframework.web.bind.annotation.RestController; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import java.util.Map; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@RestController |
||||||
|
public class ErrorLogController extends BaseErrorLogController<HttpServletRequest> { |
||||||
|
|
||||||
|
@Override |
||||||
|
protected ApiParam getApiParam(HttpServletRequest request) { |
||||||
|
Map<String, String> params = RequestUtil.convertRequestParamsToMap(request); |
||||||
|
return ApiParam.build(params); |
||||||
|
} |
||||||
|
} |
@ -1,81 +0,0 @@ |
|||||||
package com.gitee.sop.gatewaycommon.zuul.loadbalancer; |
|
||||||
|
|
||||||
import com.google.common.base.Optional; |
|
||||||
import com.netflix.loadbalancer.ILoadBalancer; |
|
||||||
import com.netflix.loadbalancer.Server; |
|
||||||
import com.netflix.loadbalancer.ZoneAvoidanceRule; |
|
||||||
import com.netflix.zuul.context.RequestContext; |
|
||||||
import lombok.extern.slf4j.Slf4j; |
|
||||||
|
|
||||||
import javax.servlet.http.HttpServletRequest; |
|
||||||
import java.util.List; |
|
||||||
import java.util.stream.Collectors; |
|
||||||
|
|
||||||
/** |
|
||||||
* 服务实例选择器 |
|
||||||
* |
|
||||||
* @author tanghc |
|
||||||
*/ |
|
||||||
@Slf4j |
|
||||||
public abstract class BaseServerChooser extends ZoneAvoidanceRule { |
|
||||||
|
|
||||||
|
|
||||||
/** |
|
||||||
* 是否是预发布服务器 |
|
||||||
* @param server 服务器 |
|
||||||
* @return true:是 |
|
||||||
*/ |
|
||||||
protected abstract boolean isPreServer(Server server); |
|
||||||
|
|
||||||
/** |
|
||||||
* 是否是灰度发布服务器 |
|
||||||
* @param server 服务器 |
|
||||||
* @return true:是 |
|
||||||
*/ |
|
||||||
protected abstract boolean isGrayServer(Server server); |
|
||||||
|
|
||||||
/** |
|
||||||
* 能否访问预发布 |
|
||||||
* @param server 预发布服务器 |
|
||||||
* @param request request |
|
||||||
* @return true:能 |
|
||||||
*/ |
|
||||||
protected abstract boolean canVisitPre(Server server, HttpServletRequest request); |
|
||||||
|
|
||||||
|
|
||||||
@Override |
|
||||||
public Server choose(Object key) { |
|
||||||
ILoadBalancer lb = getLoadBalancer(); |
|
||||||
// 获取服务实例列表
|
|
||||||
List<Server> allServers = lb.getAllServers(); |
|
||||||
HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); |
|
||||||
|
|
||||||
List<Server> preServers = allServers.stream() |
|
||||||
.filter(this::isPreServer) |
|
||||||
.filter(server -> canVisitPre(server, request)) |
|
||||||
.collect(Collectors.toList()); |
|
||||||
|
|
||||||
if (!preServers.isEmpty()) { |
|
||||||
return this.doChoose(preServers, key); |
|
||||||
} |
|
||||||
|
|
||||||
List<Server> grayServers = allServers.stream() |
|
||||||
.filter(this::isGrayServer) |
|
||||||
.collect(Collectors.toList()); |
|
||||||
if (!grayServers.isEmpty()) { |
|
||||||
return doChoose(grayServers, key); |
|
||||||
} |
|
||||||
|
|
||||||
return super.choose(key); |
|
||||||
} |
|
||||||
|
|
||||||
protected Server doChoose(List<Server> servers, Object key) { |
|
||||||
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(servers, key); |
|
||||||
if (server.isPresent()) { |
|
||||||
return server.get(); |
|
||||||
} else { |
|
||||||
return null; |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -0,0 +1,55 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.loadbalancer.ServerChooserContext; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||||
|
import com.google.common.base.Optional; |
||||||
|
import com.netflix.loadbalancer.Server; |
||||||
|
import com.netflix.loadbalancer.ZoneAvoidanceRule; |
||||||
|
import com.netflix.zuul.context.RequestContext; |
||||||
|
import lombok.extern.slf4j.Slf4j; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
import java.util.List; |
||||||
|
|
||||||
|
/** |
||||||
|
* 预发布、灰度环境选择,参考自:https://segmentfault.com/a/1190000017412946
|
||||||
|
* |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
@Slf4j |
||||||
|
public class EnvironmentServerChooser extends ZoneAvoidanceRule implements ServerChooserContext<HttpServletRequest> { |
||||||
|
|
||||||
|
private ZuulLoadBalanceServerChooser loadBalanceServerChooser = new ZuulLoadBalanceServerChooser(); |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getHost(HttpServletRequest request) { |
||||||
|
return request.getServerName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam getApiParam(HttpServletRequest request) { |
||||||
|
return ZuulContext.getApiParam(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public Server choose(Object key) { |
||||||
|
return loadBalanceServerChooser.choose( |
||||||
|
String.valueOf(key) |
||||||
|
, RequestContext.getCurrentContext().getRequest() |
||||||
|
, getLoadBalancer() |
||||||
|
, () -> super.choose(key) |
||||||
|
, (servers) -> this.doChoose(servers, key) |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
protected Server doChoose(List<Server> servers, Object key) { |
||||||
|
Optional<Server> server = getPredicate().chooseRoundRobinAfterFiltering(servers, key); |
||||||
|
if (server.isPresent()) { |
||||||
|
return server.get(); |
||||||
|
} else { |
||||||
|
return null; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,24 @@ |
|||||||
|
package com.gitee.sop.gatewaycommon.zuul.loadbalancer; |
||||||
|
|
||||||
|
import com.gitee.sop.gatewaycommon.loadbalancer.LoadBalanceServerChooser; |
||||||
|
import com.gitee.sop.gatewaycommon.param.ApiParam; |
||||||
|
import com.gitee.sop.gatewaycommon.zuul.ZuulContext; |
||||||
|
import com.netflix.loadbalancer.Server; |
||||||
|
|
||||||
|
import javax.servlet.http.HttpServletRequest; |
||||||
|
|
||||||
|
/** |
||||||
|
* @author tanghc |
||||||
|
*/ |
||||||
|
public class ZuulLoadBalanceServerChooser extends LoadBalanceServerChooser<HttpServletRequest, Server> { |
||||||
|
|
||||||
|
@Override |
||||||
|
public String getHost(HttpServletRequest request) { |
||||||
|
return request.getServerName(); |
||||||
|
} |
||||||
|
|
||||||
|
@Override |
||||||
|
public ApiParam getApiParam(HttpServletRequest request) { |
||||||
|
return ZuulContext.getApiParam(); |
||||||
|
} |
||||||
|
} |
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue