├── doc
├── 图片转换地址
├── caffeine.png
├── gateway.jpeg
├── 网关请求时序图.jpg
├── gateway
│ ├── 一个Thread.png
│ ├── 三个Thread.png
│ ├── state_machine.jpeg
│ ├── springcloud_gateway工作原理图.png
│ ├── SpringCloud Gateway如何设置keep-alive.md
│ ├── gateway过滤器工厂.md
│ ├── Gateway网关异常处理.md
│ ├── gateway路由断言工厂.md
│ └── Resilience4j CircuitBreaker断路器.md
├── 证书生成文档
├── 定制java11镜像.md
├── Connection prematurely closed BEFORE response.md
├── springboot2.4开启https异常.md
├── 死磕源码系列【consul配置中心监视器类ConfigWatch动态刷新配置】.md
├── NettyWebServer开启http端口并实现http自动跳转https.md
└── 死磕源码系列【springcloud gateway网关缓存请求body及删除缓存body之AdaptCachedBodyGlobalFilter和RemoveCachedBodyFilter过滤器】.md
├── src
└── main
│ ├── resources
│ ├── application.properties
│ ├── emily.p12
│ ├── config-bak
│ │ ├── logback.xml
│ │ ├── application-server.properties
│ │ ├── application.yml
│ │ └── application-gateway.yml
│ ├── index.html
│ ├── bootstrap.yml
│ ├── application-server.properties
│ ├── application-gateway.properties
│ └── logback.xml
│ └── java
│ ├── com
│ └── emily
│ │ └── infrastructure
│ │ └── gateway
│ │ ├── common
│ │ ├── enums
│ │ │ ├── TraceType.java
│ │ │ └── HttpStatusType.java
│ │ ├── RecordLogger.java
│ │ ├── utils
│ │ │ └── FilterUtils.java
│ │ ├── entity
│ │ │ ├── BaseResponse.java
│ │ │ └── BaseLogger.java
│ │ ├── DataBufferUtils.java
│ │ └── ServerRequestUtils.java
│ │ ├── GatewayBootstrap.java
│ │ └── config
│ │ ├── filter
│ │ ├── order
│ │ │ └── GatewayFilterOrdered.java
│ │ ├── GatewayFilterAutoConfiguration.java
│ │ ├── dedupe
│ │ │ └── DedupeLoginGatewayFilterFactory.java
│ │ └── logger
│ │ │ ├── RecordLoggerResponseDecorator.java
│ │ │ └── RecordLoggerGatewayFilterFactory.java
│ │ ├── predicate
│ │ ├── PredicateFactoryAutoConfiguration.java
│ │ └── path
│ │ │ └── PathOffRoutePredicateFactory.java
│ │ ├── exception
│ │ ├── WebFluxExceptionProperties.java
│ │ ├── handler
│ │ │ ├── WebFluxErrorWebExceptionHandler.java
│ │ │ └── WebFluxErrorAttributes.java
│ │ └── WebFluxExceptionAutoConfiguration.java
│ │ ├── circuitbreaker
│ │ ├── CircuitBreakerProperties.java
│ │ ├── controller
│ │ │ └── CircuitBreakerController.java
│ │ └── CircuitBreakerAutoConfiguration.java
│ │ └── server
│ │ ├── NettyWebServerProperties.java
│ │ └── NettyWebServerAutoConfiguration.java
│ └── org
│ └── springframework
│ └── http
│ └── server
│ └── reactive
│ └── ReactorServerHttpRequest.java
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ ├── maven-wrapper.properties
│ └── MavenWrapperDownloader.java
├── Dockerfile
├── .gitignore
├── README.md
├── pom.xml
├── mvnw.cmd
└── mvnw
/doc/图片转换地址:
--------------------------------------------------------------------------------
1 | https://jinaconvert.com/cn/convert-to-ico.php
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | spring.profiles.active=server,gateway
--------------------------------------------------------------------------------
/doc/caffeine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/caffeine.png
--------------------------------------------------------------------------------
/doc/gateway.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/gateway.jpeg
--------------------------------------------------------------------------------
/doc/网关请求时序图.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/网关请求时序图.jpg
--------------------------------------------------------------------------------
/doc/gateway/一个Thread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/gateway/一个Thread.png
--------------------------------------------------------------------------------
/doc/gateway/三个Thread.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/gateway/三个Thread.png
--------------------------------------------------------------------------------
/src/main/resources/emily.p12:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/src/main/resources/emily.p12
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/doc/gateway/state_machine.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/gateway/state_machine.jpeg
--------------------------------------------------------------------------------
/doc/gateway/springcloud_gateway工作原理图.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mingyang66/EmilyGateway/HEAD/doc/gateway/springcloud_gateway工作原理图.png
--------------------------------------------------------------------------------
/doc/证书生成文档:
--------------------------------------------------------------------------------
1 | keytool -genkey -alias emily -keypass 123456 -keyalg RSA -keysize 1024 -validity 3650 -keystore D:\emily.p12 -deststoretype pkcs12 -storepass 123456
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar
3 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/enums/TraceType.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common.enums;
2 |
3 | /**
4 | * @author Emily
5 | * @program: EmilyGateway
6 | * @description: 日志类型
7 | * @create: 2021/01/15
8 | */
9 | public enum TraceType {
10 | INFO, ERROR;
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/GatewayBootstrap.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | /**
7 | * @author Emily
8 | */
9 | @SpringBootApplication
10 | public class GatewayBootstrap {
11 | public static void main(String[] args) {
12 | SpringApplication.run(GatewayBootstrap.class, args);
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/filter/order/GatewayFilterOrdered.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.filter.order;
2 |
3 | import org.springframework.cloud.gateway.filter.GatewayFilter;
4 | import org.springframework.core.Ordered;
5 |
6 | /**
7 | * @Description : 可排序过滤器
8 | * @Author : Emily
9 | * @CreateDate : Created in 2022/7/11 11:02 上午
10 | */
11 | public interface GatewayFilterOrdered extends GatewayFilter, Ordered {
12 | }
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # 定制镜像所需的基础镜像
2 | # FROM openjdk:8-jdk-alpine
3 | FROM adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.9_11
4 | # 用于执行后面跟着的命令行命令
5 | RUN echo 'JDK11 Images Download Success'
6 | # 作者
7 | MAINTAINER Emily
8 | # 工作目录路径
9 | WORKDIR /app
10 | # 构建参数
11 | ARG JAR_FILE=target/*.jar
12 | # 复制指令,从上下文目录中复制文件或目录到容器里指定路径
13 | COPY ${JAR_FILE} emilygateway.jar
14 | # 运行程序指令 @link{reactor.netty.resources.ConnectionProvider}
15 | ENTRYPOINT ["java", "-Dreactor.netty.pool.leasingStrategy=lifo","-Dreactor.netty.http.server.accessLogEnabled=true","-jar","emilygateway.jar"]
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | /target/
3 | /**/target/
4 | /**/classes/
5 | !.mvn/wrapper/maven-wrapper.jar
6 |
7 |
8 | ### STS ###
9 | .apt_generated
10 | .classpath
11 | .factorypath
12 | .project
13 | .settings
14 | .springBeans
15 | .sts4-cache
16 |
17 | ### IntelliJ IDEA ###
18 | .idea
19 | *.iws
20 | *.iml
21 | *.ipr
22 |
23 | ### NetBeans ###
24 | /nbproject/private/
25 | /nbbuild/
26 | /dist/
27 | /nbdist/
28 | /.nb-gradle/
29 | /build/
30 |
31 | ### VS Code ###
32 | .vscode/
33 |
34 | #.mvn/
35 | #mvnw
36 | #mvnw.cmd
37 | logs/
38 |
39 | out/
40 | .DS_Store
41 | venv/
42 | /src/main/resources/application-gateway_rabbitmq.yml
43 |
--------------------------------------------------------------------------------
/src/main/resources/config-bak/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | access_log.log
5 |
6 | %msg%n
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/predicate/PredicateFactoryAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.predicate;
2 |
3 | import com.emily.infrastructure.gateway.config.predicate.path.PathOffRoutePredicateFactory;
4 | import org.springframework.context.annotation.Bean;
5 | import org.springframework.context.annotation.Configuration;
6 |
7 | /**
8 | * @Description : 断言工厂配置类
9 | * @Author : Emily
10 | * @CreateDate : Created in 2022/7/7 10:38 上午
11 | */
12 | @Configuration
13 | public class PredicateFactoryAutoConfiguration {
14 |
15 | @Bean
16 | public PathOffRoutePredicateFactory pathOffRoutePredicateFactory() {
17 | return new PathOffRoutePredicateFactory();
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/doc/gateway/SpringCloud Gateway如何设置keep-alive.md:
--------------------------------------------------------------------------------
1 | ### SpringCloud Gateway如何设置keep-alive
2 |
3 | ##### 一、Http中的keep-alive保活设置
4 |
5 | - 短连接:请求-响应模式中发起请求时建立连接,响应后直接断开连接;
6 | - 长连接:第一次发起请求后连接不断开,接下来的请求可以复用这个连接;
7 | - HTTP1.0:当前协议版本中keep-alive默认是关闭的,需要在请求头中添加"Connection:Keep-Alive",才能启用Keep-Alive;
8 | - Http1.1:当前协议浏览器发起的默认都是Keep-Alive连接请求,默认的时间可以有客户端设置,也可以由服务端设置;
9 |
10 | ##### 二、SpringCloud Gateway保活机制超时时间
11 |
12 | > 最近上了网关,有一个高并发的接口,导致通过监控发现客户端长期持有大量的连接不释放,严重影响到了网关的TPS,跟前端的同事了解Android、IOS端的Keep-Alive时间是30S,所以就想通过服务端来控制保活时长;
13 |
14 | ```properties
15 | # 网络通道(channel)连接超时时间
16 | server.netty.connection-timeout=PT10S
17 | # 连接等待时间(毫秒),超时会被自动关闭。为空表示永远不关闭,全靠请求方
18 | server.netty.idle-timeout=PT10S
19 | ```
20 |
21 | 通过idle-timeout属性配置更改超时时间为10S,通过监控观察连接数直接降低为了原来的三分之一。
22 |
23 |
24 |
25 | GitHub地址:[https://github.com/mingyang66/EmilyGateway](https://github.com/mingyang66/EmilyGateway)
--------------------------------------------------------------------------------
/doc/gateway/gateway过滤器工厂.md:
--------------------------------------------------------------------------------
1 | #### 一、CircuitBreaker过滤器工厂
2 |
3 | 开启Spring Cloud CircuitBreaker过滤器,需在maven依赖配置中引入如下配置:
4 |
5 | ```pom
6 |
7 | org.springframework.cloud
8 | spring-cloud-starter-circuitbreaker-reactor-resilience4j
9 | 2.1.1
10 |
11 | ```
12 |
13 | springcloud gateway中配置断路器,示例如下:
14 |
15 | ```yaml
16 | spring:
17 | cloud:
18 | gateway:
19 | routes:
20 | - id: circuitbreaker_route
21 | uri: lb://backing-service:8088
22 | predicates:
23 | - Path=/consumingServiceEndpoint
24 | filters:
25 | - name: CircuitBreaker
26 | args:
27 | name: myCircuitBreaker
28 | fallbackUri: forward:/inCaseOfFailureUseThis
29 | ```
30 |
31 | > 使用fallbackUri在网关应用程序中定义内部控制器,
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/exception/WebFluxExceptionProperties.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.exception;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 |
5 | /**
6 | * @Description : 网关异常处理配置类
7 | * @Author : Emily
8 | * @CreateDate : Created in 2022/4/26 3:29 下午
9 | */
10 | @ConfigurationProperties(prefix = WebFluxExceptionProperties.PREFIX)
11 | public class WebFluxExceptionProperties {
12 | /**
13 | * 默认前缀
14 | */
15 | public static final String PREFIX = "spring.emily.gateway.exception";
16 | /**
17 | * 网关全局处理开关,默认:true
18 | */
19 | private boolean enabled = true;
20 |
21 | public boolean isEnabled() {
22 | return enabled;
23 | }
24 |
25 | public void setEnabled(boolean enabled) {
26 | this.enabled = enabled;
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/enums/HttpStatusType.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common.enums;
2 |
3 | /**
4 | * @author Emily
5 | * @Description: 自定义状态码异常枚举类
6 | * @Version: 1.0
7 | */
8 | public enum HttpStatusType {
9 | OK(0, "SUCCESS"),
10 | EXCEPTION(100000, "网络异常,请稍后再试"),
11 | ILLEGAL_METHOD(100001, "非法方法请求"),
12 | ILLEGAL_ARGUMENT(100002, "非法参数"),
13 | ILLEGAL_DATA(100003, "非法数据"),
14 | ILLEGAL_ACCESS(100004, "非法访问"),
15 | ILLEGAL_PROXY(100005, "非法代理");
16 |
17 | /**
18 | * 状态码
19 | */
20 | private int status;
21 | /**
22 | * 描述字段
23 | */
24 | private String message;
25 |
26 | HttpStatusType(int status, String message) {
27 | this.status = status;
28 | this.message = message;
29 | }
30 |
31 | public int getStatus() {
32 | return status;
33 | }
34 |
35 | public String getMessage() {
36 | return message;
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/main/resources/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Title
6 |
7 |
28 |
36 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/filter/GatewayFilterAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.filter;
2 |
3 | import com.emily.infrastructure.gateway.config.filter.dedupe.DedupeLoginGatewayFilterFactory;
4 | import com.emily.infrastructure.gateway.config.filter.logger.RecordLoggerGatewayFilterFactory;
5 | import org.springframework.context.annotation.Bean;
6 | import org.springframework.context.annotation.Configuration;
7 |
8 | /**
9 | * @Description : 过滤器配置类
10 | * @Author : Emily
11 | * @CreateDate : Created in 2022/7/11 11:09 上午
12 | */
13 | @Configuration
14 | public class GatewayFilterAutoConfiguration {
15 | /**
16 | * 剔除所有访问接口登录状态
17 | *
18 | * @return
19 | */
20 | @Bean
21 | public DedupeLoginGatewayFilterFactory dedupeLoginRoutePredicateFactory() {
22 | return new DedupeLoginGatewayFilterFactory();
23 | }
24 |
25 | /**
26 | * 注册请求响应日志拦截全局过滤器
27 | */
28 | @Bean
29 | public RecordLoggerGatewayFilterFactory recordLoggerGatewayFilterFactory() {
30 | return new RecordLoggerGatewayFilterFactory();
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/circuitbreaker/CircuitBreakerProperties.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.circuitbreaker;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 |
5 | /**
6 | * @Description : 断路器配置类
7 | * @Author : Emily
8 | * @CreateDate : Created in 2022/4/26 3:55 下午
9 | */
10 | @ConfigurationProperties(prefix = CircuitBreakerProperties.PREFIX)
11 | public class CircuitBreakerProperties {
12 | /**
13 | * 前缀
14 | */
15 | public static final String PREFIX = "spring.emily.gateway.circuit-breaker";
16 | /**
17 | * 断路器配置类开关,默认:true
18 | */
19 | private boolean enabled = true;
20 | /**
21 | * 超时时间,默认:1s
22 | */
23 | private long timeout = 1;
24 |
25 | public boolean isEnabled() {
26 | return enabled;
27 | }
28 |
29 | public void setEnabled(boolean enabled) {
30 | this.enabled = enabled;
31 | }
32 |
33 | public long getTimeout() {
34 | return timeout;
35 | }
36 |
37 | public void setTimeout(long timeout) {
38 | this.timeout = timeout;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/RecordLogger.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common;
2 |
3 | import com.emily.infrastructure.gateway.common.entity.BaseLogger;
4 | import com.emily.infrastructure.json.JsonUtils;
5 | import com.google.common.collect.Maps;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.web.reactive.function.server.ServerRequest;
9 |
10 | import java.util.Map;
11 |
12 | /**
13 | * @Description : 记录日志
14 | * @Author : Emily
15 | * @CreateDate : Created in 2022/7/5 7:58 下午
16 | */
17 | public class RecordLogger {
18 | private static final Logger logger = LoggerFactory.getLogger("userActionLogger");
19 |
20 | public static void recordUser(ServerRequest request, Map attributes, String status, String message){
21 | BaseLogger userAction = new BaseLogger();
22 | userAction.setUrl(request.path());
23 | userAction.setResponseBody(attributes);
24 | Map inParams = Maps.newHashMap();
25 | inParams.put("headers", request.headers().asHttpHeaders());
26 | userAction.setRequestParams(inParams);
27 | logger.info(JsonUtils.toJSONString(userAction));
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/utils/FilterUtils.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common.utils;
2 |
3 | import com.emily.infrastructure.gateway.common.enums.HttpStatusType;
4 | import com.emily.infrastructure.json.JsonUtils;
5 | import com.google.common.collect.Maps;
6 | import org.springframework.core.io.buffer.DataBuffer;
7 | import org.springframework.http.server.reactive.ServerHttpResponse;
8 | import org.springframework.web.server.ServerWebExchange;
9 |
10 | import java.util.Map;
11 |
12 | /**
13 | * @Description : 过滤器工具类
14 | * @Author : Emily
15 | * @CreateDate : Created in 2022/7/9 10:38 上午
16 | */
17 | public class FilterUtils {
18 |
19 | /**
20 | * 获取异常返回数据
21 | *
22 | * @param exchange
23 | * @return
24 | */
25 | public static DataBuffer getResponseData(ServerWebExchange exchange) {
26 | ServerHttpResponse response = exchange.getResponse();
27 | Map dataMap = Maps.newLinkedHashMap();
28 | dataMap.put("status", HttpStatusType.ILLEGAL_ACCESS.getStatus());
29 | dataMap.put("message", HttpStatusType.ILLEGAL_ACCESS.getMessage());
30 | return response.bufferFactory().wrap(JsonUtils.toByteArray(dataMap));
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/doc/定制java11镜像.md:
--------------------------------------------------------------------------------
1 | ### 定制java11容器镜像
2 |
3 | ##### 1.获取java11镜像
4 |
5 | ```properties
6 | docker pull adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.9_11
7 | ```
8 |
9 | ##### 2.创建容器
10 |
11 | ```properties
12 | docker run -itd --name java11 -p 9000:9000 adoptopenjdk/openjdk11:x86_64-alpine-jre-11.0.9_11
13 | ```
14 |
15 | ##### 3.进入容器
16 |
17 | ```properties
18 | docker exec -it sh
19 | ```
20 |
21 | ##### 4.修改时区,解决差8个小时问题
22 |
23 | ```properties
24 | docker cp /usr/share/zoneinfo/Asia/Shanghai :/etc/localtime
25 | ```
26 |
27 | ```properties
28 | docker cp /usr/share/zoneinfo/Asia/Shanghai :/etc/timezone
29 | ```
30 |
31 | ##### 5.解决图片空指针,缺少字体库问题
32 |
33 | ```properties
34 | apk add fontconfig
35 | apk add --update ttf-dejavu
36 | fc-cache --force
37 | ```
38 |
39 | ##### 6.打包镜像
40 |
41 | ```properties
42 | docker commit -a='emily' -m 'desc' x.x.x.x/emily/java/openjdk11:alpine
43 | ```
44 |
45 | OPTIONS说明:
46 | -a :提交的镜像作者;
47 | -c :使用Dockerfile指令来创建镜像;
48 | -m :提交时的说明文字;
49 | -p :在commit时,将容器暂停。
50 |
51 | ##### 7.将镜像到出到本地
52 |
53 | ```properties
54 | docker save -o java11.tar x.x.x.x/emily/java/openjdk11:alpine
55 | ```
56 |
57 | ##### 8.服务器加载镜像到docker容器
58 |
59 | ```properties
60 | docker load --input java11.tar
61 | ```
62 |
63 | ##### 9.将docker镜像推送到容器仓库
64 |
65 | ```properties
66 | docker push x.x.x.x/emily/java/openjdk11
67 | ```
68 |
69 |
--------------------------------------------------------------------------------
/src/main/resources/config-bak/application-server.properties:
--------------------------------------------------------------------------------
1 | server.port=443
2 | # 优雅停机
3 | server.shutdown=graceful
4 | # 是否启用SSL支持
5 | server.ssl.enabled=true
6 | # 标识秘钥存储中秘钥的别名
7 | server.ssl.key-alias=emily
8 | # 访问秘钥存储的密码
9 | server.ssl.key-store-password=123456
10 | # 指定保存SSL证书的秘钥存储的路径(通常是jks文件)
11 | server.ssl.key-store=classpath:emily.p12
12 | # 秘钥存储的类型
13 | server.ssl.key-store-type=PKCS12
14 | # 要使用的SSL协议
15 | server.ssl.protocol=TLS
16 |
17 | # 是否开启http端口号
18 | server.http.enabled=true
19 | # http端口号
20 | server.http.port=80
21 | # 优雅停机
22 | server.http.shutdown=graceful
23 | # http请求是否自动跳转到https,默认:true
24 | server.http.http-to-https=true
25 |
26 | # 网络通道(channel)连接超时时间
27 | server.netty.connection-timeout=PT10S
28 | # h2c(即:http/2协议的明文版本)升级请求的最大内容长度,默认:0B
29 | server.netty.h2c-max-content-length=0B
30 | # http请求解码的初始缓冲区大小,默认:128B
31 | server.netty.initial-buffer-size=128B
32 | # HTTP请求可解码的最大块大小,默认:8KB
33 | server.netty.max-chunk-size=8KB
34 | # http请求的初始行可以解码的最大长度
35 | server.netty.max-initial-line-length=4KB
36 | # 解码请求时是否验证标头,默认:true
37 | server.netty.validate-headers=true
38 |
39 | #健康检查端口号
40 | management.server.port=7443
41 | # 是否开启SSL支持,默认:true
42 | management.server.ssl.enabled=true
43 | # 标识秘钥存储中秘钥的别名
44 | management.server.ssl.key-alias=emily
45 | #访问秘钥存储的密码
46 | management.server.ssl.key-store-password=123456
47 | # 指定保存SSL证书的秘钥存储的路径(通常是jks文件)
48 | management.server.ssl.key-store=classpath:emily.p12
49 | # 秘钥存储的类型
50 | management.server.ssl.key-store-type=PKCS12
51 | # 要使用的SSL协议
52 | management.server.ssl.protocol=TLS
53 | # 开启网关端点监控,默认:true
54 | management.endpoint.gateway.enabled=true
55 | # 是否打开网关端点健康
56 | management.endpoints.web.exposure.include=gateway,health
57 |
58 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/server/NettyWebServerProperties.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.server;
2 |
3 | import org.springframework.boot.context.properties.ConfigurationProperties;
4 | import org.springframework.boot.web.server.Shutdown;
5 |
6 | /**
7 | * @author Emily
8 | * @program: EmilyGateway
9 | * @description: Netty服务器配置
10 | * @create: 2021/01/13
11 | */
12 | @ConfigurationProperties(prefix = NettyWebServerProperties.PREFIX)
13 | public class NettyWebServerProperties {
14 | /**
15 | * 默认前缀
16 | */
17 | public static final String PREFIX = "server.http";
18 | /**
19 | * 是否开启http端口号
20 | */
21 | private boolean enabled;
22 | /**
23 | * http端口号
24 | */
25 | private int port = 8080;
26 | /**
27 | * 关闭模式
28 | */
29 | private Shutdown shutdown = Shutdown.IMMEDIATE;
30 | /**
31 | * http请求是否自动跳转到https
32 | */
33 | private boolean httpToHttps = true;
34 |
35 | public boolean isEnabled() {
36 | return enabled;
37 | }
38 |
39 | public void setEnabled(boolean enabled) {
40 | this.enabled = enabled;
41 | }
42 |
43 | public int getPort() {
44 | return port;
45 | }
46 |
47 | public void setPort(int port) {
48 | this.port = port;
49 | }
50 |
51 | public Shutdown getShutdown() {
52 | return shutdown;
53 | }
54 |
55 | public void setShutdown(Shutdown shutdown) {
56 | this.shutdown = shutdown;
57 | }
58 |
59 | public boolean isHttpToHttps() {
60 | return httpToHttps;
61 | }
62 |
63 | public void setHttpToHttps(boolean httpToHttps) {
64 | this.httpToHttps = httpToHttps;
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/circuitbreaker/controller/CircuitBreakerController.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.circuitbreaker.controller;
2 |
3 | import com.emily.infrastructure.gateway.common.entity.BaseResponse;
4 | import org.slf4j.Logger;
5 | import org.slf4j.LoggerFactory;
6 | import org.springframework.cloud.gateway.support.ServerWebExchangeUtils;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.web.bind.annotation.RequestMapping;
9 | import org.springframework.web.bind.annotation.RequestMethod;
10 | import org.springframework.web.bind.annotation.RestController;
11 | import org.springframework.web.server.ServerWebExchange;
12 | import org.springframework.web.server.ServerWebExchangeDecorator;
13 |
14 | /**
15 | * @Description : 断路器控制器
16 | * @Author : Emily
17 | * @CreateDate : Created in 2022/4/21 9:54 上午
18 | */
19 | @RestController
20 | @RequestMapping("circuitBreaker")
21 | public class CircuitBreakerController {
22 |
23 | private static final Logger logger = LoggerFactory.getLogger(CircuitBreakerController.class);
24 |
25 | @RequestMapping(value = "fallback", method = {RequestMethod.POST, RequestMethod.GET, RequestMethod.OPTIONS, RequestMethod.PUT, RequestMethod.DELETE})
26 | public BaseResponse fallback(ServerWebExchange exchange) {
27 | Throwable throwable = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
28 | ServerWebExchange delegate = ((ServerWebExchangeDecorator) exchange).getDelegate();
29 | logger.error("服务调用失败,URL={}", delegate.getRequest().getURI(), throwable);
30 | return BaseResponse.build(HttpStatus.SERVICE_UNAVAILABLE.value(), HttpStatus.SERVICE_UNAVAILABLE.getReasonPhrase());
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/doc/Connection prematurely closed BEFORE response.md:
--------------------------------------------------------------------------------
1 | Connection prematurely closed BEFORE response
2 | reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response异常解决
3 |
4 | ##### 一、最近在开发网关系统,就在感觉万事大吉可以上线的时候发现了如下的错误(这个是我在配置rabbitmq访问多个服务时发现的)
5 |
6 | ```java
7 | Connection prematurely closed BEFORE response
8 | reactor.netty.http.client.PrematureCloseException: Connection prematurely closed BEFORE response
9 | reactor.core.publisher.FluxOnAssembly$OnAssemblyException:
10 | Error has been observed at the following site(s):
11 | |_ checkpoint ⇢ org.springframework.cloud.gateway.filter.WeightCalculatorWebFilter [DefaultWebFilterChain]
12 | |_ checkpoint ⇢ org.springframework.boot.actuate.metrics.web.reactive.server.MetricsWebFilter [DefaultWebFilterChain]
13 | |_ checkpoint ⇢ HTTP GET "/rabbitmq/api/vhosts" [ExceptionHandlingWebHandler]
14 | ```
15 |
16 | 什么意思呢?就是在请求还未响应的时候连接直接断开了,什么情况,崩溃,但是问题还是要解决于是开始了度娘之路,来看下如下这张图:
17 |
18 | 
19 |
20 | 由上图可知发生异常的原因是从连接池中拿到连接之后发送请求,请求还未到达目标服务器就已经被目标服务器关闭了;而SCG中的连接还未被回收掉;
21 |
22 | ##### 二、解决方案
23 |
24 | 第一步添加JVM参数,更改从连接池中取连接的策略,由FIFO变更为LIFO(reactor.netty.resources.ConnectionProvider),确保拿到的连接永远是最新的连接;
25 |
26 | ```properties
27 | -Dreactor.netty.pool.leasingStrategy=lifo
28 | ```
29 |
30 | 第二步设置连接空闲多久后会被回收掉,这个时间要比对应服务的回收时间小(tomcat对应的是server.tomcat.connection-timeout属性配置,在浏览器中看到的就是keep-Alive),这样就可以确保SCG回收连接在后端服务之前进行,完美避开这个问题;
31 |
32 | ```properties
33 | spring:
34 | cloud:
35 | gateway:
36 | httpclient:
37 | pool:
38 | maxIdleTime: PT1S
39 | ```
40 |
41 | GitHub地址:[https://github.com/mingyang66/EmilyGateway/tree/main/doc](https://github.com/mingyang66/EmilyGateway/tree/main/doc)
42 |
43 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/entity/BaseResponse.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common.entity;
2 |
3 | import java.io.Serializable;
4 |
5 | /**
6 | * @author Emily
7 | * @Description: 控制器返回结果
8 | * @ProjectName: spring-parent
9 | * @Date: 2019/7/1 15:33
10 | * @Version: 1.0
11 | */
12 | public class BaseResponse implements Serializable {
13 | private int status;
14 | private String message;
15 | private T data;
16 | private long time;
17 |
18 | public BaseResponse() {
19 | super();
20 | }
21 |
22 | public BaseResponse(int status, String message) {
23 | this.status = status;
24 | this.message = message;
25 | }
26 |
27 | public BaseResponse(int status, String message, T data) {
28 | this.status = status;
29 | this.message = message;
30 | this.data = data;
31 | }
32 |
33 | public BaseResponse(int status, String message, T data, long time) {
34 | this.status = status;
35 | this.message = message;
36 | this.data = data;
37 | this.time = time;
38 | }
39 |
40 |
41 | public int getStatus() {
42 | return status;
43 | }
44 |
45 | public void setStatus(int status) {
46 | this.status = status;
47 | }
48 |
49 | public String getMessage() {
50 | return message;
51 | }
52 |
53 | public void setMessage(String message) {
54 | this.message = message;
55 | }
56 |
57 | public T getData() {
58 | return data;
59 | }
60 |
61 | public void setData(T data) {
62 | this.data = data;
63 | }
64 |
65 | public long getTime() {
66 | return time;
67 | }
68 |
69 | public void setTime(long time) {
70 | this.time = time;
71 | }
72 |
73 | public static BaseResponse build(int status, String message) {
74 | BaseResponse baseResponse = new BaseResponse();
75 | baseResponse.setStatus(status);
76 | baseResponse.setMessage(message);
77 | return baseResponse;
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/main/resources/bootstrap.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | application:
3 | name: @project.name@
4 | cloud:
5 | consul:
6 | config:
7 | # 配置中心开关
8 | enabled: false
9 | # 前缀,默认:config
10 | prefixes: @project.name@
11 | # 默认上下文,默认:application
12 | defaultContext: @project.version@
13 | # 监视配置中心数据更改监视器配置
14 | watch:
15 | # 是否开启配置中心配置变动,默认:true
16 | enabled: true
17 | # 监控的固定延迟值,即一次执行完成到下一次执行开始之间的间隔,默认:1000毫秒
18 | delay: 1000
19 | # 等待(或阻塞)监视查询的秒数,默认:55秒,需要小于60
20 | wait-time: 55
21 | # 配置文件格式
22 | format: yaml
23 | # consul配置key
24 | data-key: data
25 | # 分割符,默认:","
26 | profile-separator: '/'
27 | # ACL Token
28 | acl-token: ${TOKEN}
29 | discovery:
30 | # 将服务注册到consul中,默认:true
31 | register: true
32 | # 注册协议为HTTP或HTTPS,默认:http
33 | scheme: https
34 | # 注册服务时使用的tag
35 | tags: emily=gateway
36 | # 服务名称
37 | service-name: ${spring.application.name}
38 | # 实例ID(唯一标识),默认是:${spring.application.name}:comma,separated,profiles:${server.port}
39 | instance-id: ${spring.application.name}:${random.value}
40 | # 表示注册服务时使用IP而不是hostname,默认:false
41 | prefer-ip-address: true
42 | # 访问服务时要使用的IP地址(还必须设置要使用的prefer-ip-address为true)--使用docker时设置
43 | ip-address: ${IP}
44 | # 注册服务的端口(默认为监听端口)--使用docker时设置
45 | port: ${PORT}
46 | # ACL Token
47 | acl-token: ${TOKEN}
48 | # actuator监控服务后缀
49 | management-suffix: management
50 | # 注册监控服务时的tag
51 | management-tags: emily=gateway-management
52 | # 注册actuator监控服务端口(默认是:management port)--使用docker时设置
53 | management-port: ${MANAGEMENT_PORT}
54 | # 注册健康检查到consul,在服务开发过程中有用
55 | register-health-check: true
56 | # 健康检查路径,默认:/actuator/health
57 | health-check-path: /actuator/health
58 | # 健康检查超时时间,默认:10s
59 | health-check-timeout: 10s
60 | # 健康检查失败多长时间后取消注册,默认null(需要consul版本大于7.x)
61 | health-check-critical-timeout: 60s
62 | # 如果为true,则在服务检查期间跳过证书验证,否则运行证书验证
63 | health-check-tls-skip-verify: true
64 | # 监视consul配置文件时间间隔,即一次执行结束到下一次执行开始之间的时间间隔,默认:1000毫秒
65 | catalog-services-watch-delay: 1000
66 | # 监视consul配置文件的超时时间,默认:2秒
67 | catalog-services-watch-timeout: 2
68 | # consul agent主机名,默认:localhost
69 | host: ${IP}
70 | # consul agent端口号,默认:8500
71 | port: 8500
72 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/entity/BaseLogger.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common.entity;
2 |
3 | import com.emily.infrastructure.gateway.common.ServerRequestUtils;
4 | import org.springframework.http.server.reactive.ServerHttpRequest;
5 | import org.springframework.web.server.ServerWebExchange;
6 |
7 | import java.io.Serializable;
8 | import java.util.UUID;
9 |
10 | /**
11 | * @author Emily
12 | * @program: EmilyGateway
13 | * @description: 日志实体类
14 | * @create: 2021/01/15
15 | */
16 | public class BaseLogger implements Serializable {
17 | /**
18 | * 唯一标识
19 | */
20 | private String traceId = UUID.randomUUID().toString();
21 | /**
22 | * 请求方法
23 | */
24 | private String method;
25 | /**
26 | * 请求URL
27 | */
28 | private String url;
29 | /**
30 | * 请求参数
31 | */
32 | private Object requestParams;
33 | /**
34 | * 响应数据
35 | */
36 | private Object responseBody;
37 | /**
38 | * 请求时间
39 | */
40 | private long time;
41 |
42 | public BaseLogger() {
43 | }
44 |
45 | public BaseLogger(ServerWebExchange exchange) {
46 | ServerHttpRequest request = exchange.getRequest();
47 | this.method = ServerRequestUtils.getMethod(request);
48 | this.requestParams = ServerRequestUtils.getRequestParams(exchange);
49 | this.url = ServerRequestUtils.getUrl(exchange);
50 | }
51 |
52 | public String getTraceId() {
53 | return traceId;
54 | }
55 |
56 | public void setTraceId(String traceId) {
57 | this.traceId = traceId;
58 | }
59 |
60 | public String getMethod() {
61 | return method;
62 | }
63 |
64 | public void setMethod(String method) {
65 | this.method = method;
66 | }
67 |
68 | public String getUrl() {
69 | return url;
70 | }
71 |
72 | public void setUrl(String url) {
73 | this.url = url;
74 | }
75 |
76 | public Object getRequestParams() {
77 | return requestParams;
78 | }
79 |
80 | public void setRequestParams(Object requestParams) {
81 | this.requestParams = requestParams;
82 | }
83 |
84 | public Object getResponseBody() {
85 | return responseBody;
86 | }
87 |
88 | public void setResponseBody(Object responseBody) {
89 | this.responseBody = responseBody;
90 | }
91 |
92 | public long getTime() {
93 | return time;
94 | }
95 |
96 | public void setTime(long time) {
97 | this.time = time;
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/doc/springboot2.4开启https异常.md:
--------------------------------------------------------------------------------
1 | springboot2.4开启HTTPS功能报DerInputStream.getLength(): lengthTag=111, too big异常
2 |
3 | ##### 1.生成证书
4 |
5 | ```properties
6 | keytool -genkey -alias emily -keypass 123456 -keyalg RSA -keysize 1024 -validity 3650 -keystore D:\emily.p12 -deststoretype pkcs12 -storepass 123456
7 | ```
8 |
9 | ##### 2.将emily.p12证书放到项目resources目录下,在yml配置文件中添加配置
10 |
11 | ```yml
12 | server:
13 | port: 443
14 | # 优雅停机
15 | shutdown: graceful
16 | ssl:
17 | # 是否启用SSL支持
18 | enabled: true
19 | # 标识秘钥存储中秘钥的别名
20 | key-alias: emily
21 | # 访问秘钥存储的密码
22 | key-store-password: 123456
23 | # 指定保存SSL证书的秘钥存储的路径(通常是jks文件)
24 | key-store: classpath:emily.p12
25 | # 秘钥存储的类型
26 | key-store-type: PKCS12
27 | # 要使用的SSL协议
28 | protocol: TLS
29 | ```
30 |
31 | ##### 3.上述配置好之后按理启动服务就可以开启https能力,但是不幸报错了
32 |
33 | ```java
34 | Caused by: java.io.IOException: DerInputStream.getLength(): lengthTag=111, too big.
35 | at sun.security.util.DerInputStream.getLength(DerInputStream.java:599) ~[na:1.8.0_181]
36 | at sun.security.util.DerValue.init(DerValue.java:391) ~[na:1.8.0_181]
37 | at sun.security.util.DerValue.(DerValue.java:332) ~[na:1.8.0_181]
38 | at sun.security.util.DerValue.(DerValue.java:345) ~[na:1.8.0_181]
39 | at sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:1938) ~[na:1.8.0_181]
40 | at java.security.KeyStore.load(KeyStore.java:1445) ~[na:1.8.0_181]
41 | at org.springframework.boot.web.embedded.netty.SslServerCustomizer.loadStore(SslServerCustomizer.java:173) ~[spring-boot-2.4.1.jar:2.4.1]
42 | ... 21 common frames omitted
43 | ```
44 |
45 | > 看到上面的错误有点懵逼,秘钥生成的没问题的,之前已经测试过了,然后就开始度娘,最后添加一个maven插件解决;
46 |
47 | ```xml
48 |
49 | maven-resources-plugin
50 | 3.2.0
51 |
52 | utf-8
53 |
54 | true
55 |
56 |
57 | p12
58 | cer
59 | pem
60 | pfx
61 | jkx
62 |
63 |
64 |
65 | ```
66 |
67 | 这个插件的作用是在maven编译打包项目的时候忽略指定后缀的文件,秘钥如果不忽略就会被编译,编译后就会出问题;
68 |
69 | GitHub地址:[https://github.com/mingyang66/EmilyGateway](https://github.com/mingyang66/EmilyGateway)
70 |
71 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/predicate/path/PathOffRoutePredicateFactory.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.predicate.path;
2 |
3 | import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
4 | import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
5 | import org.springframework.util.AntPathMatcher;
6 | import org.springframework.validation.annotation.Validated;
7 | import org.springframework.web.server.ServerWebExchange;
8 |
9 | import java.util.ArrayList;
10 | import java.util.Arrays;
11 | import java.util.List;
12 | import java.util.function.Predicate;
13 |
14 | /**
15 | * @Description : 路由关闭断言
16 | * @Author : Emily
17 | * @CreateDate : Created in 2022/7/7 10:30 上午
18 | */
19 | public class PathOffRoutePredicateFactory extends AbstractRoutePredicateFactory {
20 |
21 | private static final String MATCH_TRAILING_SLASH = "matchTrailingSlash";
22 |
23 | private AntPathMatcher matcher = new AntPathMatcher();
24 |
25 | public PathOffRoutePredicateFactory() {
26 | super(Config.class);
27 | }
28 |
29 | @Override
30 | public List shortcutFieldOrder() {
31 | return Arrays.asList("patterns", MATCH_TRAILING_SLASH);
32 | }
33 |
34 | @Override
35 | public ShortcutType shortcutType() {
36 | return ShortcutType.GATHER_LIST_TAIL_FLAG;
37 | }
38 |
39 | @Override
40 | public Predicate apply(Config config) {
41 | final ArrayList pathPatterns = new ArrayList<>();
42 | config.getPatterns().forEach(pattern -> {
43 | pathPatterns.add(pattern);
44 | });
45 |
46 | return (GatewayPredicate) exchange -> {
47 | String path = exchange.getRequest().getURI().getRawPath();
48 | boolean match = false;
49 | for (int i = 0; i < pathPatterns.size(); i++) {
50 | if (matcher.match(pathPatterns.get(i), path)) {
51 | match = true;
52 | break;
53 | }
54 | }
55 | if (match) {
56 | return false;
57 | }
58 | return true;
59 | };
60 | }
61 |
62 | @Validated
63 | public static class Config {
64 |
65 | private List patterns = new ArrayList<>();
66 |
67 | private boolean matchTrailingSlash = true;
68 |
69 | public List getPatterns() {
70 | return patterns;
71 | }
72 |
73 | public Config setPatterns(List patterns) {
74 | this.patterns = patterns;
75 | return this;
76 | }
77 |
78 | public boolean isMatchTrailingSlash() {
79 | return matchTrailingSlash;
80 | }
81 |
82 | public void setMatchTrailingSlash(boolean matchTrailingSlash) {
83 | this.matchTrailingSlash = matchTrailingSlash;
84 | }
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/server/NettyWebServerAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.server;
2 |
3 | import com.emily.infrastructure.common.StringUtils;
4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
5 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
6 | import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
7 | import org.springframework.boot.web.server.WebServer;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.http.HttpStatus;
10 | import org.springframework.http.server.reactive.HttpHandler;
11 | import reactor.core.publisher.Mono;
12 |
13 | import javax.annotation.PostConstruct;
14 | import javax.annotation.PreDestroy;
15 | import java.net.URI;
16 | import java.net.URISyntaxException;
17 | import java.util.Objects;
18 |
19 | /**
20 | * @author Emily
21 | * @program: EmilyGateway
22 | * @description: 开启http端口
23 | * @create: 2021/01/13
24 | */
25 | @Configuration(proxyBeanMethods = false)
26 | @EnableConfigurationProperties(NettyWebServerProperties.class)
27 | @ConditionalOnProperty(prefix = NettyWebServerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = false)
28 | public class NettyWebServerAutoConfiguration {
29 |
30 | private HttpHandler httpHandler;
31 | private NettyWebServerProperties nettyWebServerProperties;
32 | private WebServer webServer;
33 |
34 | public NettyWebServerAutoConfiguration(HttpHandler httpHandler, NettyWebServerProperties nettyWebServerProperties) {
35 | this.httpHandler = httpHandler;
36 | this.nettyWebServerProperties = nettyWebServerProperties;
37 | }
38 |
39 | @PostConstruct
40 | public void start() {
41 | NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory(nettyWebServerProperties.getPort());
42 | //设置关机模式
43 | factory.setShutdown(nettyWebServerProperties.getShutdown());
44 | if (nettyWebServerProperties.isHttpToHttps()) {
45 | webServer = factory.getWebServer(((request, response) -> {
46 | URI uri = request.getURI();
47 | try {
48 | response.getHeaders().setLocation(new URI(StringUtils.replace(uri.toString(), "http", "https")));
49 | } catch (URISyntaxException e) {
50 | return Mono.error(e);
51 | }
52 | //https://tools.ietf.org/html/rfc7231#section-6.4.2
53 | response.setStatusCode(HttpStatus.MOVED_PERMANENTLY);
54 | return response.setComplete();
55 | }));
56 | } else {
57 | webServer = factory.getWebServer(httpHandler);
58 | }
59 | webServer.start();
60 | }
61 |
62 | @PreDestroy
63 | public void stop() {
64 | if (Objects.nonNull(webServer)) {
65 | webServer.stop();
66 | }
67 | }
68 |
69 | }
70 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/DataBufferUtils.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common;
2 |
3 | import com.emily.infrastructure.common.StringUtils;
4 | import io.netty.buffer.ByteBufAllocator;
5 | import org.apache.commons.lang3.ArrayUtils;
6 | import org.springframework.core.io.buffer.DataBuffer;
7 | import org.springframework.core.io.buffer.NettyDataBufferFactory;
8 | import reactor.core.publisher.Flux;
9 |
10 | import java.nio.charset.StandardCharsets;
11 | import java.util.List;
12 |
13 | import static java.util.stream.Collectors.toList;
14 |
15 | /**
16 | * @program: EmilyGateway
17 | * @description: DataBuffer工具方法
18 | * @author: 洋
19 | * @create: 2021/01/15
20 | */
21 | public class DataBufferUtils {
22 | /**
23 | * string转DataBuffer
24 | *
25 | * @param value 字符串
26 | */
27 | public static Flux stringToDataBuffer(String value) {
28 | if (StringUtils.isEmpty(value)) {
29 | return Flux.empty();
30 | }
31 | byte[] bytes = value.getBytes(StandardCharsets.UTF_8);
32 | NettyDataBufferFactory nettyDataBufferFactory = new NettyDataBufferFactory(ByteBufAllocator.DEFAULT);
33 | DataBuffer buffer = nettyDataBufferFactory.allocateBuffer(bytes.length);
34 | buffer.write(bytes);
35 | return Flux.just(buffer);
36 | }
37 |
38 | /**
39 | * DataBuffer对象转string
40 | *
41 | * @param dataBuffer 缓存对象
42 | */
43 | public static String dataBufferToString(DataBuffer dataBuffer) {
44 | if (dataBuffer == null) {
45 | return null;
46 | }
47 | //新建存放响应体的字节数组
48 | byte[] content = new byte[dataBuffer.readableByteCount()];
49 | //将响应数据读取到字节数组
50 | dataBuffer.read(content);
51 | //释放内存
52 | org.springframework.core.io.buffer.DataBufferUtils.release(dataBuffer);
53 | // 获取请求body
54 | return new String(content, StandardCharsets.UTF_8);
55 | }
56 |
57 | /**
58 | * 将DataBuffer集合转换为byte数组,如果参数为null,则返回空数组
59 | */
60 | public static byte[] dataBufferToByte(List extends DataBuffer> dataBuffers) {
61 | byte[] allBytes = new byte[]{};
62 | // 解决分片传输多次返回问题
63 | List list = dataBuffers.stream().map(dataBuffer -> dataBufferToByte(dataBuffer)).collect(toList());
64 |
65 | for (int i = 0; i < list.size(); i++) {
66 | allBytes = ArrayUtils.addAll(allBytes, list.get(i));
67 | }
68 | return allBytes;
69 | }
70 |
71 | /**
72 | * 将DataBuffer转换为byte数组,如果参数为null,则返回空数组
73 | */
74 | public static byte[] dataBufferToByte(DataBuffer dataBuffer) {
75 | if (dataBuffer == null) {
76 | return new byte[]{};
77 | }
78 | // 新建存放响应体的字节数组
79 | byte[] content = new byte[dataBuffer.readableByteCount()];
80 | // 将响应数据读取到字节数组
81 | dataBuffer.read(content);
82 | // 释放内存
83 | org.springframework.core.io.buffer.DataBufferUtils.release(dataBuffer);
84 | return content;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/doc/gateway/Gateway网关异常处理.md:
--------------------------------------------------------------------------------
1 | ### Gateway网关异常处理
2 |
3 | #### 一、webflux框架全局异常处理
4 |
5 | > 通过实现ErrorWebExceptionHandler接口,重写handle方法,替换掉框架默认的异常处理实现类DefaultErrorWebExceptionHandler
6 |
7 | ```java
8 | public class GatewayErrorWebExceptionHandler implements ErrorWebExceptionHandler {
9 | /**
10 | * 处理给定的异常
11 | * @param exchange
12 | * @param ex
13 | * @return
14 | */
15 | @Override
16 | public Mono handle(ServerWebExchange exchange, Throwable ex) {
17 | ServerHttpResponse response = exchange.getResponse();
18 | BaseResponse baseResponse = new BaseResponse();
19 | if (ex instanceof ResponseStatusException) {
20 | baseResponse.setStatus(((ResponseStatusException) ex).getStatus().value());
21 | baseResponse.setMessage(((ResponseStatusException) ex).getReason());
22 | } else {
23 | baseResponse.setStatus(500);
24 | baseResponse.setMessage(ex.getMessage());
25 | }
26 | DataBuffer dataBuffer = response.bufferFactory()
27 | .allocateBuffer().write(JSONUtils.toJSONString(baseResponse).getBytes());
28 | response.setStatusCode(HttpStatus.OK);
29 | //基于流形式
30 | response.getHeaders().setContentType(MediaType.APPLICATION_NDJSON);
31 | return response.writeAndFlushWith(Mono.just(ByteBufMono.just(dataBuffer)));
32 | }
33 | }
34 | ```
35 |
36 | #### 二、circuitBreaker断路器异常处理
37 |
38 | > SpringCloudCircuitBreakerFilterFactory拦截器工厂类apply方法中addExceptionDetails方法是处理断路器熔断时异常,handleErrorWithoutFallback方法是在未配置断路器跳转地址的时候异常处理;
39 |
40 | ```java
41 | private void addExceptionDetails(Throwable t, ServerWebExchange exchange) {
42 | ofNullable(t).ifPresent(
43 | exception -> exchange.getAttributes().put(CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR, exception));
44 | }
45 | ```
46 |
47 | 断路器异常会存在ServerWebExchange的属性CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR中,所以我们可以在断路器跳转地址中从此属性中获取异常信息;
48 |
49 | ```java
50 | @GetMapping("fallback")
51 | public String fallback(ServerWebExchange exchange){
52 | Throwable throwable = exchange.getAttribute(ServerWebExchangeUtils.CIRCUITBREAKER_EXECUTION_EXCEPTION_ATTR);
53 | ServerWebExchange delegate = ((ServerWebExchangeDecorator) exchange).getDelegate();
54 | logger.error("服务调用失败,URL={}", delegate.getRequest().getURI(), throwable);
55 | return "Service Unavailable";
56 | }
57 | ```
58 |
59 | handleErrorWithoutFallback方法的具体实现在SpringCloudCircuitBreakerResilience4JFilterFactory类中,其会在指定了断路器但是未指定跳转方法的时候处理异常,具体会被全局异常处理,如下:
60 |
61 | ```java
62 | @Override
63 | protected Mono handleErrorWithoutFallback(Throwable t, boolean resumeWithoutError) {
64 | if (java.util.concurrent.TimeoutException.class.isInstance(t)) {
65 | return Mono.error(new ResponseStatusException(HttpStatus.GATEWAY_TIMEOUT, t.getMessage(), t));
66 | }
67 | if (CallNotPermittedException.class.isInstance(t)) {
68 | return Mono.error(new ServiceUnavailableException());
69 | }
70 | if (resumeWithoutError) {
71 | return Mono.empty();
72 | }
73 | return Mono.error(t);
74 | }
75 | ```
76 |
77 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/exception/handler/WebFluxErrorWebExceptionHandler.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.exception.handler;
2 |
3 | import com.emily.infrastructure.gateway.common.RecordLogger;
4 | import com.google.common.collect.Maps;
5 | import org.springframework.boot.autoconfigure.web.ErrorProperties;
6 | import org.springframework.boot.autoconfigure.web.WebProperties;
7 | import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
8 | import org.springframework.boot.web.error.ErrorAttributeOptions;
9 | import org.springframework.boot.web.reactive.error.ErrorAttributes;
10 | import org.springframework.context.ApplicationContext;
11 | import org.springframework.http.MediaType;
12 | import org.springframework.web.reactive.function.server.*;
13 |
14 | import java.util.Map;
15 | import java.util.Objects;
16 |
17 |
18 | /**
19 | * @author Emily
20 | */
21 | public class WebFluxErrorWebExceptionHandler extends DefaultErrorWebExceptionHandler {
22 |
23 | public WebFluxErrorWebExceptionHandler(ErrorAttributes errorAttributes, WebProperties.Resources resourceProperties,
24 | ErrorProperties errorProperties, ApplicationContext applicationContext) {
25 | super(errorAttributes, resourceProperties, errorProperties, applicationContext);
26 | }
27 |
28 | /**
29 | * 指定响应处理方法为JSON处理的方法
30 | *
31 | * @param errorAttributes
32 | */
33 | @Override
34 | protected RouterFunction getRoutingFunction(ErrorAttributes errorAttributes) {
35 | return RouterFunctions.route(RequestPredicates.all(), this::renderErrorResponse);
36 | }
37 |
38 | /**
39 | * 根据code获取对应的HttpStatus
40 | *
41 | * @param errorAttributes
42 | * @return
43 | */
44 | @Override
45 | public int getHttpStatus(Map errorAttributes) {
46 | int statusCode = (int) errorAttributes.get("status");
47 | return statusCode;
48 | }
49 |
50 | @Override
51 | public ErrorAttributeOptions getErrorAttributeOptions(ServerRequest request, MediaType mediaType) {
52 | return ErrorAttributeOptions.of(
53 | ErrorAttributeOptions.Include.EXCEPTION,
54 | ErrorAttributeOptions.Include.STACK_TRACE,
55 | ErrorAttributeOptions.Include.MESSAGE,
56 | ErrorAttributeOptions.Include.BINDING_ERRORS
57 | );
58 | }
59 |
60 | @Override
61 | protected Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
62 | Map attributes = super.getErrorAttributes(request, options);
63 | Map responseMap = Maps.newLinkedHashMap();
64 | responseMap.put("status", attributes.get("status"));
65 | responseMap.put("message", Objects.nonNull(attributes.get("error")) ? attributes.get("error") : attributes.get("message"));
66 | RecordLogger.recordUser(request, attributes, String.valueOf(responseMap.get("status")), String.valueOf(responseMap.get("message")));
67 | return responseMap;
68 | }
69 | }
--------------------------------------------------------------------------------
/src/main/resources/config-bak/application.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | cloud:
3 | loadbalancer:
4 | ribbon:
5 | # 关闭ribbon客户端负载均衡策略,默认:true
6 | enabled: false
7 | service-discovery:
8 | # 服务发现调用超时时间,默认30S(缓存关闭时有效:DiscoveryClientServiceInstanceListSupplier)
9 | timeout: PT30S
10 | # 负载均衡缓存从consul上拿到的服务实例信息配置(CachingServiceInstanceListSupplier)
11 | cache:
12 | # 开启springcloud loadbalancer负载均衡缓存机制,默认:true
13 | enabled: true
14 | # 缓存过期时间,默认:35 单位:s
15 | #ttl: 35
16 | # 缓存初始化容量,默认:256
17 | #capacity: 256
18 | # 使用java8高性能缓存caffeine
19 | caffeine:
20 | # initialCapacity:初始化容量,默认:-1
21 | # ------------基于大小的驱逐策略--------------
22 | # maximumSize:最大容量,默认:-1
23 | # maximumWeight:权重大小,默认:-1(maximumSize和maximumWeight不可以同时设置)
24 | # ------------基于引用的的去捉策略-------------
25 | # weakKeys:使用弱引用存储key
26 | # weakValues:使用弱引用存储value
27 | # softValues:使用软引用存储value
28 | # -------------基于时间的驱逐策略---------------
29 | # expireAfterAccess:在最后一次访问或者写入后开始计时,在指定的时间后过期。假如一直有请求访问该Key,那么这个缓存将一直不会过期
30 | # expireAfterWrite:在最后一次写入缓存后开始计时,在指定的时间后过期
31 | # refreshAfterWrite:指定在创建或者最近一次更新缓存后经过固定的时间刷新缓存,
32 | # recordStats: 开启统计监控功能
33 | spec: initialCapacity=256,maximumSize=1000,expireAfterWrite=35s,recordStats
34 | zipkin:
35 | # zipkin服务器地址,默认是:http://localhost:9411/
36 | base-url: http://127.0.0.1:9411
37 | service:
38 | # zipkin中展示的服务名称
39 | name: ${spring.application.name}
40 | sender:
41 | # 向zipkin发送spans的方法,默认:web
42 | type: web
43 | sleuth:
44 | reactor:
45 | # 链路追踪上下文的侵入性,manual:以最小的侵入性的方式包装每个REACTOR,而无需通过跟踪上下文,为了向后兼容,默认是:DECORATE_ON_EACH
46 | instrumentation-type: decorate_on_each
47 | sampler:
48 | # 采样率,0-1上的小数
49 | probability: 1.0
50 | redis:
51 | # 启用Redis跨域信息传播
52 | enabled: true
53 | # 服务名
54 | remote-service-name: redis
55 | integration:
56 | # 启用spring集成sleuth instrumentation
57 | enabled: true
58 | # 断路器
59 | circuitbreaker:
60 | # seluth是否开启断路器检查,默认:true
61 | enabled: true
62 | resilience4j:
63 | # 关闭resilience4j断路器自动化配置
64 | enabled: false
65 | blocking:
66 | enabled: false
67 | ---
68 | spring:
69 | emily:
70 | logback:
71 | # 开启日志记录组件
72 | enabled: true
73 | level: info
74 | gateway:
75 | circuitbreaker:
76 | # 断路器超时时间
77 | timeout: 3000
78 | # 指定网关路由访问日志记录限制
79 | exclude-logging-routes:
80 | #- id: rabbitmq2
81 | # path: /rabbitmq/**
82 | #- id: rabbitmq3
83 | # path: /rabbitmq/**
84 | #- id: rabbitmq4
85 | # path: /rabbitmq/**
86 | #- id: consul
87 | # path: /ui/**,/v1/**
88 | # 指定对外部网络访问限制
89 | exclude-externel-routes:
90 | - id: rabbitmq2
91 | path: /rabbitmq/**
92 | - id: rabbitmq3
93 | path: /rabbitmq/**
94 | - id: rabbitmq4
95 | path: /rabbitmq/**
96 | # 指定外部网络访问协议限制
97 | include-schemas: http,https
98 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/filter/dedupe/DedupeLoginGatewayFilterFactory.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.filter.dedupe;
2 |
3 | import com.emily.infrastructure.gateway.common.utils.FilterUtils;
4 | import com.emily.infrastructure.gateway.config.filter.order.GatewayFilterOrdered;
5 | import org.apache.commons.lang3.StringUtils;
6 | import org.springframework.cloud.gateway.filter.GatewayFilter;
7 | import org.springframework.cloud.gateway.filter.GatewayFilterChain;
8 | import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
9 | import org.springframework.core.Ordered;
10 | import org.springframework.core.io.buffer.DataBuffer;
11 | import org.springframework.http.HttpHeaders;
12 | import org.springframework.http.HttpStatus;
13 | import org.springframework.http.MediaType;
14 | import org.springframework.http.server.reactive.ServerHttpResponse;
15 | import org.springframework.validation.annotation.Validated;
16 | import org.springframework.web.server.ServerWebExchange;
17 | import reactor.core.publisher.Mono;
18 |
19 | import java.util.Arrays;
20 | import java.util.List;
21 |
22 | /**
23 | * @Description : 剔除登录断言
24 | * @Author : Emily
25 | * @CreateDate : Created in 2022/7/7 10:30 上午
26 | */
27 | public class DedupeLoginGatewayFilterFactory extends AbstractGatewayFilterFactory {
28 |
29 | private static final String WHITE_LIST = "whiteList";
30 |
31 | public DedupeLoginGatewayFilterFactory() {
32 | super(Config.class);
33 | }
34 |
35 | @Override
36 | public List shortcutFieldOrder() {
37 | return Arrays.asList(WHITE_LIST);
38 | }
39 |
40 |
41 | @Override
42 | public GatewayFilter apply(Config config) {
43 | return new GatewayFilterOrdered() {
44 | @Override
45 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
46 | String[] paths = StringUtils.split(exchange.getRequest().getURI().getRawPath(), "/");
47 | if (config.getWhiteList().contains(paths[0])) {
48 | return chain.filter(exchange);
49 | }
50 | ServerHttpResponse response = exchange.getResponse();
51 | //返回结果
52 | DataBuffer dataBuffer = FilterUtils.getResponseData(exchange);
53 | //指定编码,否则在浏览器中会中文乱码
54 | response.setStatusCode(HttpStatus.UNAUTHORIZED);
55 | response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
56 | return response.writeWith(Mono.just(dataBuffer));
57 | }
58 |
59 | @Override
60 | public int getOrder() {
61 | return Ordered.HIGHEST_PRECEDENCE;
62 | }
63 | };
64 | }
65 |
66 | @Validated
67 | public static class Config {
68 |
69 | private List whiteList;
70 |
71 | public List getWhiteList() {
72 | return whiteList;
73 | }
74 |
75 | public void setWhiteList(List whiteList) {
76 | this.whiteList = whiteList;
77 | }
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/common/ServerRequestUtils.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.common;
2 |
3 | import com.emily.infrastructure.gateway.common.enums.HttpStatusType;
4 | import com.emily.infrastructure.json.JsonUtils;
5 | import com.google.common.collect.Maps;
6 | import org.springframework.core.io.buffer.DataBuffer;
7 | import org.springframework.http.MediaType;
8 | import org.springframework.http.server.reactive.ServerHttpRequest;
9 | import org.springframework.util.Assert;
10 | import org.springframework.web.server.ServerWebExchange;
11 |
12 | import java.nio.charset.StandardCharsets;
13 | import java.util.Arrays;
14 | import java.util.Map;
15 |
16 | import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.CACHED_REQUEST_BODY_ATTR;
17 | import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
18 |
19 | /**
20 | * @program: EmilyGateway
21 | * @description: 请求工具类
22 | * @author:
23 | * @create: 2021/03/04
24 | */
25 | public class ServerRequestUtils {
26 | /**
27 | * 获取网关请求IP
28 | */
29 | public static String getIp(ServerHttpRequest request) {
30 | Assert.notNull(request, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
31 | return request.getRemoteAddress().getHostString();
32 | }
33 |
34 | /**
35 | * 获取请求路径
36 | */
37 | public static String getUrl(ServerWebExchange exchange) {
38 | Assert.notNull(exchange, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
39 | return exchange.getAttributes().containsKey(GATEWAY_REQUEST_URL_ATTR) ? exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR).toString() : exchange.getRequest().getURI().toString();
40 | }
41 |
42 | /**
43 | * 网关请求协议
44 | */
45 | public static String getSchema(ServerHttpRequest request) {
46 | Assert.notNull(request, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
47 | return request.getURI().getScheme();
48 | }
49 |
50 | /**
51 | * 获取请求Method
52 | */
53 | public static String getMethod(ServerHttpRequest request) {
54 | Assert.notNull(request, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
55 | return request.getMethodValue();
56 | }
57 |
58 | /**
59 | * 获取请求ContentType
60 | *
61 | * @param request
62 | * @return
63 | */
64 | public static String getContentType(ServerHttpRequest request) {
65 | Assert.notNull(request, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
66 | return request.getHeaders().getContentType() == null ? null : MediaType.toString(Arrays.asList(request.getHeaders().getContentType()));
67 | }
68 |
69 | /**
70 | * 获取请求参数
71 | */
72 | public static Object getRequestParams(ServerWebExchange exchange) {
73 | Assert.notNull(exchange, HttpStatusType.ILLEGAL_ARGUMENT.getMessage());
74 | Map paramsMap = Maps.newHashMap();
75 | paramsMap.put("headers", exchange.getRequest().getHeaders());
76 | DataBuffer dataBuffer = exchange.getAttribute(CACHED_REQUEST_BODY_ATTR);
77 | try {
78 | if (dataBuffer == null) {
79 | return paramsMap;
80 | }
81 | paramsMap.putAll(JsonUtils.toJavaBean(dataBuffer.toString(StandardCharsets.UTF_8), Map.class));
82 | } catch (Exception e) {
83 | paramsMap.put("result", dataBuffer == null ? null : dataBuffer.toString(StandardCharsets.UTF_8));
84 | }
85 | return paramsMap;
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/src/main/resources/application-server.properties:
--------------------------------------------------------------------------------
1 | server.port=443
2 | # 优雅停机
3 | server.shutdown=graceful
4 | # 是否启用SSL支持
5 | server.ssl.enabled=true
6 | # 标识秘钥存储中秘钥的别名
7 | server.ssl.key-alias=emily
8 | # 指定保存SSL证书的秘钥存储的路径(通常是jks文件)
9 | server.ssl.key-store=classpath:emily.p12
10 | # 访问秘钥存储的密码
11 | server.ssl.key-store-password=123456
12 | # 秘钥存储的类型
13 | server.ssl.key-store-type=PKCS12
14 | # 要使用的SSL协议
15 | server.ssl.protocol=TLS
16 |
17 | # 是否开启http端口号
18 | server.http.enabled=false
19 | # http端口号
20 | server.http.port=80
21 | # http请求是否自动跳转到https,默认:true
22 | server.http.http-to-https=false
23 | # 优雅停机
24 | server.http.shutdown=graceful
25 |
26 | # 网络通道(channel)连接超时时间
27 | server.netty.connection-timeout=PT10S
28 | # 连接等待时间(毫秒),超时会被自动关闭。为空表示永远不关闭,全靠请求方
29 | server.netty.idle-timeout=PT10S
30 | # 每个连接可以发送的最大连接数,默认无限制
31 | server.netty.max-keep-alive-requests=100000
32 | # h2c(即:http/2协议的明文版本)升级请求的最大内容长度,默认:0B
33 | server.netty.h2c-max-content-length=0B
34 | # http请求解码的初始缓冲区大小,默认:128B
35 | server.netty.initial-buffer-size=128B
36 | # HTTP请求可解码的最大块大小,默认:8KB
37 | server.netty.max-chunk-size=8KB
38 | # http请求的初始行可以解码的最大长度
39 | server.netty.max-initial-line-length=4KB
40 | # 解码请求时是否验证标头,默认:true
41 | server.netty.validate-headers=true
42 |
43 | # 开启网关端点监控,默认:true
44 | management.endpoint.gateway.enabled=true
45 | # 是否打开网关端点健康
46 | management.endpoints.web.exposure.include=gateway,health
47 | #健康检查端口号
48 | management.server.port=7443
49 | # 是否开启SSL支持,默认:true
50 | management.server.ssl.enabled=true
51 | # 标识秘钥存储中秘钥的别名
52 | management.server.ssl.key-alias=emily
53 | # 指定保存SSL证书的秘钥存储的路径(通常是jks文件)
54 | management.server.ssl.key-store=classpath:emily.p12
55 | #访问秘钥存储的密码
56 | management.server.ssl.key-store-password=123456
57 | # 秘钥存储的类型
58 | management.server.ssl.key-store-type=PKCS12
59 | # 要使用的SSL协议
60 | management.server.ssl.protocol=TLS
61 |
62 | # 连接超时,单位毫秒,默认:45s
63 | spring.cloud.gateway.httpclient.connect-timeout=45000
64 | # 响应超时时间,{@link java.time.Duration}类型
65 | spring.cloud.gateway.httpclient.response-timeout=PT1S
66 | # 最大响应头大小,默认:8192,单位:字节B
67 | spring.cloud.gateway.httpclient.max-header-size=8192
68 | # 最大初始行大小,单位:字节B
69 | spring.cloud.gateway.httpclient.max-initial-line-length=4096
70 | # channel pool映射名称,默认:proxy
71 | spring.cloud.gateway.httpclient.pool.name=proxy
72 | # HttpClient要使用的池的类型,默认:elastic
73 | spring.cloud.gateway.httpclient.pool.type=elastic
74 | # 空闲请求在空闲多久后会被回收,默认:毫秒 -Dreactor.netty.pool.leasingStrategy=lifo
75 | spring.cloud.gateway.httpclient.pool.max-idle-time=PT1S
76 | # 信任下游的所有证书TLS, 默认:false
77 | spring.cloud.gateway.httpclient.ssl.use-insecure-trust-manager=true
78 | # 为Netty HttpClient启用监听调试,默认:false
79 | spring.cloud.gateway.httpclient.wiretap=true
80 | # 为Netty HttpClient开启监听debugging模式,默认:false
81 | spring.cloud.gateway.httpclient.compression=false
82 | # 为Netty HttpServer开启监听debugging模式,默认:false
83 | spring.cloud.gateway.httpserver.wiretap=true
84 |
85 | # 是否允许携带认证信息,默认:false
86 | spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowCredentials=false
87 | # 允许跨域的源(网站域名|IP),设置*为全部
88 | spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedOrigins=*
89 | # 允许跨域的method,设置*为全部
90 | spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedMethods=*
91 | # 允许跨域秦秋的Header字段,设置*为全部
92 | spring.cloud.gateway.globalcors.corsConfigurations.[/**].allowedHeaders=*
93 | # 配置客户端预检请求响应缓存时间,单位:秒(默认:1800)
94 | spring.cloud.gateway.globalcors.corsConfigurations.[/**].maxAge=1800
95 |
96 | # 支持HTTP预检请求OPTIONS CORS请求,默认:false
97 | spring.cloud.gateway.globalcors.add-to-simple-url-handler-mapping=false
98 |
--------------------------------------------------------------------------------
/doc/gateway/gateway路由断言工厂.md:
--------------------------------------------------------------------------------
1 | ### 一、Path路由断言工厂
2 |
3 | > 路由断言工厂类有两个参数,patterns(基于spring的PathMatcher)、matchTrailingSlash(是否匹配斜杠,默认:true)
4 |
5 | ```yaml
6 | spring:
7 | cloud:
8 | gateway:
9 | routes:
10 | - id: path_route
11 | uri: https://example.org
12 | predicates:
13 | - Path=/red/{segment},/blue/{segment}
14 | ```
15 |
16 | - 支持'/foo/{*foobar}' 和 '/{*foobar}'格式,由CaptureTheRestPathElement提供支持;
17 | - 支持'/foo/{bar}/goo'格式,将一段变量作为路径元素,由CaptureVariablePathElement提供支持;
18 | - 支持'/foo/bar/goo'格式,由LiteralPathElement提供支持;
19 | - 支持'/foo/*/goo'通配符格式,*代表至少匹配一个字符,由WildcardPathElement提供支持;
20 | - 支持'/foo/**' 和 /** Rest通配符格式,匹配0个或者多个目录,由WildcardTheRestPathElement提供支持;
21 | - 支持'/foo/??go'单字符通配符格式,一个?代表单个字符,若需要适配多个可用多个?标识,由SingleCharWildcardedPathElement提供支持;
22 | - 支持/foo/*_*/*\_{foobar}格式,由RegexPathElement提供支持;
23 |
24 | #### 二、Method路由断言工厂
25 |
26 | ```yaml
27 | spring:
28 | cloud:
29 | gateway:
30 | routes:
31 | - id: method_route
32 | uri: https://example.org
33 | predicates:
34 | - Method=GET,POST
35 | ```
36 |
37 | > Method路由断言工厂有一个参数Metod,指定多个支持的请求方法;支持的方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
38 |
39 | #### 三、Weight权重路由断言工厂
40 |
41 | ```yaml
42 | spring:
43 | cloud:
44 | gateway:
45 | routes:
46 | - id: weight_high
47 | uri: https://weighthigh.org
48 | predicates:
49 | - Weight=group1, 8
50 | - id: weight_low
51 | uri: https://weightlow.org
52 | predicates:
53 | - Weight=group1, 2
54 | ```
55 |
56 | > Weight权重路由断言工厂有两个参数group(分组名称)和weight(权重 int类型),每组按照权重计算流量
57 |
58 | #### 四、After路由断言工厂类
59 |
60 | > After路由断言工厂类带有一个参数datetime(是ZonedDateTime类型),此断言工厂类判定所有请求只有发生在指定的时间之后才符合条件;
61 |
62 | ```yaml
63 | spring:
64 | cloud:
65 | gateway:
66 | routes:
67 | - id: after_route
68 | uri: https://example.org
69 | predicates:
70 | - After=2022-04-20T15:35:08.721398+08:00[Asia/Shanghai]
71 | ```
72 |
73 | 获取带区域时间方法如下:
74 |
75 | ```java
76 | public static void main(String[] args) {
77 | ZonedDateTime zbj = ZonedDateTime.now();
78 | ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York"));
79 | System.out.println(zbj);
80 | System.out.println(zny);
81 | }
82 | ```
83 |
84 | #### 四、Before路由断言工厂类
85 |
86 | > Before路由断言工厂类只有一个ZonedDateTime类型参数datetime,所有的请求只会发生在datetime之前;
87 |
88 | ```
89 | spring:
90 | cloud:
91 | gateway:
92 | routes:
93 | - id: before_route
94 | uri: https://example.org
95 | predicates:
96 | - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
97 | ```
98 |
99 | #### 五、Between路由断言工厂类
100 |
101 | > Between路由断言工厂类有两个ZonedDateTime类型参数datetime1、datetime2,请求必须发生在datetime1和datetime2之间,datetime2必须小于datetime1;
102 |
103 | ```
104 | spring:
105 | cloud:
106 | gateway:
107 | routes:
108 | - id: between_route
109 | uri: https://example.org
110 | predicates:
111 | - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
112 | ```
113 |
114 | #### 六、RemoteAddr路由断言工厂类
115 |
116 | > RemoteAddr路由断言工厂类有一个列表类型参数sources,可以指定路由请求的IP地址;其中192.168.1.1是IP地址,24是子网掩码;
117 |
118 | ```
119 | spring:
120 | cloud:
121 | gateway:
122 | routes:
123 | - id: remoteaddr_route
124 | uri: https://example.org
125 | predicates:
126 | - RemoteAddr=192.168.1.1/24
127 | ```
128 |
129 |
--------------------------------------------------------------------------------
/src/main/resources/application-gateway.properties:
--------------------------------------------------------------------------------
1 | #spring.cloud.gateway.default-filters[0].name=DedupeLogin
2 | #spring.cloud.gateway.default-filters[0].args.whiteList=openAccount,guba
3 |
4 |
5 | spring.cloud.gateway.routes[0].id=emily
6 | spring.cloud.gateway.routes[0].uri=lb://demo-emily-spring-cloud
7 | spring.cloud.gateway.routes[0].order=0
8 | spring.cloud.gateway.routes[0].predicates[0]=Path=/emily/api/**
9 | spring.cloud.gateway.routes[0].predicates[1]=Method=POST,GET,OPTIONS
10 | spring.cloud.gateway.routes[0].predicates[2]=RemoteAddr=192.168.0.108/24,127.0.0.1
11 | spring.cloud.gateway.routes[0].filters[0]=StripPrefix=1
12 | spring.cloud.gateway.routes[0].filters[1].name=RecordLogger
13 | spring.cloud.gateway.routes[0].filters[1].args.enabled=true
14 | #spring.cloud.gateway.routes[0].filters[2].name=CircuitBreaker
15 | #spring.cloud.gateway.routes[0].filters[2].args.name=circuitBreaker
16 | #spring.cloud.gateway.routes[0].filters[2].args.fallbackUri=forward:/circuitBreaker/fallback
17 | #spring.cloud.gateway.routes[0].filters[2].args.resumeWithoutError=false
18 | #spring.cloud.gateway.routes[0].filters[2].args.statusCodes[0]=500
19 | #spring.cloud.gateway.routes[0].filters[2].args.statusCodes[1]=404
20 | #spring.cloud.gateway.routes[0].filters[3].name=CacheRequestBody
21 | #spring.cloud.gateway.routes[0].filters[3].args.bodyClass=java.lang.String
22 |
23 | spring.cloud.gateway.routes[1].id=simple
24 | spring.cloud.gateway.routes[1].uri=lb://demo-emily-spring-cloud
25 | spring.cloud.gateway.routes[1].order=0
26 | spring.cloud.gateway.routes[1].predicates[0]=Path=/simple/api/**
27 | spring.cloud.gateway.routes[1].filters[0]=StripPrefix=1
28 | spring.cloud.gateway.routes[1].filters[1].name=CircuitBreaker
29 | spring.cloud.gateway.routes[1].filters[1].args.name=circuitBreaker
30 | spring.cloud.gateway.routes[1].filters[1].args.fallbackUri=forward:/circuitBreaker/fallback
31 |
32 | spring.cloud.gateway.routes[2].id=weight-high
33 | spring.cloud.gateway.routes[2].uri=https://127.0.0.1:9000
34 | spring.cloud.gateway.routes[2].order=0
35 | spring.cloud.gateway.routes[2].predicates[0]=Path=/weight/api/test/{segment}
36 | spring.cloud.gateway.routes[2].predicates[1]=Method=POST,GET
37 | spring.cloud.gateway.routes[2].predicates[2]=Weight=group, 20
38 | spring.cloud.gateway.routes[2].filters[0]=StripPrefix=1
39 |
40 | spring.cloud.gateway.routes[3].id=weight-low
41 | spring.cloud.gateway.routes[3].uri=http://127.0.0.1:9111
42 | spring.cloud.gateway.routes[3].predicates[0]=Path=/weight/api/test/{segment}
43 | spring.cloud.gateway.routes[3].predicates[1]=Method=POST,GET
44 | spring.cloud.gateway.routes[3].predicates[2]=Weight=group, 20
45 | spring.cloud.gateway.routes[3].filters[0]=StripPrefix=1
46 |
47 | spring.cloud.gateway.routes[4].id=rabbitmq2
48 | spring.cloud.gateway.routes[4].uri=http://10.10.181.41:15672
49 | spring.cloud.gateway.routes[4].predicates[0]=Path=/rabbitmq/**
50 | spring.cloud.gateway.routes[4].predicates[1]=Weight=rabbitmqGroup, 40
51 | spring.cloud.gateway.routes[4].filters[0]=StripPrefix=1
52 |
53 | spring.cloud.gateway.routes[5].id=rabbitmq3
54 | spring.cloud.gateway.routes[5].uri=http://10.10.181.42:15672
55 | spring.cloud.gateway.routes[5].predicates[0]=Path=/rabbitmq/**
56 | spring.cloud.gateway.routes[5].predicates[1]=Weight=rabbitmqGroup, 30
57 | spring.cloud.gateway.routes[5].filters[0]=StripPrefix=1
58 |
59 | spring.cloud.gateway.routes[6].id=rabbitmq4
60 | spring.cloud.gateway.routes[6].uri=http://10.10.181.43:15672
61 | spring.cloud.gateway.routes[6].predicates[0]=Path=/rabbitmq/**
62 | spring.cloud.gateway.routes[6].predicates[1]=Weight=rabbitmqGroup, 30
63 | spring.cloud.gateway.routes[6].filters[0]=StripPrefix=1
64 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/exception/WebFluxExceptionAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.exception;
2 |
3 | import com.emily.infrastructure.gateway.config.exception.handler.WebFluxErrorAttributes;
4 | import com.emily.infrastructure.gateway.config.exception.handler.WebFluxErrorWebExceptionHandler;
5 | import org.springframework.beans.factory.ObjectProvider;
6 | import org.springframework.boot.autoconfigure.AutoConfigureBefore;
7 | import org.springframework.boot.autoconfigure.condition.*;
8 | import org.springframework.boot.autoconfigure.web.ServerProperties;
9 | import org.springframework.boot.autoconfigure.web.WebProperties;
10 | import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration;
11 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
12 | import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
13 | import org.springframework.boot.web.reactive.error.ErrorAttributes;
14 | import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
15 | import org.springframework.context.ApplicationContext;
16 | import org.springframework.context.annotation.Bean;
17 | import org.springframework.context.annotation.Configuration;
18 | import org.springframework.core.annotation.Order;
19 | import org.springframework.http.codec.ServerCodecConfigurer;
20 | import org.springframework.web.reactive.config.WebFluxConfigurer;
21 | import org.springframework.web.reactive.result.view.ViewResolver;
22 |
23 | import java.util.stream.Collectors;
24 |
25 | /**
26 | * Gateway网关全局异常处理,覆盖默认异常处理
27 | *
28 | * @author Emily
29 | */
30 | @Configuration(proxyBeanMethods = false)
31 | @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
32 | @ConditionalOnClass(WebFluxConfigurer.class)
33 | @AutoConfigureBefore(WebFluxAutoConfiguration.class)
34 | @EnableConfigurationProperties({ServerProperties.class, WebProperties.class,WebFluxExceptionProperties.class})
35 | @ConditionalOnProperty(prefix = WebFluxExceptionProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
36 | public class WebFluxExceptionAutoConfiguration {
37 |
38 | private final ServerProperties serverProperties;
39 |
40 | public WebFluxExceptionAutoConfiguration(ServerProperties serverProperties) {
41 | this.serverProperties = serverProperties;
42 | }
43 |
44 |
45 | @Bean
46 | @ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT)
47 | @Order(-1)
48 | public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes,
49 | WebProperties webProperties, ObjectProvider viewResolvers,
50 | ServerCodecConfigurer serverCodecConfigurer, ApplicationContext applicationContext) {
51 | WebFluxErrorWebExceptionHandler exceptionHandler = new WebFluxErrorWebExceptionHandler(errorAttributes,
52 | webProperties.getResources(), this.serverProperties.getError(), applicationContext);
53 | exceptionHandler.setViewResolvers(viewResolvers.orderedStream().collect(Collectors.toList()));
54 | exceptionHandler.setMessageWriters(serverCodecConfigurer.getWriters());
55 | exceptionHandler.setMessageReaders(serverCodecConfigurer.getReaders());
56 | return exceptionHandler;
57 | }
58 |
59 | @Bean
60 | @ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
61 | public DefaultErrorAttributes errorAttributes() {
62 | return new WebFluxErrorAttributes(this.serverProperties.getError().isIncludeException());
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/filter/logger/RecordLoggerResponseDecorator.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.filter.logger;
2 |
3 | import com.emily.infrastructure.gateway.common.entity.BaseLogger;
4 | import com.emily.infrastructure.json.JsonUtils;
5 | import com.fasterxml.jackson.core.type.TypeReference;
6 | import org.apache.commons.lang3.StringUtils;
7 | import org.reactivestreams.Publisher;
8 | import org.springframework.core.io.buffer.DataBuffer;
9 | import org.springframework.core.io.buffer.DataBufferFactory;
10 | import org.springframework.core.io.buffer.DataBufferUtils;
11 | import org.springframework.core.io.buffer.DefaultDataBufferFactory;
12 | import org.springframework.http.MediaType;
13 | import org.springframework.http.server.reactive.ServerHttpResponseDecorator;
14 | import org.springframework.web.server.ServerWebExchange;
15 | import reactor.core.publisher.Flux;
16 | import reactor.core.publisher.Mono;
17 |
18 | import java.nio.charset.StandardCharsets;
19 | import java.util.List;
20 | import java.util.Map;
21 |
22 | import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR;
23 |
24 | /**
25 | * @Description : 网关记录日志装饰器类
26 | * @Author : Emily
27 | * @CreateDate : Created in 2022/4/28 5:51 下午
28 | */
29 | public class RecordLoggerResponseDecorator extends ServerHttpResponseDecorator {
30 | private ServerWebExchange exchange;
31 |
32 | public RecordLoggerResponseDecorator(ServerWebExchange exchange) {
33 | super(exchange.getResponse());
34 | this.exchange = exchange;
35 | }
36 |
37 | @Override
38 | public Mono writeWith(Publisher extends DataBuffer> body) {
39 | if (body instanceof Flux) {
40 | Flux fluxBody = (Flux) body;
41 | return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
42 | DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
43 | DataBuffer join = dataBufferFactory.join(dataBuffers);
44 | byte[] content = new byte[join.readableByteCount()];
45 | join.read(content);
46 | DataBufferUtils.release(join);
47 |
48 | String bodyStr = new String(content, StandardCharsets.UTF_8);
49 | // 获取记录日志对象
50 | BaseLogger baseLogger = exchange.getAttribute(RecordLoggerGatewayFilterFactory.BASE_LOGGER);
51 | //设置请求URL
52 | baseLogger.setUrl(exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR).toString());
53 | //设置响应结果
54 | baseLogger.setResponseBody(transBody(exchange.getResponse().getHeaders().getContentType(), bodyStr));
55 | this.getDelegate().getHeaders().setContentLength(bodyStr.getBytes().length);
56 | return bufferFactory().wrap(bodyStr.getBytes());
57 | }));
58 | }
59 | return super.writeWith(body);
60 | }
61 |
62 | /**
63 | * 将数据转换为对象类型
64 | *
65 | * @param body 字符串
66 | */
67 |
68 | private Object transBody(MediaType mediaType, String body) {
69 | if (StringUtils.isEmpty(body)) {
70 | return body;
71 | }
72 | if (mediaType.includes(MediaType.APPLICATION_JSON) || mediaType.includes(MediaType.APPLICATION_JSON_UTF8)) {
73 | try {
74 | return JsonUtils.toJavaBean(body, Map.class);
75 | } catch (Exception e) {
76 | try {
77 | return JsonUtils.toJavaBean(body, new TypeReference>>() {
78 | });
79 | } catch (Exception exception) {
80 | }
81 | }
82 | }
83 | return body;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/circuitbreaker/CircuitBreakerAutoConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.circuitbreaker;
2 |
3 | import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
4 | import io.github.resilience4j.timelimiter.TimeLimiterConfig;
5 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
6 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
7 | import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
8 | import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
9 | import org.springframework.cloud.client.circuitbreaker.Customizer;
10 | import org.springframework.context.annotation.Bean;
11 | import org.springframework.context.annotation.Configuration;
12 |
13 | import java.time.Duration;
14 |
15 | /**
16 | * @Description : 弹性断路器配置类
17 | * @Author : Emily
18 | * @CreateDate : Created in 2022/4/20 5:28 下午
19 | */
20 | @Configuration
21 | @EnableConfigurationProperties(CircuitBreakerProperties.class)
22 | @ConditionalOnProperty(prefix = CircuitBreakerProperties.PREFIX, name = "enabled", havingValue = "true", matchIfMissing = true)
23 | public class CircuitBreakerAutoConfiguration {
24 | /**
25 | * 断路器默认配置
26 | *
27 | * @return
28 | */
29 | @Bean
30 | public Customizer defaultCustomizer(CircuitBreakerProperties properties) {
31 | CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
32 | // 滑动窗口的类型为时间窗口,默认:COUNT_BASED
33 | .slidingWindowType(CircuitBreakerConfig.SlidingWindowType.TIME_BASED)
34 | // 时间窗口的大小为100(COUNT_BASED-次数,TIME_BASED-秒),默认:100
35 | .slidingWindowSize(100)
36 | // 断路器故障率阀值(百分比),大于等于阀值会将断路器置为打开状态,默认:50
37 | .failureRateThreshold(50)
38 | // 配置调用持续时间阀值,超过这个阀值的调用将被视为慢查询,并增加慢查询百分比,默认:60秒
39 | .slowCallDurationThreshold(Duration.ofSeconds(5))
40 | // 慢查询百分比,默认:100(百分比)
41 | .slowCallRateThreshold(40)
42 | // 在单个滑动窗口计算故障率或慢查询率之前最小的调用次数,默认:100
43 | .minimumNumberOfCalls(10)
44 | // (可选参数)配置CircuitBreaker断路器由half open状态转为open状态之前需保持的时间,
45 | // 默认断路器将会保持half open状态直到minimumNumberOfCalls指定的次数(无论成功还是失败)
46 | //.maxWaitDurationInHalfOpenState(Duration.ofSeconds(1))
47 | // 在HALF-OPEN(半开状态)下允许进行正常调用的次数,默认:10
48 | .permittedNumberOfCallsInHalfOpenState(10)
49 | // 断路器由打开状态转换为半开状态需要时间,默认:60秒
50 | .waitDurationInOpenState(Duration.ofSeconds(60))
51 | // 允许断路器自动由OPEN状态转为HALF_OPEN半开状态
52 | .enableAutomaticTransitionFromOpenToHalfOpen()
53 | // 指定记录为失败的异常列表,只要匹配或其子类都会记录为异常
54 | .recordExceptions(Throwable.class)
55 | // 自定义异常列表,即不会计入成功也不会计入失败(即使在recordExceptions列表中指定)
56 | .ignoreExceptions()
57 | // 自定义断言,如果异常要被统计,则返回true,否则返回false (recordFailure)
58 | .recordException(throwable -> true)
59 | // 自定义断言,如果异常即不计入成功也不计入失败,则返回true,否则返回false
60 | .ignoreException(throwable -> false)
61 | .build();
62 |
63 |
64 | return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
65 | .circuitBreakerConfig(circuitBreakerConfig)
66 | .timeLimiterConfig(TimeLimiterConfig.custom()
67 | .timeoutDuration(Duration.ofSeconds(properties.getTimeout()))
68 | .cancelRunningFuture(true).build())
69 | .build());
70 |
71 | }
72 | }
73 |
--------------------------------------------------------------------------------
/doc/死磕源码系列【consul配置中心监视器类ConfigWatch动态刷新配置】.md:
--------------------------------------------------------------------------------
1 | ### 死磕源码系列【consul配置中心监视器类ConfigWatch动态刷新配置】
2 |
3 | > consul作为配置中心时可以开启动态刷新配置的功能,其实现类是通过ConfigWatch来实现;
4 |
5 | ##### 监视器类相关属性配置
6 |
7 | ```yaml
8 | spring:
9 | cloud:
10 | consul:
11 | config:
12 | watch:
13 | # 是否开启配置中心配置变动,默认:true
14 | enabled: true
15 | # 监控的固定延迟值,即一次执行完成到下一次执行开始之间的间隔,默认:1000毫秒
16 | delay: 1000
17 | # 等待(或阻塞)监视查询的秒数,默认:55秒,需要小于60
18 | wait-time: 55
19 | ```
20 |
21 | > 此处要说明一下wait-time属性,此属性是设置调用consul的接口方法时会阻塞指定的时间,如果在阻塞过程中有配置修改则立马返回,否则要等到阻塞时间结束,delay属性指的是在上次调用接口结束之后多久开始下一次调度;
22 |
23 | ##### 1.ConfigWatch定时任务配置方法
24 |
25 | ```java
26 | @Override
27 | public void start() {
28 | if (this.running.compareAndSet(false, true)) {
29 | this.watchFuture = this.taskScheduler.scheduleWithFixedDelay(this::watchConfigKeyValues,
30 | this.properties.getWatch().getDelay());
31 | }
32 | }
33 | ```
34 |
35 | > 指定一个固定的线程池,每次在上次任务执行完成后间隔指定的时间执行下次任务;
36 |
37 | ##### 2.watchConfigKeyValues监视器任务方法
38 |
39 | ```java
40 | @Timed("consul.watch-config-keys")
41 | public void watchConfigKeyValues() {
42 | if (!this.running.get()) {
43 | return;
44 | }
45 | for (String context : this.consulIndexes.keySet()) {
46 | ...
47 | //通过API调度获取配置中心配置信息,接口阻塞指定的超时时间,默认是:55s
48 | Response> response = this.consul.getKVValues(context, aclToken,
49 | new QueryParams(this.properties.getWatch().getWaitTime(), currentIndex));
50 |
51 | // if response.value == null, response was a 404, otherwise it was a
52 | // 200, reducing churn if there wasn't anything
53 | if (response.getValue() != null && !response.getValue().isEmpty()) {
54 | // 获取配置中心的新版本
55 | Long newIndex = response.getConsulIndex();
56 |
57 | if (newIndex != null && !newIndex.equals(currentIndex)) {
58 | // 判定新版本和老版本是否一样,不同则发布事件刷新
59 | if (!this.consulIndexes.containsValue(newIndex) && !currentIndex.equals(-1L)) {
60 | if (log.isTraceEnabled()) {
61 | log.trace("Context " + context + " has new index " + newIndex);
62 | }
63 | RefreshEventData data = new RefreshEventData(context, currentIndex, newIndex);
64 | //发布刷新本地配置事件
65 | this.publisher.publishEvent(new RefreshEvent(this, data, data.toString()));
66 | }
67 | else if (log.isTraceEnabled()) {
68 | log.trace("Event for index already published for context " + context);
69 | }
70 | this.consulIndexes.put(context, newIndex);
71 | }
72 | ...
73 | }
74 | ```
75 |
76 | > 上述监视器方法监测到配置变化会发布RefreshEvent事件,该事件会被RefreshEventListener监听器监听
77 |
78 | ##### 3.RefreshEventListener监听器
79 |
80 | ```java
81 | public class RefreshEventListener implements SmartApplicationListener {
82 |
83 | private static Log log = LogFactory.getLog(RefreshEventListener.class);
84 |
85 | private ContextRefresher refresh;
86 |
87 | private AtomicBoolean ready = new AtomicBoolean(false);
88 |
89 | public RefreshEventListener(ContextRefresher refresh) {
90 | this.refresh = refresh;
91 | }
92 |
93 | @Override
94 | public boolean supportsEventType(Class extends ApplicationEvent> eventType) {
95 | return ApplicationReadyEvent.class.isAssignableFrom(eventType)
96 | || RefreshEvent.class.isAssignableFrom(eventType);
97 | }
98 |
99 | @Override
100 | public void onApplicationEvent(ApplicationEvent event) {
101 | if (event instanceof ApplicationReadyEvent) {
102 | handle((ApplicationReadyEvent) event);
103 | }
104 | else if (event instanceof RefreshEvent) {
105 | handle((RefreshEvent) event);
106 | }
107 | }
108 |
109 | public void handle(ApplicationReadyEvent event) {
110 | this.ready.compareAndSet(false, true);
111 | }
112 |
113 | public void handle(RefreshEvent event) {
114 | if (this.ready.get()) { // don't handle events before app is ready
115 | log.debug("Event received " + event.getEventDesc());
116 | //调用ContextRefresher刷新配置方法
117 | Set keys = this.refresh.refresh();
118 | log.info("Refresh keys changed: " + keys);
119 | }
120 | }
121 |
122 | }
123 | ```
124 |
125 | GitHub地址:[https://github.com/mingyang66/EmilyGateway](https://github.com/mingyang66/EmilyGateway)
--------------------------------------------------------------------------------
/doc/NettyWebServer开启http端口并实现http自动跳转https.md:
--------------------------------------------------------------------------------
1 | ##### NettyWebServer开启http端口并实现http自动跳转https
2 |
3 | ##### 1.定义配置文件类
4 |
5 | ```java
6 | package com.emily.cloud.gateway.config;
7 |
8 | import org.springframework.boot.context.properties.ConfigurationProperties;
9 | import org.springframework.boot.web.server.Shutdown;
10 |
11 | /**
12 | * @program: EmilyGateway
13 | * @description: Netty服务器配置
14 | * @create: 2021/01/13
15 | */
16 | @ConfigurationProperties(prefix = "server.http")
17 | public class ServerProperties {
18 | /**
19 | * 是否开启http端口号
20 | */
21 | private boolean enable;
22 | /**
23 | * http端口号
24 | */
25 | private int port = 8080;
26 | /**
27 | * 关闭模式
28 | */
29 | private Shutdown shutdown = Shutdown.IMMEDIATE;
30 | /**
31 | * http请求是否自动跳转到https
32 | */
33 | private boolean httpToHttps = true;
34 |
35 | public boolean isEnable() {
36 | return enable;
37 | }
38 |
39 | public void setEnable(boolean enable) {
40 | this.enable = enable;
41 | }
42 |
43 | public int getPort() {
44 | return port;
45 | }
46 |
47 | public void setPort(int port) {
48 | this.port = port;
49 | }
50 |
51 | public Shutdown getShutdown() {
52 | return shutdown;
53 | }
54 |
55 | public void setShutdown(Shutdown shutdown) {
56 | this.shutdown = shutdown;
57 | }
58 |
59 | public boolean isHttpToHttps() {
60 | return httpToHttps;
61 | }
62 |
63 | public void setHttpToHttps(boolean httpToHttps) {
64 | this.httpToHttps = httpToHttps;
65 | }
66 | }
67 |
68 | ```
69 |
70 | ##### 2.配置NettyWebServer服务配置类,实现独立端口及自动跳转https
71 |
72 | ```java
73 | package com.emily.cloud.gateway.config;
74 |
75 | import com.emily.infrastructure.gateway.config.ServerProperties;import org.apache.commons.lang3.StringUtils;
76 | import org.springframework.beans.factory.annotation.Autowired;
77 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
78 | import org.springframework.boot.context.properties.EnableConfigurationProperties;
79 | import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory;
80 | import org.springframework.boot.web.server.WebServer;
81 | import org.springframework.context.annotation.Configuration;
82 | import org.springframework.http.server.reactive.HttpHandler;
83 | import reactor.core.publisher.Mono;
84 |
85 | import javax.annotation.PostConstruct;
86 | import javax.annotation.PreDestroy;
87 | import java.net.URI;
88 | import java.net.URISyntaxException;
89 |
90 | /**
91 | * @program: EmilyGateway
92 | * @description: 开启http端口
93 | * @create: 2021/01/13
94 | */
95 | @Configuration(proxyBeanMethods = false)
96 | @EnableConfigurationProperties(ServerProperties.class)
97 | @ConditionalOnProperty(prefix = "server.http", name = "enable", havingValue = "true", matchIfMissing = true)
98 | public class NettyWebServerAutoConfiguration {
99 | @Autowired
100 | private HttpHandler httpHandler;
101 | @Autowired
102 | private ServerProperties serverProperties;
103 |
104 | private WebServer webServer;
105 |
106 | @PostConstruct
107 | public void start() {
108 | NettyReactiveWebServerFactory factory = new NettyReactiveWebServerFactory(serverProperties.getPort());
109 | //设置关机模式
110 | factory.setShutdown(serverProperties.getShutdown());
111 | if (serverProperties.isHttpToHttps()) {
112 | webServer = factory.getWebServer(((request, response) -> {
113 | URI uri = request.getURI();
114 | try {
115 | response.getHeaders().setLocation(new URI(StringUtils.replace(uri.toString(), "http", "https")));
116 | } catch (URISyntaxException e) {
117 | return Mono.error(e);
118 | }
119 | return response.setComplete();
120 | }));
121 | } else {
122 | factory.getWebServer(httpHandler);
123 | }
124 | webServer.start();
125 | }
126 |
127 | @PreDestroy
128 | public void stop() {
129 | webServer.stop();
130 | }
131 |
132 | }
133 |
134 | ```
135 |
136 | 将上述两个加入系统之中,然后启动就可以实现开启独立http端口及http自动跳转https的能力;
137 |
138 | GitHub地址:[https://github.com/mingyang66/EmilyGateway](https://github.com/mingyang66/EmilyGateway)
139 |
140 |
--------------------------------------------------------------------------------
/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2007-present the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | import java.util.Properties;
18 |
19 | public class MavenWrapperDownloader {
20 |
21 | private static final String WRAPPER_VERSION = "0.5.6";
22 | /**
23 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
24 | */
25 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/"
26 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar";
27 |
28 | /**
29 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
30 | * use instead of the default one.
31 | */
32 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
33 | ".mvn/wrapper/maven-wrapper.properties";
34 |
35 | /**
36 | * Path where the maven-wrapper.jar will be saved to.
37 | */
38 | private static final String MAVEN_WRAPPER_JAR_PATH =
39 | ".mvn/wrapper/maven-wrapper.jar";
40 |
41 | /**
42 | * Name of the property which should be used to override the default download url for the wrapper.
43 | */
44 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
45 |
46 | public static void main(String args[]) {
47 | System.out.println("- Downloader started");
48 | File baseDirectory = new File(args[0]);
49 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
50 |
51 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
52 | // wrapperUrl parameter.
53 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
54 | String url = DEFAULT_DOWNLOAD_URL;
55 | if (mavenWrapperPropertyFile.exists()) {
56 | FileInputStream mavenWrapperPropertyFileInputStream = null;
57 | try {
58 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
59 | Properties mavenWrapperProperties = new Properties();
60 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
61 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
62 | } catch (IOException e) {
63 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
64 | } finally {
65 | try {
66 | if (mavenWrapperPropertyFileInputStream != null) {
67 | mavenWrapperPropertyFileInputStream.close();
68 | }
69 | } catch (IOException e) {
70 | // Ignore ...
71 | }
72 | }
73 | }
74 | System.out.println("- Downloading from: " + url);
75 |
76 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
77 | if (!outputFile.getParentFile().exists()) {
78 | if (!outputFile.getParentFile().mkdirs()) {
79 | System.out.println(
80 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'");
81 | }
82 | }
83 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
84 | try {
85 | downloadFileFromURL(url, outputFile);
86 | System.out.println("Done");
87 | System.exit(0);
88 | } catch (Throwable e) {
89 | System.out.println("- Error downloading");
90 | e.printStackTrace();
91 | System.exit(1);
92 | }
93 | }
94 |
95 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
96 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) {
97 | String username = System.getenv("MVNW_USERNAME");
98 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray();
99 | Authenticator.setDefault(new Authenticator() {
100 | @Override
101 | protected PasswordAuthentication getPasswordAuthentication() {
102 | return new PasswordAuthentication(username, password);
103 | }
104 | });
105 | }
106 | URL website = new URL(urlString);
107 | ReadableByteChannel rbc;
108 | rbc = Channels.newChannel(website.openStream());
109 | FileOutputStream fos = new FileOutputStream(destination);
110 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
111 | fos.close();
112 | rbc.close();
113 | }
114 |
115 | }
116 |
--------------------------------------------------------------------------------
/doc/死磕源码系列【springcloud gateway网关缓存请求body及删除缓存body之AdaptCachedBodyGlobalFilter和RemoveCachedBodyFilter过滤器】.md:
--------------------------------------------------------------------------------
1 | 死磕源码系列【springcloud gateway网关缓存请求body及删除缓存body之AdaptCachedBodyGlobalFilter和RemoveCachedBodyFilter过滤器】
2 |
3 | > RemoveCachedBodyFilter和AdaptCachedBodyGlobalFilter是两个全局过滤器,在网关过滤器链中RemoveCachedBodyFilter优先级最高,AdaptCachedBodyGlobalFilter次之,所以每次请求发送过来先将网关上线文中的请求body删除,然后再从请求中获取body缓存到网关上线文,属性是CACHED_REQUEST_BODY_ATTR(cachedRequestBody),这样就可以直接从网关上下文中拿到请求参数,而不会出现从request中拿到之后还要回填到请求体的问题;
4 |
5 | ##### 1.AdaptCachedBodyGlobalFilter缓存请求体全局过滤器
6 |
7 | ```java
8 | public class AdaptCachedBodyGlobalFilter implements GlobalFilter, Ordered, ApplicationListener {
9 |
10 | private ConcurrentMap routesToCache = new ConcurrentHashMap<>();
11 |
12 | @Override
13 | public void onApplicationEvent(EnableBodyCachingEvent event) {
14 | this.routesToCache.putIfAbsent(event.getRouteId(), true);
15 | }
16 |
17 | @Override
18 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
19 | // the cached ServerHttpRequest is used when the ServerWebExchange can not be
20 | // mutated, for example, during a predicate where the body is read, but still
21 | // needs to be cached.
22 | ServerHttpRequest cachedRequest = exchange.getAttributeOrDefault(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR,
23 | null);
24 | if (cachedRequest != null) {
25 | exchange.getAttributes().remove(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR);
26 | return chain.filter(exchange.mutate().request(cachedRequest).build());
27 | }
28 |
29 | //
30 | DataBuffer body = exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_ATTR, null);
31 | Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
32 |
33 | if (body != null || !this.routesToCache.containsKey(route.getId())) {
34 | return chain.filter(exchange);
35 | }
36 | //此处是缓存过滤器的核心,在此工具方法中会将缓存存入网关上下文之中
37 | return ServerWebExchangeUtils.cacheRequestBody(exchange, (serverHttpRequest) -> {
38 | // don't mutate and build if same request object
39 | if (serverHttpRequest == exchange.getRequest()) {
40 | return chain.filter(exchange);
41 | }
42 | return chain.filter(exchange.mutate().request(serverHttpRequest).build());
43 | });
44 | }
45 |
46 | @Override
47 | public int getOrder() {
48 | return Ordered.HIGHEST_PRECEDENCE + 1000;
49 | }
50 |
51 | }
52 |
53 | ```
54 |
55 | ##### ServerWebExchangeUtils#cacheRequestBody工具方法
56 |
57 | ```java
58 | public static Mono cacheRequestBody(ServerWebExchange exchange,
59 | Function> function) {
60 | return cacheRequestBody(exchange, false, function);
61 | }
62 |
63 | private static Mono cacheRequestBody(ServerWebExchange exchange, boolean cacheDecoratedRequest,
64 | Function> function) {
65 | ServerHttpResponse response = exchange.getResponse();
66 | NettyDataBufferFactory factory = (NettyDataBufferFactory) response.bufferFactory();
67 | // 将所有的DataBuffer拼接起来,这样我们可以有一个完整的body
68 | return DataBufferUtils.join(exchange.getRequest().getBody())
69 | .defaultIfEmpty(factory.wrap(new EmptyByteBuf(factory.getByteBufAllocator())))
70 | //此处decorate方法中会将缓存放入网关上下文
71 | .map(dataBuffer -> decorate(exchange, dataBuffer, cacheDecoratedRequest))
72 | .switchIfEmpty(Mono.just(exchange.getRequest())).flatMap(function);
73 | }
74 |
75 | private static ServerHttpRequest decorate(ServerWebExchange exchange, DataBuffer dataBuffer,
76 | boolean cacheDecoratedRequest) {
77 | if (dataBuffer.readableByteCount() > 0) {
78 | if (log.isTraceEnabled()) {
79 | log.trace("retaining body in exchange attribute");
80 | }
81 | //此处会将请求的body信息放入网关上下文,方便后面获取
82 | exchange.getAttributes().put(CACHED_REQUEST_BODY_ATTR, dataBuffer);
83 | }
84 |
85 | ServerHttpRequest decorator = new ServerHttpRequestDecorator(exchange.getRequest()) {
86 | @Override
87 | public Flux getBody() {
88 | return Mono.fromSupplier(() -> {
89 | if (exchange.getAttributeOrDefault(CACHED_REQUEST_BODY_ATTR, null) == null) {
90 | // probably == downstream closed or no body
91 | return null;
92 | }
93 | // TODO: deal with Netty
94 | NettyDataBuffer pdb = (NettyDataBuffer) dataBuffer;
95 | return pdb.factory().wrap(pdb.getNativeBuffer().retainedSlice());
96 | }).flux();
97 | }
98 | };
99 | if (cacheDecoratedRequest) {
100 | exchange.getAttributes().put(CACHED_SERVER_HTTP_REQUEST_DECORATOR_ATTR, decorator);
101 | }
102 | return decorator;
103 | }
104 | ```
105 |
106 | ##### 2.RemoveCachedBodyFilter删除缓存过滤器
107 |
108 | ```java
109 |
110 | public class RemoveCachedBodyFilter implements GlobalFilter, Ordered {
111 |
112 | private static final Log log = LogFactory.getLog(RemoveCachedBodyFilter.class);
113 |
114 | @Override
115 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
116 | return chain.filter(exchange).doFinally(s -> {
117 | //将网关上下文中的请求body删除
118 | Object attribute = exchange.getAttributes().remove(CACHED_REQUEST_BODY_ATTR);
119 | if (attribute != null && attribute instanceof PooledDataBuffer) {
120 | PooledDataBuffer dataBuffer = (PooledDataBuffer) attribute;
121 | if (dataBuffer.isAllocated()) {
122 | if (log.isTraceEnabled()) {
123 | log.trace("releasing cached body in exchange attribute");
124 | }
125 | //释放内存
126 | dataBuffer.release();
127 | }
128 | }
129 | });
130 | }
131 |
132 | @Override
133 | public int getOrder() {
134 | return HIGHEST_PRECEDENCE;
135 | }
136 |
137 | }
138 | ```
139 |
140 | > 此过滤器最简单,只做了一件事,就是删除网关上下文中的缓存;
141 |
142 | GitHub地址:[https://github.com/mingyang66/EmilyGateway](https://github.com/mingyang66/EmilyGateway)
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/exception/handler/WebFluxErrorAttributes.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.exception.handler;
2 |
3 | import org.springframework.boot.web.error.ErrorAttributeOptions;
4 | import org.springframework.boot.web.reactive.error.DefaultErrorAttributes;
5 | import org.springframework.core.annotation.MergedAnnotation;
6 | import org.springframework.core.annotation.MergedAnnotations;
7 | import org.springframework.http.HttpStatus;
8 | import org.springframework.util.StringUtils;
9 | import org.springframework.validation.BindingResult;
10 | import org.springframework.web.bind.annotation.ResponseStatus;
11 | import org.springframework.web.reactive.function.server.ServerRequest;
12 | import org.springframework.web.server.ResponseStatusException;
13 |
14 | import java.io.PrintWriter;
15 | import java.io.StringWriter;
16 | import java.util.Date;
17 | import java.util.LinkedHashMap;
18 | import java.util.Map;
19 |
20 | /**
21 | *
22 | * @author Emily
23 | */
24 | public class WebFluxErrorAttributes extends DefaultErrorAttributes {
25 | private final Boolean includeException;
26 |
27 | public WebFluxErrorAttributes(boolean includeException) {
28 | this.includeException = includeException;
29 | }
30 |
31 | @Override
32 | public Map getErrorAttributes(ServerRequest request, ErrorAttributeOptions options) {
33 | Map errorAttributes = getErrorAttributes(request, options.isIncluded(ErrorAttributeOptions.Include.STACK_TRACE));
34 | if (Boolean.TRUE.equals(includeException)) {
35 | options = options.including(ErrorAttributeOptions.Include.EXCEPTION);
36 | }
37 | if (!options.isIncluded(ErrorAttributeOptions.Include.EXCEPTION)) {
38 | errorAttributes.remove("exception");
39 | }
40 | if (!options.isIncluded(ErrorAttributeOptions.Include.STACK_TRACE)) {
41 | errorAttributes.remove("trace");
42 | }
43 | if (!options.isIncluded(ErrorAttributeOptions.Include.MESSAGE) && errorAttributes.get("message") != null) {
44 | errorAttributes.put("message", "");
45 | }
46 | if (!options.isIncluded(ErrorAttributeOptions.Include.BINDING_ERRORS)) {
47 | errorAttributes.remove("errors");
48 | }
49 | return errorAttributes;
50 | }
51 |
52 | public Map getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
53 | Map errorAttributes = new LinkedHashMap();
54 | Throwable error = getError(request);
55 | errorAttributes.put("timestamp", new Date());
56 | errorAttributes.put("path", request.path());
57 | MergedAnnotation responseStatusAnnotation = MergedAnnotations
58 | .from(error.getClass(), MergedAnnotations.SearchStrategy.TYPE_HIERARCHY).get(ResponseStatus.class);
59 | HttpStatus errorStatus = determineHttpStatus(error, responseStatusAnnotation);
60 | errorAttributes.put("status", errorStatus.value());
61 | errorAttributes.put("error", errorStatus.getReasonPhrase());
62 | errorAttributes.put("message", determineMessage(error, responseStatusAnnotation));
63 | errorAttributes.put("requestId", request.exchange().getRequest().getId());
64 | handleException(errorAttributes, determineException(error), includeStackTrace);
65 |
66 | return errorAttributes;
67 | }
68 |
69 |
70 | private HttpStatus determineHttpStatus(Throwable error, MergedAnnotation responseStatusAnnotation) {
71 | if (error instanceof ResponseStatusException) {
72 | return ((ResponseStatusException) error).getStatus();
73 | }
74 | return responseStatusAnnotation.getValue("code", HttpStatus.class).orElse(HttpStatus.INTERNAL_SERVER_ERROR);
75 |
76 | }
77 |
78 | private String determineMessage(Throwable error, MergedAnnotation responseStatusAnnotation) {
79 | if (error instanceof BindingResult) {
80 | return error.getMessage();
81 | }
82 | if (error instanceof ResponseStatusException) {
83 | return ((ResponseStatusException) error).getReason();
84 | }
85 | String reason = responseStatusAnnotation.getValue("reason", String.class).orElse("");
86 | if (StringUtils.hasText(reason)) {
87 | return reason;
88 | }
89 | return (error.getMessage() != null) ? error.getMessage() : "";
90 | }
91 |
92 | private Throwable determineException(Throwable error) {
93 | if (error instanceof ResponseStatusException) {
94 | return error.getCause() != null ? error.getCause() : error;
95 | } else {
96 | return error;
97 | }
98 | }
99 |
100 | private void addStackTrace(Map errorAttributes, Throwable error) {
101 | StringWriter stackTrace = new StringWriter();
102 | error.printStackTrace(new PrintWriter(stackTrace));
103 | stackTrace.flush();
104 | errorAttributes.put("trace", stackTrace.toString());
105 | }
106 |
107 | private void handleException(Map errorAttributes, Throwable error, boolean includeStackTrace) {
108 | errorAttributes.put("exception", error.getClass().getName());
109 | if (includeStackTrace) {
110 | this.addStackTrace(errorAttributes, error);
111 | }
112 |
113 | if (error instanceof BindingResult) {
114 | BindingResult result = (BindingResult) error;
115 | if (result.hasErrors()) {
116 | errorAttributes.put("errors", result.getAllErrors());
117 | }
118 | }
119 |
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | ## Emily网关服务
4 |
5 | #### 一、项目依赖版本
6 |
7 | - JDK11
8 | - springboot2.6.7
9 | - 基础库组件4.0.10
10 | - springcloud3.1.0
11 | - springcloud gateway3.1.2
12 | - Resilience4j2.1.2
13 |
14 | #### 二、支持功能
15 |
16 | - 支持同时启动两个端口,一个http端口一个https端口,支持由http跳转到https;
17 | - 支持全局捕获网关异常,打印日志到指定的日志文件,方便查问题;
18 | - 支持基于Resilience4j的断路器熔断支持;
19 | - 支持跨域全局设置;
20 | - 支持对网关http|https对下游请求相关参数设置,如:连接超时时间等;
21 |
22 | #### 三、网关配置示例
23 |
24 | 请参考项目目录下的application系列配置文件
25 |
26 | #### 四、断言工厂配置示例
27 |
28 | ##### 1.Path路由断言工厂
29 |
30 | > 路由断言工厂类有两个参数,patterns(基于spring的PathMatcher)、matchTrailingSlash(是否匹配斜杠,默认:true)
31 |
32 | ```yaml
33 | spring:
34 | cloud:
35 | gateway:
36 | routes:
37 | - id: path_route
38 | uri: https://example.org
39 | predicates:
40 | - Path=/red/{segment},/blue/{segment}
41 | ```
42 |
43 | - 支持'/foo/{*foobar}' 和 '/{*foobar}'格式,由CaptureTheRestPathElement提供支持;
44 | - 支持'/foo/{bar}/goo'格式,将一段变量作为路径元素,由CaptureVariablePathElement提供支持;
45 | - 支持'/foo/bar/goo'格式,由LiteralPathElement提供支持;
46 | - 支持'/foo/*/goo'通配符格式,*代表至少匹配一个字符,由WildcardPathElement提供支持;
47 | - 支持'/foo/**' 和 /** Rest通配符格式,匹配0个或者多个目录,由WildcardTheRestPathElement提供支持;
48 | - 支持'/foo/??go'单字符通配符格式,一个?代表单个字符,若需要适配多个可用多个?标识,由SingleCharWildcardedPathElement提供支持;
49 | - 支持/foo/*_*/*\_{foobar}格式,由RegexPathElement提供支持;
50 |
51 | ##### 2.Method路由断言工厂
52 |
53 | ```yaml
54 | spring:
55 | cloud:
56 | gateway:
57 | routes:
58 | - id: method_route
59 | uri: https://example.org
60 | predicates:
61 | - Method=GET,POST
62 | ```
63 |
64 | > Method路由断言工厂有一个参数Metod,指定多个支持的请求方法;支持的方法:GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS, TRACE
65 |
66 | ##### 3.Weight权重路由断言工厂
67 |
68 | ```yaml
69 | spring:
70 | cloud:
71 | gateway:
72 | routes:
73 | - id: weight_high
74 | uri: https://weighthigh.org
75 | predicates:
76 | - Weight=group1, 8
77 | - id: weight_low
78 | uri: https://weightlow.org
79 | predicates:
80 | - Weight=group1, 2
81 | ```
82 |
83 | > Weight权重路由断言工厂有两个参数group(分组名称)和weight(权重 int类型),每组按照权重计算流量
84 |
85 | ##### 4.After路由断言工厂类
86 |
87 | > After路由断言工厂类带有一个参数datetime(是ZonedDateTime类型),此断言工厂类判定所有请求只有发生在指定的时间之后才符合条件;
88 |
89 | ```yaml
90 | spring:
91 | cloud:
92 | gateway:
93 | routes:
94 | - id: after_route
95 | uri: https://example.org
96 | predicates:
97 | - After=2022-04-20T15:35:08.721398+08:00[Asia/Shanghai]
98 | ```
99 |
100 | 获取带区域时间方法如下:
101 |
102 | ```java
103 | public static void main(String[] args) {
104 | ZonedDateTime zbj = ZonedDateTime.now();
105 | ZonedDateTime zny = ZonedDateTime.now(ZoneId.of("America/New_York"));
106 | System.out.println(zbj);
107 | System.out.println(zny);
108 | }
109 | ```
110 |
111 | ##### 5.Before路由断言工厂类
112 |
113 | > Before路由断言工厂类只有一个ZonedDateTime类型参数datetime,所有的请求只会发生在datetime之前;
114 |
115 | ```yaml
116 | spring:
117 | cloud:
118 | gateway:
119 | routes:
120 | - id: before_route
121 | uri: https://example.org
122 | predicates:
123 | - Before=2017-01-20T17:42:47.789-07:00[America/Denver]
124 | ```
125 |
126 | ##### 6.Between路由断言工厂类
127 |
128 | > Between路由断言工厂类有两个ZonedDateTime类型参数datetime1、datetime2,请求必须发生在datetime1和datetime2之间,datetime2必须小于datetime1;
129 |
130 | ```
131 | spring:
132 | cloud:
133 | gateway:
134 | routes:
135 | - id: between_route
136 | uri: https://example.org
137 | predicates:
138 | - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
139 | ```
140 |
141 | ##### 6.RemoteAddr路由断言工厂类
142 |
143 | > RemoteAddr路由断言工厂类有一个列表类型参数sources,可以指定路由请求的IP地址;其中192.168.1.1是IP地址,24是子网掩码;
144 |
145 | ```yaml
146 | spring:
147 | cloud:
148 | gateway:
149 | routes:
150 | - id: remoteaddr_route
151 | uri: https://example.org
152 | predicates:
153 | - RemoteAddr=192.168.1.1/24
154 | ```
155 |
156 | #### 五、过滤器工厂配置示例
157 |
158 | ##### 1.DedupeResponseHeader消除重复响应头过滤器工厂类
159 |
160 | > DedupeResponseHeader过滤器工厂类有两个可选参数name和strategy,name可以指定用逗号分隔的header名称列表。strategy指定保留重复header的策略,有三个选择RETAIN_FIRST-保留第一个值(默认)、RETAIN_LAST-保留最后一个值、RETAIN_UNIQUE-保留唯一值
161 |
162 | ```yaml
163 | spring:
164 | cloud:
165 | gateway:
166 | routes:
167 | - id: dedupe_response_header_route
168 | uri: https://example.org
169 | filters:
170 | - DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
171 | ```
172 |
173 | ##### 2.PreserveHostHeader传递原始Host过滤器工厂
174 |
175 | > PreserveHostHeader 过滤器工厂没有参数,此过滤器设置一个属性来决定是否发送原始Host,而不是通过Http客户端来决定Host。
176 |
177 | ```yaml
178 | spring:
179 | cloud:
180 | gateway:
181 | routes:
182 | - id: preserve_host_route
183 | uri: https://example.org
184 | filters:
185 | - PreserveHostHeader
186 | ```
187 |
188 | #### 六、故障诊断
189 |
190 | ##### 1.以下包可通过设置日志级别进行重要故障诊断信息打印
191 |
192 | - `org.springframework.cloud.gateway`
193 | - `org.springframework.http.server.reactive`
194 | - `org.springframework.web.reactive`
195 | - `org.springframework.boot.autoconfigure.web`
196 | - `reactor.netty`
197 | - `redisratelimiter`
198 |
199 | 以上包可以通过如下配置开启日志调试模式:
200 |
201 | ```yaml
202 | logging:
203 | level:
204 | reactor.netty: debug
205 | ```
206 |
207 | Reactor Netty HttpClient和HttpServer可以开启监控,当包reactor.netty日志级别设置为DEBUG或TRACE结合起来后,它可以打印相关日志信息,如:请求header和body,接收到的响应信息,以下是开启监控模式配置:
208 |
209 | ```
210 | spring:
211 | cloud:
212 | gateway:
213 | httpclient:
214 | # 为Netty HttpClient启用监听调试,默认:false
215 | wiretap: true
216 | httpserver:
217 | # 为Netty HttpServer开启监听debugging模式,默认:false
218 | wiretap: true
219 | ```
220 |
221 |
--------------------------------------------------------------------------------
/src/main/java/com/emily/infrastructure/gateway/config/filter/logger/RecordLoggerGatewayFilterFactory.java:
--------------------------------------------------------------------------------
1 | package com.emily.infrastructure.gateway.config.filter.logger;
2 |
3 | import com.emily.infrastructure.gateway.common.entity.BaseLogger;
4 | import com.emily.infrastructure.gateway.config.filter.order.GatewayFilterOrdered;
5 | import com.emily.infrastructure.json.JsonUtils;
6 | import org.slf4j.Logger;
7 | import org.slf4j.LoggerFactory;
8 | import org.springframework.cloud.gateway.event.EnableBodyCachingEvent;
9 | import org.springframework.cloud.gateway.filter.GatewayFilter;
10 | import org.springframework.cloud.gateway.filter.GatewayFilterChain;
11 | import org.springframework.cloud.gateway.filter.NettyWriteResponseFilter;
12 | import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
13 | import org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory;
14 | import org.springframework.cloud.gateway.route.Route;
15 | import org.springframework.validation.annotation.Validated;
16 | import org.springframework.web.server.ServerWebExchange;
17 | import reactor.core.publisher.Mono;
18 |
19 | import java.util.Arrays;
20 | import java.util.HashSet;
21 | import java.util.List;
22 | import java.util.Set;
23 |
24 | import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR;
25 |
26 | /**
27 | * @author Emily
28 | * @program: EmilyGateway
29 | * @description: 网关全局过滤器,拦截请求响应日志
30 | * @create: 2020/12/22
31 | */
32 | public class RecordLoggerGatewayFilterFactory extends AbstractGatewayFilterFactory{
33 |
34 | private static final Logger logger = LoggerFactory.getLogger(RecordLoggerGatewayFilterFactory.class);
35 |
36 | public static final String KEY_PREFIX = "enabled";
37 | public static final String KEY_EXCLUDE_PATH = "excludePath";
38 | /**
39 | * 日志实体对象
40 | */
41 | public static final String BASE_LOGGER = "BASE_LOGGER";
42 | /**
43 | * 请求开始时间
44 | */
45 | public static final String START_TIME = "START_TIME";
46 |
47 | public RecordLoggerGatewayFilterFactory() {
48 | super(RecordLoggerGatewayFilterFactory.Config.class);
49 | }
50 |
51 | @Override
52 | public List shortcutFieldOrder() {
53 | return Arrays.asList(KEY_PREFIX, KEY_EXCLUDE_PATH);
54 | }
55 |
56 | @Override
57 | public GatewayFilter apply(RecordLoggerGatewayFilterFactory.Config config) {
58 |
59 | return new GatewayFilterOrdered() {
60 | @Override
61 | public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
62 | // 判定是否开启日志记录
63 | if (!config.isEnabled()) {
64 | return chain.filter(exchange);
65 | }
66 | Route route = exchange.getAttribute(GATEWAY_ROUTE_ATTR);
67 | if (route != null && getPublisher() != null) {
68 | // send an event to enable caching
69 | getPublisher().publishEvent(new EnableBodyCachingEvent(this, route.getId()));
70 | }
71 | exchange.getAttributes().put(BASE_LOGGER, new BaseLogger(exchange));
72 | exchange.getAttributes().put(START_TIME, System.currentTimeMillis());
73 | /**
74 | * 获取响应结果有两种方案:
75 | * 1.就是如下自定义修饰类的方式获取响应结果
76 | * 2.通过重写 {@link ModifyResponseBodyGatewayFilterFactory.ModifiedServerHttpResponse}修饰类的方式来实现
77 | */
78 | return chain.filter(exchange.mutate().response(new RecordLoggerResponseDecorator(exchange)).build())
79 | //如果Mono在没有数据的情况下完成,则要调用的回调参数为null
80 | .doOnSuccess((args) -> {
81 | BaseLogger baseLogger = exchange.getAttribute(BASE_LOGGER);
82 | // 设置响应时间
83 | baseLogger.setTime(System.currentTimeMillis() - exchange.getAttributeOrDefault(START_TIME, 0L));
84 | // 记录日志信息
85 | logger.info(JsonUtils.toJSONString(baseLogger));
86 | })
87 | // 当Mono完成并出现错误时触发,将会发送onError信号
88 | .doOnError(throwable -> {
89 | BaseLogger baseLogger = exchange.getAttribute(BASE_LOGGER);
90 | // 设置响应时间
91 | baseLogger.setTime(System.currentTimeMillis() - exchange.getAttributeOrDefault(START_TIME, 0L));
92 | // 设置返回的错误信息
93 | baseLogger.setResponseBody(throwable.getMessage());
94 | // 记录日志信息
95 | logger.error(JsonUtils.toJSONString(baseLogger));
96 | });
97 | }
98 |
99 | @Override
100 | public int getOrder() {
101 | return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER - 1;
102 | }
103 | };
104 |
105 | }
106 |
107 |
108 | @Validated
109 | public static class Config {
110 | /**
111 | * 是否开启日志记录,默认:true
112 | */
113 | private boolean enabled = true;
114 | /**
115 | * 排除请求URL
116 | */
117 | private Set excludePath = new HashSet<>();
118 |
119 | public boolean isEnabled() {
120 | return enabled;
121 | }
122 |
123 | public void setEnabled(boolean enabled) {
124 | this.enabled = enabled;
125 | }
126 |
127 | public Set getExcludePath() {
128 | return excludePath;
129 | }
130 |
131 | public void setExcludePath(Set excludePath) {
132 | this.excludePath = excludePath;
133 | }
134 | }
135 | }
136 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.6.11
9 |
10 |
11 | com.emily.infrastructure
12 | gateway
13 | 1.0.3
14 | EmilyGateway
15 | Gateway网关
16 |
17 |
18 |
19 | 11
20 |
21 | 11
22 |
23 | 11
24 |
25 | 3.1.2
26 |
27 | 2.1.3
28 |
29 | 4.3.4
30 |
31 |
32 |
33 |
34 | io.github.mingyang66
35 | oceansky-common
36 | ${emily.infrastructure.version}
37 |
38 |
39 | io.github.mingyang66
40 | oceansky-json
41 | ${emily.infrastructure.version}
42 |
43 |
44 | com.google.guava
45 | guava
46 | 32.0.1-jre
47 |
48 |
49 | org.apache.commons
50 | commons-lang3
51 | 3.12.0
52 |
53 |
54 | org.springframework.boot
55 | spring-boot-configuration-processor
56 |
57 |
58 | org.springframework.boot
59 | spring-boot-starter-actuator
60 |
61 |
62 | org.springframework.cloud
63 | spring-cloud-starter-gateway
64 | ${spring.cloud.version}
65 |
66 |
67 |
68 | org.springframework.cloud
69 | spring-cloud-starter-consul-discovery
70 | ${spring.cloud.version}
71 |
72 |
73 |
74 | org.springframework.cloud
75 | spring-cloud-starter-consul-config
76 | ${spring.cloud.version}
77 |
78 |
79 |
80 | org.springframework.cloud
81 | spring-cloud-starter-bootstrap
82 | ${spring.cloud.version}
83 |
84 |
85 |
86 | org.springframework.cloud
87 | spring-cloud-starter-circuitbreaker-reactor-resilience4j
88 | ${spring.resilience4j.version}
89 |
90 |
91 | com.github.ben-manes.caffeine
92 | caffeine
93 |
94 |
95 |
96 |
97 | gateway
98 |
99 |
100 |
101 | src/main/resources
102 |
107 | true
108 |
109 |
110 |
111 |
112 | org.springframework.boot
113 | spring-boot-maven-plugin
114 |
115 |
116 | maven-resources-plugin
117 | 3.2.0
118 |
119 | utf-8
120 |
121 | true
122 |
123 |
124 | p12
125 | cer
126 | pem
127 | pfx
128 | jkx
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
124 |
125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
162 | if ERRORLEVEL 1 goto error
163 | goto end
164 |
165 | :error
166 | set ERROR_CODE=1
167 |
168 | :end
169 | @endlocal & set ERROR_CODE=%ERROR_CODE%
170 |
171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
175 | :skipRcPost
176 |
177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause
179 |
180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
181 |
182 | exit /B %ERROR_CODE%
183 |
--------------------------------------------------------------------------------
/src/main/resources/logback.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | INFO
17 |
18 |
19 |
20 | ${pattern}
21 |
22 | utf8
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | INFO
31 | ACCEPT
32 | DENY
33 |
34 |
35 |
36 | ${pattern}
37 |
38 | utf8
39 |
40 | false
41 |
42 |
43 | ${log.path}/info/info.log
44 |
45 | true
46 |
47 |
56 | ${log.path}/info/info.%d{yyyy-MM-dd}.log
57 |
58 | ${log.date}
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | ERROR
67 | ACCEPT
68 | DENY
69 |
70 |
71 |
72 | ${pattern}
73 |
74 | utf8
75 |
76 | false
77 |
78 |
79 | ${log.path}/error/error.log
80 |
81 | true
82 |
83 |
84 | ${log.path}/error/error.%d{yyyy-MM-dd}.log
85 |
86 | ${log.date}
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 | WARN
95 | ACCEPT
96 | DENY
97 |
98 |
99 |
100 | ${pattern}
101 |
102 | utf8
103 |
104 | false
105 |
106 |
107 | ${log.path}/warn/warn.log
108 |
109 | true
110 |
111 |
112 | ${log.path}/warn/warn.%d{yyyy-MM-dd}.log
113 |
114 | ${log.date}
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 | DEBUG
123 | ACCEPT
124 | DENY
125 |
126 |
127 |
128 | ${pattern}
129 |
130 | utf8
131 |
132 | false
133 |
134 |
135 | ${log.path}/debug/debug.log
136 |
137 | true
138 |
139 |
140 | ${log.path}/debug/debug.%d{yyyy-MM-dd}.log
141 |
142 | ${log.date}
143 |
144 |
145 |
146 |
147 | ${log.path}/access/access.log
148 |
149 | %msg%n
150 |
151 |
152 | true
153 |
154 |
155 | ${log.path}/access/access.%d{yyyy-MM-dd}.log
156 |
157 | ${log.date}
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
--------------------------------------------------------------------------------
/src/main/java/org/springframework/http/server/reactive/ReactorServerHttpRequest.java:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2002-2022 the original author or authors.
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License");
5 | * you may not use this file except in compliance with the License.
6 | * You may obtain a copy of the License at
7 | *
8 | * https://www.apache.org/licenses/LICENSE-2.0
9 | *
10 | * Unless required by applicable law or agreed to in writing, software
11 | * distributed under the License is distributed on an "AS IS" BASIS,
12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | * See the License for the specific language governing permissions and
14 | * limitations under the License.
15 | */
16 |
17 | package org.springframework.http.server.reactive;
18 |
19 | import io.netty.channel.Channel;
20 | import io.netty.handler.codec.http.HttpHeaderNames;
21 | import io.netty.handler.codec.http.cookie.Cookie;
22 | import io.netty.handler.ssl.SslHandler;
23 | import org.apache.commons.logging.Log;
24 | import org.springframework.core.io.buffer.DataBuffer;
25 | import org.springframework.core.io.buffer.NettyDataBufferFactory;
26 | import org.springframework.http.HttpCookie;
27 | import org.springframework.http.HttpLogging;
28 | import org.springframework.lang.Nullable;
29 | import org.springframework.util.Assert;
30 | import org.springframework.util.ClassUtils;
31 | import org.springframework.util.LinkedMultiValueMap;
32 | import org.springframework.util.MultiValueMap;
33 | import reactor.core.publisher.Flux;
34 | import reactor.netty.Connection;
35 | import reactor.netty.http.server.HttpServerRequest;
36 |
37 | import javax.net.ssl.SSLSession;
38 | import java.net.InetSocketAddress;
39 | import java.net.URI;
40 | import java.net.URISyntaxException;
41 | import java.util.concurrent.atomic.AtomicLong;
42 |
43 | /**
44 | * Adapt {@link ServerHttpRequest} to the Reactor {@link HttpServerRequest}.
45 | *
46 | * @author Stephane Maldini
47 | * @author Rossen Stoyanchev
48 | * @since 5.0
49 | */
50 | class ReactorServerHttpRequest extends AbstractServerHttpRequest {
51 |
52 | /** Reactor Netty 1.0.5+. */
53 | static final boolean reactorNettyRequestChannelOperationsIdPresent = ClassUtils.isPresent(
54 | "reactor.netty.ChannelOperationsId", ReactorServerHttpRequest.class.getClassLoader());
55 |
56 | private static final Log logger = HttpLogging.forLogName(ReactorServerHttpRequest.class);
57 |
58 |
59 | private static final AtomicLong logPrefixIndex = new AtomicLong();
60 |
61 |
62 | private final HttpServerRequest request;
63 |
64 | private final NettyDataBufferFactory bufferFactory;
65 |
66 |
67 | public ReactorServerHttpRequest(HttpServerRequest request, NettyDataBufferFactory bufferFactory)
68 | throws URISyntaxException {
69 |
70 | super(initUri(request), "", new NettyHeadersAdapter(request.requestHeaders()));
71 | Assert.notNull(bufferFactory, "DataBufferFactory must not be null");
72 | this.request = request;
73 | this.bufferFactory = bufferFactory;
74 | }
75 |
76 | private static URI initUri(HttpServerRequest request) throws URISyntaxException {
77 | Assert.notNull(request, "HttpServerRequest must not be null");
78 | String resolveRequestUri = resolveRequestUri(request);
79 | if(resolveRequestUri.contains("^")){
80 | resolveRequestUri = resolveRequestUri.replace("^", "%5E");
81 | }
82 | if(resolveRequestUri.contains("|")){
83 | resolveRequestUri = resolveRequestUri.replace("|", "%7C");
84 | }
85 | if(resolveRequestUri.contains("{")){
86 | resolveRequestUri = resolveRequestUri.replace("{", "%7B");
87 | }
88 | if(resolveRequestUri.contains("}")){
89 | resolveRequestUri = resolveRequestUri.replace("}", "%7D");
90 | }
91 | return new URI(resolveBaseUrl(request) + resolveRequestUri);
92 | }
93 |
94 | private static URI resolveBaseUrl(HttpServerRequest request) throws URISyntaxException {
95 | String scheme = getScheme(request);
96 | String header = request.requestHeaders().get(HttpHeaderNames.HOST);
97 | if (header != null) {
98 | final int portIndex;
99 | if (header.startsWith("[")) {
100 | portIndex = header.indexOf(':', header.indexOf(']'));
101 | }
102 | else {
103 | portIndex = header.indexOf(':');
104 | }
105 | if (portIndex != -1) {
106 | try {
107 | return new URI(scheme, null, header.substring(0, portIndex),
108 | Integer.parseInt(header.substring(portIndex + 1)), null, null, null);
109 | }
110 | catch (NumberFormatException ex) {
111 | throw new URISyntaxException(header, "Unable to parse port", portIndex);
112 | }
113 | }
114 | else {
115 | return new URI(scheme, header, null, null);
116 | }
117 | }
118 | else {
119 | InetSocketAddress localAddress = request.hostAddress();
120 | Assert.state(localAddress != null, "No host address available");
121 | return new URI(scheme, null, localAddress.getHostString(),
122 | localAddress.getPort(), null, null, null);
123 | }
124 | }
125 |
126 | private static String getScheme(HttpServerRequest request) {
127 | return request.scheme();
128 | }
129 |
130 | private static String resolveRequestUri(HttpServerRequest request) {
131 | String uri = request.uri();
132 | for (int i = 0; i < uri.length(); i++) {
133 | char c = uri.charAt(i);
134 | if (c == '/' || c == '?' || c == '#') {
135 | break;
136 | }
137 | if (c == ':' && (i + 2 < uri.length())) {
138 | if (uri.charAt(i + 1) == '/' && uri.charAt(i + 2) == '/') {
139 | for (int j = i + 3; j < uri.length(); j++) {
140 | c = uri.charAt(j);
141 | if (c == '/' || c == '?' || c == '#') {
142 | return uri.substring(j);
143 | }
144 | }
145 | return "";
146 | }
147 | }
148 | }
149 | return uri;
150 | }
151 |
152 |
153 | @Override
154 | public String getMethodValue() {
155 | return this.request.method().name();
156 | }
157 |
158 | @Override
159 | protected MultiValueMap initCookies() {
160 | MultiValueMap cookies = new LinkedMultiValueMap<>();
161 | for (CharSequence name : this.request.cookies().keySet()) {
162 | for (Cookie cookie : this.request.cookies().get(name)) {
163 | HttpCookie httpCookie = new HttpCookie(name.toString(), cookie.value());
164 | cookies.add(name.toString(), httpCookie);
165 | }
166 | }
167 | return cookies;
168 | }
169 |
170 | @Override
171 | @Nullable
172 | public InetSocketAddress getLocalAddress() {
173 | return this.request.hostAddress();
174 | }
175 |
176 | @Override
177 | @Nullable
178 | public InetSocketAddress getRemoteAddress() {
179 | return this.request.remoteAddress();
180 | }
181 |
182 | @Override
183 | @Nullable
184 | protected SslInfo initSslInfo() {
185 | Channel channel = ((Connection) this.request).channel();
186 | SslHandler sslHandler = channel.pipeline().get(SslHandler.class);
187 | if (sslHandler == null && channel.parent() != null) { // HTTP/2
188 | sslHandler = channel.parent().pipeline().get(SslHandler.class);
189 | }
190 | if (sslHandler != null) {
191 | SSLSession session = sslHandler.engine().getSession();
192 | return new DefaultSslInfo(session);
193 | }
194 | return null;
195 | }
196 |
197 | @Override
198 | public Flux getBody() {
199 | return this.request.receive().retain().map(this.bufferFactory::wrap);
200 | }
201 |
202 | @SuppressWarnings("unchecked")
203 | @Override
204 | public T getNativeRequest() {
205 | return (T) this.request;
206 | }
207 |
208 | @Override
209 | @Nullable
210 | protected String initId() {
211 | if (this.request instanceof Connection) {
212 | return ((Connection) this.request).channel().id().asShortText() +
213 | "-" + logPrefixIndex.incrementAndGet();
214 | }
215 | return null;
216 | }
217 |
218 | @Override
219 | protected String initLogPrefix() {
220 | if (reactorNettyRequestChannelOperationsIdPresent) {
221 | String id = (ChannelOperationsIdHelper.getId(this.request));
222 | if (id != null) {
223 | return id;
224 | }
225 | }
226 | if (this.request instanceof Connection) {
227 | return ((Connection) this.request).channel().id().asShortText() +
228 | "-" + logPrefixIndex.incrementAndGet();
229 | }
230 | return getId();
231 | }
232 |
233 |
234 | private static class ChannelOperationsIdHelper {
235 |
236 | @Nullable
237 | public static String getId(HttpServerRequest request) {
238 | if (request instanceof reactor.netty.ChannelOperationsId) {
239 | return (logger.isDebugEnabled() ?
240 | ((reactor.netty.ChannelOperationsId) request).asLongText() :
241 | ((reactor.netty.ChannelOperationsId) request).asShortText());
242 | }
243 | return null;
244 | }
245 | }
246 |
247 | }
248 |
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /etc/mavenrc ] ; then
40 | . /etc/mavenrc
41 | fi
42 |
43 | if [ -f "$HOME/.mavenrc" ] ; then
44 | . "$HOME/.mavenrc"
45 | fi
46 |
47 | fi
48 |
49 | # OS specific support. $var _must_ be set to either true or false.
50 | cygwin=false;
51 | darwin=false;
52 | mingw=false
53 | case "`uname`" in
54 | CYGWIN*) cygwin=true ;;
55 | MINGW*) mingw=true;;
56 | Darwin*) darwin=true
57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
59 | if [ -z "$JAVA_HOME" ]; then
60 | if [ -x "/usr/libexec/java_home" ]; then
61 | export JAVA_HOME="`/usr/libexec/java_home`"
62 | else
63 | export JAVA_HOME="/Library/Java/Home"
64 | fi
65 | fi
66 | ;;
67 | esac
68 |
69 | if [ -z "$JAVA_HOME" ] ; then
70 | if [ -r /etc/gentoo-release ] ; then
71 | JAVA_HOME=`java-config --jre-home`
72 | fi
73 | fi
74 |
75 | if [ -z "$M2_HOME" ] ; then
76 | ## resolve links - $0 may be a link to maven's home
77 | PRG="$0"
78 |
79 | # need this for relative symlinks
80 | while [ -h "$PRG" ] ; do
81 | ls=`ls -ld "$PRG"`
82 | link=`expr "$ls" : '.*-> \(.*\)$'`
83 | if expr "$link" : '/.*' > /dev/null; then
84 | PRG="$link"
85 | else
86 | PRG="`dirname "$PRG"`/$link"
87 | fi
88 | done
89 |
90 | saveddir=`pwd`
91 |
92 | M2_HOME=`dirname "$PRG"`/..
93 |
94 | # make it fully qualified
95 | M2_HOME=`cd "$M2_HOME" && pwd`
96 |
97 | cd "$saveddir"
98 | # echo Using m2 at $M2_HOME
99 | fi
100 |
101 | # For Cygwin, ensure paths are in UNIX format before anything is touched
102 | if $cygwin ; then
103 | [ -n "$M2_HOME" ] &&
104 | M2_HOME=`cygpath --unix "$M2_HOME"`
105 | [ -n "$JAVA_HOME" ] &&
106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
107 | [ -n "$CLASSPATH" ] &&
108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
109 | fi
110 |
111 | # For Mingw, ensure paths are in UNIX format before anything is touched
112 | if $mingw ; then
113 | [ -n "$M2_HOME" ] &&
114 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
115 | [ -n "$JAVA_HOME" ] &&
116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
117 | fi
118 |
119 | if [ -z "$JAVA_HOME" ]; then
120 | javaExecutable="`which javac`"
121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
122 | # readlink(1) is not available as standard on Solaris 10.
123 | readLink=`which readlink`
124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
125 | if $darwin ; then
126 | javaHome="`dirname \"$javaExecutable\"`"
127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
128 | else
129 | javaExecutable="`readlink -f \"$javaExecutable\"`"
130 | fi
131 | javaHome="`dirname \"$javaExecutable\"`"
132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
133 | JAVA_HOME="$javaHome"
134 | export JAVA_HOME
135 | fi
136 | fi
137 | fi
138 |
139 | if [ -z "$JAVACMD" ] ; then
140 | if [ -n "$JAVA_HOME" ] ; then
141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
142 | # IBM's JDK on AIX uses strange locations for the executables
143 | JAVACMD="$JAVA_HOME/jre/sh/java"
144 | else
145 | JAVACMD="$JAVA_HOME/bin/java"
146 | fi
147 | else
148 | JAVACMD="`which java`"
149 | fi
150 | fi
151 |
152 | if [ ! -x "$JAVACMD" ] ; then
153 | echo "Error: JAVA_HOME is not defined correctly." >&2
154 | echo " We cannot execute $JAVACMD" >&2
155 | exit 1
156 | fi
157 |
158 | if [ -z "$JAVA_HOME" ] ; then
159 | echo "Warning: JAVA_HOME environment variable is not set."
160 | fi
161 |
162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
163 |
164 | # traverses directory structure from process work directory to filesystem root
165 | # first directory with .mvn subdirectory is considered project base directory
166 | find_maven_basedir() {
167 |
168 | if [ -z "$1" ]
169 | then
170 | echo "Path not specified to find_maven_basedir"
171 | return 1
172 | fi
173 |
174 | basedir="$1"
175 | wdir="$1"
176 | while [ "$wdir" != '/' ] ; do
177 | if [ -d "$wdir"/.mvn ] ; then
178 | basedir=$wdir
179 | break
180 | fi
181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
182 | if [ -d "${wdir}" ]; then
183 | wdir=`cd "$wdir/.."; pwd`
184 | fi
185 | # end of workaround
186 | done
187 | echo "${basedir}"
188 | }
189 |
190 | # concatenates all lines of a file
191 | concat_lines() {
192 | if [ -f "$1" ]; then
193 | echo "$(tr -s '\n' ' ' < "$1")"
194 | fi
195 | }
196 |
197 | BASE_DIR=`find_maven_basedir "$(pwd)"`
198 | if [ -z "$BASE_DIR" ]; then
199 | exit 1;
200 | fi
201 |
202 | ##########################################################################################
203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
204 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
205 | ##########################################################################################
206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
207 | if [ "$MVNW_VERBOSE" = true ]; then
208 | echo "Found .mvn/wrapper/maven-wrapper.jar"
209 | fi
210 | else
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
213 | fi
214 | if [ -n "$MVNW_REPOURL" ]; then
215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
216 | else
217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar"
218 | fi
219 | while IFS="=" read key value; do
220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
221 | esac
222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
223 | if [ "$MVNW_VERBOSE" = true ]; then
224 | echo "Downloading from: $jarUrl"
225 | fi
226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
227 | if $cygwin; then
228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
229 | fi
230 |
231 | if command -v wget > /dev/null; then
232 | if [ "$MVNW_VERBOSE" = true ]; then
233 | echo "Found wget ... using wget"
234 | fi
235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
236 | wget "$jarUrl" -O "$wrapperJarPath"
237 | else
238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath"
239 | fi
240 | elif command -v curl > /dev/null; then
241 | if [ "$MVNW_VERBOSE" = true ]; then
242 | echo "Found curl ... using curl"
243 | fi
244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
245 | curl -o "$wrapperJarPath" "$jarUrl" -f
246 | else
247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
248 | fi
249 |
250 | else
251 | if [ "$MVNW_VERBOSE" = true ]; then
252 | echo "Falling back to using Java to download"
253 | fi
254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
255 | # For Cygwin, switch paths to Windows format before running javac
256 | if $cygwin; then
257 | javaClass=`cygpath --path --windows "$javaClass"`
258 | fi
259 | if [ -e "$javaClass" ]; then
260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
261 | if [ "$MVNW_VERBOSE" = true ]; then
262 | echo " - Compiling MavenWrapperDownloader.java ..."
263 | fi
264 | # Compiling the Java class
265 | ("$JAVA_HOME/bin/javac" "$javaClass")
266 | fi
267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
268 | # Running the downloader
269 | if [ "$MVNW_VERBOSE" = true ]; then
270 | echo " - Running MavenWrapperDownloader.java ..."
271 | fi
272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
273 | fi
274 | fi
275 | fi
276 | fi
277 | ##########################################################################################
278 | # End of extension
279 | ##########################################################################################
280 |
281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
282 | if [ "$MVNW_VERBOSE" = true ]; then
283 | echo $MAVEN_PROJECTBASEDIR
284 | fi
285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
286 |
287 | # For Cygwin, switch paths to Windows format before running java
288 | if $cygwin; then
289 | [ -n "$M2_HOME" ] &&
290 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
291 | [ -n "$JAVA_HOME" ] &&
292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
293 | [ -n "$CLASSPATH" ] &&
294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
295 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
297 | fi
298 |
299 | # Provide a "standardized" way to retrieve the CLI args that will
300 | # work with both Windows and non-Windows executions.
301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
302 | export MAVEN_CMD_LINE_ARGS
303 |
304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
305 |
306 | exec "$JAVACMD" \
307 | $MAVEN_OPTS \
308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
311 |
--------------------------------------------------------------------------------
/src/main/resources/config-bak/application-gateway.yml:
--------------------------------------------------------------------------------
1 | spring:
2 | cloud:
3 | gateway:
4 | metrics:
5 | # 开启指标收集
6 | enabled: true
7 | loadbalancer:
8 | # 负载均衡503服务不可用时将状态码更换为404,默认:false
9 | use404: false
10 | #set-status:
11 | # 结合SetStatus过滤器,将原始返回的状态码放入指定的响应header
12 | #original-status-header-name: original-http-status
13 | httpclient:
14 | # 连接超时,单位毫秒,默认:45s
15 | connect-timeout: 45000
16 | # 响应超时时间,{@link java.time.Duration}类型
17 | response-timeout: PT1S
18 | # 最大响应头大小,默认:8192,单位:字节B
19 | max-header-size: 8192
20 | # 最大初始行大小,单位:字节B
21 | max-initial-line-length: 4096
22 | pool:
23 | # channel pool映射名称,默认:proxy
24 | name: proxy
25 | # HttpClient要使用的池的类型,默认:elastic
26 | type: elastic
27 | # 空闲请求在空闲多久后会被回收,默认:毫秒 -Dreactor.netty.pool.leasingStrategy=lifo
28 | max-idle-time: PT1S
29 | ssl:
30 | # 信任下游的所有证书TLS, 默认:false
31 | use-insecure-trust-manager: true
32 | globalcors:
33 | corsConfigurations:
34 | '[/**]':
35 | # 是否允许携带认证信息,默认:false
36 | allowCredentials: false
37 | # 允许跨域的源(网站域名|IP),设置*为全部
38 | allowedOrigins: "*"
39 | #- "https://127.0.0.1"
40 | #- https://localhost
41 | #- http://61.152.230.66:80
42 | # 允许跨域的源正则表达式匹配
43 | #allowedOriginPatterns: "*"
44 | # 允许跨域的method,设置*为全部
45 | allowedMethods:
46 | - GET
47 | - POST
48 | - PUT
49 | - OPTIONS
50 | # 允许跨域秦秋的Header字段,设置*为全部
51 | allowedHeaders: "*"
52 | # 发生跨域问题时展示的响应header信息,默认:null,设置*为所有
53 | #exposedHeaders: "*"
54 | # 配置客户端预检请求响应缓存时间,单位:秒(默认:1800)
55 | maxAge: 1800
56 | default-filters:
57 | # 消除重复的请求header
58 | - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Headers Vary, RETAIN_UNIQUE
59 | routes:
60 | # 自定义路由ID,保持唯一
61 | - id: EmilyFramework
62 | #目标服务地址
63 | uri: lb://EmilyFramework
64 | #顺序,当请求匹配到多个路由时,使用顺序小的
65 | order: 0
66 | # 断言,路由条件,
67 | predicates:
68 | # 根据路由断言请求的目标服务器
69 | - Path=/emilyframework/api/**
70 | # 根据分组及百分比断言请求服务器的权重,group, 8(80%)
71 | #- Weight=EmilyFrameworkGroup, 50
72 | # 根据请求Host断言
73 | - Host=127.0.0.**
74 | # 根据请求头断言
75 | #- Header=Content-Type, ^\w*/\w*$
76 | # 根据请求Method头断言
77 | - Method=POST,GET
78 | # 根据请求参数断言,请求参数在路径后拼接模式
79 | #- Query=name,emily*
80 | filters:
81 | # 删除前缀过滤器
82 | - StripPrefix=1
83 | # 添加指定前缀过滤器
84 | #- PrefixPath=/api
85 | # 在发送请求之前,所有请求添加参数
86 | #- AddRequestParameter=name,emily
87 | # 添加多个参数过滤器
88 | #- AddRequestParameter=age,1
89 | # 添加请求Header过滤器
90 | - AddRequestHeader=token,emily-token-request
91 | # 添加响应Header过滤器
92 | - AddResponseHeader=token,emily-token-response
93 | # 删除请求Header过滤器
94 | - RemoveRequestHeader=token
95 | # 删除响应参数过滤器
96 | - RemoveResponseHeader=token
97 | # 删除请求参数过滤器
98 | #- RemoveRequestParameter=age,name
99 | # 无论哪种情况,响应的HTTP状态都设置为指定值,必须为HttpStatus中指定
100 | #- SetStatus=401
101 | # 添加请求头(或者修改)
102 | - SetRequestHeader=X-Request-Red, Yellow
103 | # 替换指定的Header(或者添加)过滤器
104 | - SetResponseHeader=X-Response-Red, Blue
105 | # 指定覆盖请求Header Host的值过滤器
106 | #- SetRequestHostHeader=xxx.org
107 | # 设置重定向地址url
108 | #- RedirectTo=200, http://www.baidu.com
109 | # 请求大小限制过滤器,默认是:5MB
110 | - name: RequestSize
111 | args:
112 | # 最大请求大小,单位默认:B(支持org.springframework.util.unit.DataUnit类中定义的单位)
113 | maxSize: 5MB
114 | # 请求Header大小限制过滤器,默认是:16000B
115 | - name: RequestHeaderSize
116 | args:
117 | # 最大请求大小,单位默认是:B(支持org.springframework.util.unit.DataUnit类中定义的单位)
118 | maxSize: 16000B
119 | # 通过指定的Header更改请求URL过滤器
120 | - RequestHeaderToRequestUri=X-New-Url
121 | # 将符合正则表达式的URL重写为指定的URL
122 | #- RewritePath=/api/test2, /api/test4
123 | - name: RewritePath
124 | args:
125 | # 原URL,支持正则表达式
126 | regexp: /api/test2
127 | # 将要被替换的URL
128 | replacement: /api/test2
129 | # 重写响应Header过滤器,支持正则表达式匹配
130 | #- RewriteResponseHeader=X-Response-Header, Emily, Lovely Emily
131 | - name: RewriteResponseHeader
132 | args:
133 | # Header名称
134 | name: X-Response-Header
135 | # Header中的内容,支持正则表达式
136 | regexp: Emily
137 | # 替换的内容
138 | replacement: Lovely
139 | # 重写指定响应Header的hostValue
140 | #- RewriteLocationResponseHeader=AS_IN_REQUEST, Location, 127.0.0.1:8866, http
141 | - name: RewriteLocationResponseHeader
142 | args:
143 | # 默认:AS_IN_REQUEST - 如果源请求URL不包含版本号,则去除版本号;
144 | # NEVER_STRIP - 即使源URL不包含版本号,也不会去除版本号;
145 | # ALWAYS_STRIP - 即使源URL包含版本号,也会去除版本号;
146 | stripVersion: AS_IN_REQUEST
147 | # 位置地址Header名
148 | locationHeaderName: Location
149 | # 如果设置,则将会替换Location Header头中的host:port,否则,将会使用请求Header的Host
150 | hostValue: 127.0.0.1:8080
151 | # 协议,默认https?|ftps?
152 | protocols: http
153 | # 限流过滤器
154 | - name: RequestRateLimiter
155 | # 限流参数如果想设置稳定的请求速度,可以将replenishRate和burstCapacity设置为一致
156 | # 允许burstCapacity的值高于replenishRate的值,反之则无效
157 | args:
158 | # 希望允许用户在没有丢弃任何请求的情况下每秒执行的请求数,也是令牌桶的填充速度
159 | redis-rate-limiter.replenishRate: 1000
160 | # 令牌桶的容量,允许在一秒钟内完成的最大请求数量,将此值设置为0将组织所有请求,默认:1
161 | redis-rate-limiter.burstCapacity: 1000
162 | # 一个请求需要多少令牌,这是每个请求从bucket中获取的令牌数,默认:1
163 | redis-rate-limiter.requestedTokens: 1
164 | # 设置限流依据beanName
165 | key-resolver: '#{@ipAddressKeyResolver}'
166 | # 断路器配置
167 | #- name: CircuitBreaker
168 | # args:
169 | # name: emilyCircuitBreaker
170 | # 支持控制器、schema地址
171 | # fallbackUri: forward:/defaultFallback
172 | # 根据返回的状态码熔断降级
173 | #statusCodes:
174 | # - 500
175 | #- 404
176 | # 失败重试过滤器
177 | - name: Retry
178 | args:
179 | #应尝试重试的次数
180 | retries: 1
181 | # 应该重试的HTTP方法,用{@link org.springframework.http.HttpMethod},默认:GET
182 | methods: GET,POST
183 | # 应该重试的Http状态码,使用{@link org.springframework.http.HttpStatus}
184 | statuses:
185 | #- INTERNAL_SERVER_ERROR
186 | # 状态码配置,符合末端状态码才会进行重试逻辑,默认值是5(SERVER_ERROR)也就是5XX开头的状态码,
187 | # 参考{@link org.springframework.http.HttpStatus.Series}
188 | series:
189 | - INFORMATIONAL
190 | #- SUCCESSFUL
191 | - REDIRECTION
192 | #- CLIENT_ERROR
193 | - SERVER_ERROR
194 | # 抛出如下列表中的异常将会进行重试,默认是:IOException、TimeoutException
195 | exceptions:
196 | - java.io.IOException
197 | - org.springframework.cloud.gateway.support.TimeoutException
198 | # 如果basedOnPreviousValue为true,下次重试的计算规则是prevBackoff * factor,但是最大只能为maxBackoff
199 | # 如果basedOnPreviousValue为false
200 | # 下次重试等待时间,第一次为firstBackoff * (factor ^ (n-1)),n为迭代的次数,但是最大只能为maxBackoff
201 | backoff:
202 | # 第一次重试等待时间
203 | firstBackoff: 10ms
204 | # 重试最大等待时间
205 | maxBackoff: 50ms
206 | # 因子
207 | factor: 2
208 | # 是否基于上次重试等待时间计算下次重试等待时间
209 | basedOnPreviousValue: false
210 | # 自定义路由ID,保持唯一
211 | - id: consul-demo
212 | #目标服务地址
213 | uri: lb://consul-demo
214 | #顺序,当请求匹配到多个路由时,使用顺序小的
215 | order: 0
216 | # 断言,路由条件,
217 | predicates:
218 | - Path=/api/**
219 | # 根据分组及百分比断言请求服务器的权重,group, 8(80%)
220 | #- Weight=EmilyFrameworkGroup, 50
221 | # 根据请求Host断言
222 | - Host=127.0.0.**
223 | # 根据请求Method断言
224 | - Method=POST,GET
225 | # 根据请求参数断言,请求参数在路径后拼接模式
226 | #- Query=name,emily*
227 | filters:
228 | # 删除前缀过滤器
229 | - StripPrefix=1
230 | # 添加指定前缀过滤器
231 | - PrefixPath=/api
232 | - name: Retry
233 | args:
234 | retries: 3
235 | methods: GET,POST
236 | statuses: INTERNAL_SERVER_ERROR
237 | series:
238 | - SERVER_ERROR
239 | # 抛出如下列表中的异常将会进行重试,默认是:IOException、TimeoutException
240 | exceptions:
241 | - java.lang.Exception
242 | - org.springframework.cloud.gateway.support.TimeoutException
243 | backoff:
244 | firstBackoff: 10ms
245 | maxBackoff: 50ms
246 | factor: 2
247 | basedOnPreviousValue: false
248 | metadata:
249 | # 连接超时时间,单位:毫秒
250 | connect-timeout: 1000
251 | # 响应超时时间,单位:毫秒
252 | response-timeout: 5000
253 | # RabbitMQ服务配置
254 | - id: rabbitmq2
255 | uri: http://127.0.0.1:15672
256 | predicates:
257 | - Path=/rabbitmq/**
258 | - Weight=rabbitmqGroup, 40
259 | filters:
260 | - StripPrefix=1
261 | - id: rabbitmq3
262 | uri: http://127.0.0.1:15673
263 | predicates:
264 | - Path=/rabbitmq/**
265 | - Weight=rabbitmqGroup, 30
266 | filters:
267 | - StripPrefix=1
268 | - id: rabbitmq4
269 | uri: http://127.0.0.1:15674
270 | predicates:
271 | - Path=/rabbitmq/**
272 | - Weight=rabbitmqGroup, 30
273 | filters:
274 | - StripPrefix=1
275 | # Consul服务配置
276 | - id: consul
277 | uri: http://127.0.0.1:8500
278 | predicates:
279 | - Path=/ui/**,/v1/**
280 | filters:
281 | - name: EmilyExternal
282 | args:
283 | enable: true
284 | path: /ui/**, /v1/**
285 | metadata:
286 | # 连接超时时间,单位:毫秒
287 | connect-timeout: 60000
288 | # 响应超时时间,单位:毫秒
289 | response-timeout: 60000
290 | - id: websocket
291 | uri: wss://127.0.0.1:9000/websocket/connect
292 | predicates:
293 | - Path=/websocket/**
294 | filters:
295 | #- StripPrefix=1
296 | - id: file
297 | uri: http://127.0.0.1:9000/api/dataDownload
298 | predicates:
299 | - Path=/file/**
300 | filters:
301 | - StripPrefix=1
302 |
--------------------------------------------------------------------------------
/doc/gateway/Resilience4j CircuitBreaker断路器.md:
--------------------------------------------------------------------------------
1 | #### 一、CircuitBreaker断路器介绍
2 |
3 | CircuitBreaker断路器通过具有三种正常状态的有限状态机实现:CLOSED、OPEN、HALF_OPEN和两种特殊的状态DISABLED和FORCED_OPEN;
4 |
5 | 
6 |
7 | CircuitBreaker断路器使用滑动窗口存储和汇总调用结果,你可以在基于时间(time-based)的滑动窗口和基于计数(count-based)的滑动窗口之间做选择。基于计数的滑动窗口会汇总最后N次调用的结果,基于时间的滑动窗口会汇总最后N秒的调用结果。
8 |
9 | #### 二、基于数量(count-based)的滑动窗口
10 |
11 | 基于计数(count-based)的滑动窗口由N个状态组成的圆形数组组成,如果窗口计数值是10,说明圆形数组计数为10次。滑动窗口以增量的方式更新汇总计数,汇总计数会在新的调用结果返回后更新。当旧的计数被逐出时,会从总的计数中减去计数值,并重置存储桶。
12 |
13 | #### 三、基于时间(time-based)的滑动窗口
14 |
15 | 基于时间(time-based)的滑动窗口由N个状态组成的圆形数组组成,如果窗口时间是10秒,说明圆形数组统计时间为10秒。每个bucket汇总在某一秒内发生的所有调用的结果。循环数组的头bucket存储第二个轮回的返回结果,
16 |
17 | 滑动窗口不单独存储调用结果,而是增量更新部分聚合(bucket)和总聚合(bucket)。
18 |
19 | 当记录新的调用结果时,总聚合将以增量的方式更新。逐出最旧的存储桶时,将从总聚合中减去该存储桶的部分总聚合,并重置该存储桶。
20 |
21 | #### 四、故障率和慢调用阀值
22 |
23 | 当故障率大于等于配置的阀值时CircuitBreaker断路器的状态会由CLOSED转为OPEN。例如:记录的故障率大于50%时,默认所有的异常都视为故障,你可以定义一个可以被视为故障的异常列表。其它所有的异常都会计为成功,除非被忽略。异常也可以被忽略,因此其即可以不计入异常也不计入成功。
24 |
25 | 当慢调用的百分比大于等于配置的阀值时CircuitBreaker断路器会由CLOSED转为OPEN。例如:当调用记录的50%耗时超过5秒,这有助于在外部系统实际无响应之前减少其负载。
26 |
27 | 如果记录了最少的调用次数,故障率和慢调用只可以通过计算判定。例如:如果配置最小调用次数是10,那么最少要记录10次,在故障率计算出来之前,如果评估了9次调用,即使所有9次调用均失败,断路器状态也不会转为OPEN。
28 |
29 | 当CircuitBreaker断路器状态为OPEN时将会拒绝调用并抛出CallNotPermittedException异常,等待一段时间后,CircuitBreaker断路器状态由OPEN转为HALF_OPEN并允许进行可配置的调用次数,以查看后端是否仍然不可用或已恢复可用。在所有的允许调用完成后,更多的调用将会被拒绝并抛出CallPermittedException异常。
30 |
31 | 如果故障率或慢调用率大于等于配置的阀值,断路器的状态变为OPEN。如果故障率和慢调用率低于配置的阀值,断路器的状态变为CLOSED。
32 |
33 | CircuitBreaker断路器支持另外两种特殊的状态,DISABLED(一直允许访问)和FORCED_OPEN(一直拒绝访问)。在这两种状态下,不会产生断路器事件(状态转换除外),也不会记录任何指标。退出这些状态的唯一方法是触发状态转换或重置断路器。
34 |
35 | CircuitBreaker断路器是线程安全的,如下所示:
36 |
37 | - CircuitBreaker断路器的状态存储在AtomicReference;
38 | - CircuitBreaker断路器使用原子操作更新状态;
39 | - 通过滑动窗口同步记录调用次数和读取快照;
40 |
41 | 这意味着原子性应该得到保证,一个时间点只能有一个线程更新状态或滑动窗口;
42 |
43 | 然而CircuitBreaker断路器不是同步调用函数,这也就意味着函数调用不是关键部分;否则,断路器将会遇到巨大的性能损失和瓶颈,慢的函数调用会对整体性能/吞吐量造成巨大的负面影响;
44 |
45 | 如果20个并发线程请求执行一个函数的权限,并且CircuitBreaker断路器的状态为CLOSED,则允许所有的线程调用该函数。即使滑动窗口的大小是15,滑动窗口并不意味着只允许同时运行15个调用,如果要限制并发线程的数量,请使用Bulkhead,你可以把Bulkhead和CircuitBreaker断路器结合起来。
46 |
47 | 一个线程示例:
48 |
49 | 
50 |
51 | 三个线程示例:
52 |
53 | 
54 |
55 | #### 五、创建一个CircuitBreakerRegistry实例
56 |
57 | Resilience4j附带了一个基于ConcurrentHashMap内存的CircuitBreakerRegistry,它提供了线程安全和原子性保证,你可以使用CircuitBreakerRegistry来管理(创建和检索)断路器实例。你可以为所有断路器实例创建全局默认CircuitBreakerConfig的CircuitBreakerRegistry实例;
58 |
59 | ```java
60 | CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
61 | ```
62 |
63 | 六、创建并配置CircuitBreaker断路器
64 |
65 | 你可以提供自定义的全局配置CircuitBreakerConfig,要创建自定义全局CircuitBreakerConfig,可以使用CircuitBreakerConfig builder生成器,可以使用builder配置如下属性:
66 |
67 | | 配置属性 | 默认值 | 描述 |
68 | | -------------------------------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
69 | | failureRateThreshold | 50 | 配置故障率阀值百分比。当故障率大于等于CircuitBreaker断路器阀值状态转为OPEN,并且开启短路调用状态; |
70 | | slowCallRateThreshold | 100 | 配置慢查询阀值百分比,CircuitBreaker断路器将调用持续时间大于等于slowCallDurationThreshold阀值的调用视为慢查询,当慢查询百分比大于等于阀值,CircuitBreaker断路器转为OPEN状态,并且开启短路调用状态。 |
71 | | slowCallDurationThreshold | 60000[ms] | 配置调用持续时间阀值,超过阀值的调用将被视为慢调用,并增加慢查询百分比 |
72 | | permittedNumberOfCallsInHalfOpenState | 10 | 当CircuitBreaker断路器处于Half-Open(半开)状态时允许的正常调用次数。 |
73 | | maxWaitDurationInHalfOpenState | 0[ms] | 配置最大等待持续时间,以控制断路器在切换至OPEN状态之前保持HALF OPEN状态的最长时间。值为0标识断路器将在半开状态下无限等待,直到所有允许的调用完成。 |
74 | | slidingWindowType | COUNT_BASED | 配置滑动窗口的类型,当断路器closed时用于记录调用结果;滑动窗口可以是count-based或time-based; 如果滑动窗口是COUNT_BASED,最后的slidingWindowSize将会以次数为单位计算和聚合次数。如果滑动窗口是TIME_BASED的,最后的slidingWindowSize将以秒为单位记录和聚合; |
75 | | slidingWindowSize | 100 | 配置用于记录CircuitBreaker关闭时的调用次数(时间) |
76 | | minimumNumberOfCalls | 100 | 配置CircuitBreaker断路器计算故障率或慢查询率之前的最小调用次数(单个滑动窗口期);例如:如果minimumNumberOfCalls值为10,如果CircuitBreaker断路器记录了9次调用将不会转为OPEN状态,即使9次都失败 |
77 | | waitDurationInOpenState | 60000[ms] | 断路器由打开状态转为关闭状态需要的时间 |
78 | | automaticTransitionFromOpenToHalfOpenEnabled | false | 如果设置为true,则意味着断路器将自动从OPEN状态转换为HALF_OPEN状态,无需调用即可触发转换。创建一个线程来监视断路器的所有实例,以便在waitDurationInOpenState通过后将其转换为HALF_OPEN状态。然而如果设置为false,则只有在发出调用时才会转换为HALF_OPEN,即使waitDurationInOpenState被设置之后也是如此,优点是没有线程监视所有断路器的状态。 |
79 | | recordExceptions | empty | 记录为错误的异常列表用于增加故障率,任何匹配到的异常或者其子类都会被当做失败。除非通过ignoreExceptions忽略掉的异常,如果你指定了一个异常列表,所有其它的异常都会被计算为成功,除非他们被ignoreExceptions忽略。 |
80 | | ignoreExceptions | empty | 一个指定被忽略的异常列表,既不会被计入成功也不会计入失败,任何匹配的异常或者异常的子类都不会计入成功或者失败,即使异常时recordExceptions |
81 | | recordFailurePredicate | throwable->true,默认所有的异常都被记录为失败。 | 一个自定义断言,用于计算异常是否应该记录为失败。如果异常要记录为失败,则必须返回true;如果异常要记录为成功,则必须返回false;除非ignoreExceptions明确忽略该异常。 |
82 | | ignoreExceptionPredicate | throwable->false,默认没有异常会被忽略 | 一个自定义断言,用于判定是否被忽略,即不被视为失败也不被视为成功。如果异常要被忽略,则必须返回true;如果异常要被视为失败,则必须返回false。 |
83 |
84 | ```java
85 | // Create a custom configuration for a CircuitBreaker
86 | CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
87 | .failureRateThreshold(50)
88 | .slowCallRateThreshold(50)
89 | .waitDurationInOpenState(Duration.ofMillis(1000))
90 | .slowCallDurationThreshold(Duration.ofSeconds(2))
91 | .permittedNumberOfCallsInHalfOpenState(3)
92 | .minimumNumberOfCalls(10)
93 | .slidingWindowType(SlidingWindowType.TIME_BASED)
94 | .slidingWindowSize(5)
95 | .recordException(e -> INTERNAL_SERVER_ERROR
96 | .equals(getResponse().getStatus()))
97 | .recordExceptions(IOException.class, TimeoutException.class)
98 | .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
99 | .build();
100 |
101 | // Create a CircuitBreakerRegistry with a custom global configuration
102 | CircuitBreakerRegistry circuitBreakerRegistry =
103 | CircuitBreakerRegistry.of(circuitBreakerConfig);
104 |
105 | // Get or create a CircuitBreaker from the CircuitBreakerRegistry
106 | // with the global default configuration
107 | CircuitBreaker circuitBreakerWithDefaultConfig =
108 | circuitBreakerRegistry.circuitBreaker("name1");
109 |
110 | // Get or create a CircuitBreaker from the CircuitBreakerRegistry
111 | // with a custom configuration
112 | CircuitBreaker circuitBreakerWithCustomConfig = circuitBreakerRegistry
113 | .circuitBreaker("name2", circuitBreakerConfig);
114 | ```
115 |
116 | 你可以添加由多个断路器共享的实例配置:
117 |
118 | ```java
119 | CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
120 | .failureRateThreshold(70)
121 | .build();
122 |
123 | circuitBreakerRegistry.addConfiguration("someSharedConfig", config);
124 |
125 | CircuitBreaker circuitBreaker = circuitBreakerRegistry
126 | .circuitBreaker("name", "someSharedConfig");
127 | ```
128 |
129 | 可以覆盖配置:
130 |
131 | ```java
132 | CircuitBreakerConfig defaultConfig = circuitBreakerRegistry
133 | .getDefaultConfig();
134 |
135 | CircuitBreakerConfig overwrittenConfig = CircuitBreakerConfig
136 | .from(defaultConfig)
137 | .waitDurationInOpenState(Duration.ofSeconds(20))
138 | .build();
139 | ```
140 |
141 | 如果你不想使用CircuitBreakerRegistry管理CircuitBreaker实例,你可以直接创建实例对象:
142 |
143 | ```java
144 | // Create a custom configuration for a CircuitBreaker
145 | CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
146 | .recordExceptions(IOException.class, TimeoutException.class)
147 | .ignoreExceptions(BusinessException.class, OtherBusinessException.class)
148 | .build();
149 |
150 | CircuitBreaker customCircuitBreaker = CircuitBreaker
151 | .of("testName", circuitBreakerConfig);
152 | ```
153 |
154 | 另外,你可以使用CircuitBreakerRegistry的建造方法创建:
155 |
156 | ```java
157 | Map circuitBreakerTags = Map.of("key1", "value1", "key2", "value2");
158 |
159 | CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.custom()
160 | .withCircuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
161 | .addRegistryEventConsumer(new RegistryEventConsumer() {
162 | @Override
163 | public void onEntryAddedEvent(EntryAddedEvent entryAddedEvent) {
164 | // implementation
165 | }
166 | @Override
167 | public void onEntryRemovedEvent(EntryRemovedEvent entryRemoveEvent) {
168 | // implementation
169 | }
170 | @Override
171 | public void onEntryReplacedEvent(EntryReplacedEvent entryReplacedEvent) {
172 | // implementation
173 | }
174 | })
175 | .withTags(circuitBreakerTags)
176 | .build();
177 |
178 | CircuitBreaker circuitBreaker = circuitBreakerRegistry.circuitBreaker("testName");
179 | ```
180 |
181 | 如果你想插入自己的Registry实现,可以使用builder方法提供的接口RegistryStore和插件的自定义实现。
182 |
183 | ```java
184 | CircuitBreakerRegistry registry = CircuitBreakerRegistry.custom()
185 | .withRegistryStore(new YourRegistryStoreImplementation())
186 | .withCircuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
187 | .build();
188 | ```
189 |
190 | #### 六、修饰并执行函数式接口
191 |
192 | 你可以使用CircuitBreaker断路器修饰Callable、Supplier、Runnable、Consumer、CheckedRunnable、CheckedSupplier、CheckedConsumer或CompletionStage中的任何一个函数式接口。可以调用通过Try.of(...)或Try.run(...)修饰的函数,可以级联更多的函数,像map、flatMap、filter、recover或andThen,关联的函数仅仅是被调用。如果CircuitBreaker断路器的状态是CLOSED或HALF_OPEN,示例如下:
193 |
194 | ```java
195 | // Given
196 | CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("testName");
197 |
198 | // When I decorate my function
199 | CheckedFunction0 decoratedSupplier = CircuitBreaker
200 | .decorateCheckedSupplier(circuitBreaker, () -> "This can be any method which returns: 'Hello");
201 |
202 | // and chain an other function with map
203 | Try result = Try.of(decoratedSupplier)
204 | .map(value -> value + " world'");
205 |
206 | // Then the Try Monad returns a Success, if all functions ran successfully.
207 | assertThat(result.isSuccess()).isTrue();
208 | assertThat(result.get()).isEqualTo("This can be any method which returns: 'Hello world'");
209 | ```
210 |
211 | #### 七、消费发布的RegistryEvents时间
212 |
213 | 可以在CircuitBreakerRegistry上注册事件消费者,无论什么时候发生创建、替换或者删除都会触发消费;
214 |
215 | ```java
216 | CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.ofDefaults();
217 | circuitBreakerRegistry.getEventPublisher()
218 | .onEntryAdded(entryAddedEvent -> {
219 | CircuitBreaker addedCircuitBreaker = entryAddedEvent.getAddedEntry();
220 | LOG.info("CircuitBreaker {} added", addedCircuitBreaker.getName());
221 | })
222 | .onEntryRemoved(entryRemovedEvent -> {
223 | CircuitBreaker removedCircuitBreaker = entryRemovedEvent.getRemovedEntry();
224 | LOG.info("CircuitBreaker {} removed", removedCircuitBreaker.getName());
225 | });
226 | ```
227 |
228 | #### 八、消费发布的CircuitBreakerEvents事件
229 |
230 | CircuitBreakerEvent可以是状态转换、断路器重置、成功调用、记录的错误或忽略的错误。所有的时间都包含一些其它信息,如时间创建时间和调用处理持续时间,如果要消费事件,必须注册事件消费者。
231 |
232 | ```java
233 | circuitBreaker.getEventPublisher()
234 | .onSuccess(event -> logger.info(...))
235 | .onError(event -> logger.info(...))
236 | .onIgnoredError(event -> logger.info(...))
237 | .onReset(event -> logger.info(...))
238 | .onStateTransition(event -> logger.info(...));
239 | // Or if you want to register a consumer listening
240 | // to all events, you can do:
241 | circuitBreaker.getEventPublisher()
242 | .onEvent(event -> logger.info(...));
243 | ```
244 |
245 | 可以使用CircularEventConsumer将时间存储在具有固定容量的循环缓冲区中。
246 |
247 | ```java
248 | CircularEventConsumer ringBuffer =
249 | new CircularEventConsumer<>(10);
250 | circuitBreaker.getEventPublisher().onEvent(ringBuffer);
251 | List bufferedEvents = ringBuffer.getBufferedEvents()
252 | ```
253 |
254 | 九、覆盖RegistryStore
255 |
256 | 你可以通过自定义实现RegistryStore接口来覆盖基于内存实现的RegistryStore,例如:如果你想使用缓存,在一段时间后删除未使用的实例。
257 |
258 | ```java
259 | CircuitBreakerRegistry circuitBreakerRegistry = CircuitBreakerRegistry.custom()
260 | .withRegistryStore(new CacheCircuitBreakerRegistryStore())
261 | .build();
262 | ```
263 |
264 |
--------------------------------------------------------------------------------