├── 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 | ![在这里插入图片描述](https://img-blog.csdnimg.cn/20210224131207157.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3lhb21pbmd5YW5n,size_16,color_FFFFFF,t_70) 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 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 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 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 | ![请添加图片描述](https://img-blog.csdnimg.cn/69099d0a794f41d7b74b8d58c879eaf6.png) 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 | ![请添加图片描述](https://img-blog.csdnimg.cn/12d8af29f248488f84a2c6d9224462ce.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6Im-57Gz6I6JRW1pbHk=,size_17,color_FFFFFF,t_70,g_se,x_16) 50 | 51 | 三个线程示例: 52 | 53 | ![请添加图片描述](https://img-blog.csdnimg.cn/c472f571f6ec42c1bb55455b40d8d6b3.png?x-oss-process=image/watermark,type_d3F5LXplbmhlaQ,shadow_50,text_Q1NETiBA6Im-57Gz6I6JRW1pbHk=,size_17,color_FFFFFF,t_70,g_se,x_16) 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 | --------------------------------------------------------------------------------