1.pom文件
<org.springframework.boot.version>2.2.2.RELEASE</org.springframework.boot.version>
<org.springframework.boot.maven.version>2.1.0.RELEASE</org.springframework.boot.maven.version>
<org.springframework.cloud.version>Hoxton.SR1</org.springframework.cloud.version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- 降级熔断 resilience4j -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-circuitbreaker-reactor-resilience4j</artifactId>
</dependency>
<!--resilience4j-spring-boot2中包含了resilience4j 的所有功能 -->
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
</dependencies>
2.配置yml文件
gateway:
matcher:
whitelist: #gateway白名单
- /**/api-docs # swagger
- /consul-service/**
- /**/**.css
- /**/*.js
server:
port: 80
logging:
file: './log/gateway.log'
level:
root: ${LOG_LEVE:info}
spring:
application:
name: gateway
cloud:
consul:
enabled: true
host: localhost
port: 8500
discovery:
enabled: true # 启用服务发现
prefer-ip-address: true # 表示注册时使用IP而不是hostname
register: true #是否将自身服务注册到consul中
deregister: true # 服务停止时取消注册
hostname: localhost
health-check-path: /actuator/health #consul默认健康检查地址,可以通过management进行修改
health-check-interval: 15s #健康检查间隔,默认10s
health-check-critical-timeout: 30s # 设置健康检查失败多长时间后,取消注册
service-name: ${spring.application.name}
instance-id: ${spring.application.name}-${spring.cloud.client.ip-address}-${server.port} # 服务id
tags: gatewayDemo
gateway:
globalcors: #配置跨域请求
cors-configurations:
'[/**]':
allowed-origins: "*" #允许来源
allowed-headers: "*" #允许的请求头
allow-credentials: true #允许证书
allowed-methods: #允许的请求方式
- "*"
maxAge: 18000 #预检请求的缓存时间(秒),即在这个时间段里,对于相同的跨域请求不会再预检了
discovery:
locator:
enabled: true #开启从注册中心动态创建路由的功能,利用微服务名进行路由
routes:
- id: api
uri: lb://api
predicates:
- Path=/api/**
filters:
# - StripPrefix=1 #前缀, 在当前路径匹配中表示去掉第一个前缀 /data-service2
default-filters: #全局过滤,可以配置全局过滤,也可以给具体的路由配置过滤
- StripPrefix=1 #前缀, 在当前路径匹配中表示去掉第一个前缀 /consul-service
- name: CircuitBreaker #使用resilience4j断路器
args:
name: fallback #自定义断路器配置
fallbackUri: forward:/circuitBreaker/fallback #异常跳转 resilience4j中使用的fallbackMethod优先级大于这个
resilience4j:
retry: #请求重试
retry-aspect-order: 396 # 表示Retry优先级(级别高于比如ratelimiter bulkhead timelimiter) 值越小 优先级 越高
configs:
myConfig: # 设置组名 可以配置不同的 Retry 策略,给不同的策略分别取一个名字, default 就是一个 Retry 策略的名字 默认名称就是default
maxRetryAttempts: 5 # 表示最大重试次数
waitDuration: 500 # 表示下一次重试等待时间,最小为100 ms
eventConsumerBufferSize: 10 #重试事件缓冲区大小
enableExponentialBackoff: true #是否开启指数退避抖动算法,当一次调用失败后,如果在相同的时间间隔内发起重试,有可能发生连续的调用失败,因此可以开启指数退避抖动算法
exponentialBackoffMultiplier: 1.1 # 间隔乘数(场景: 正好每次间隔为1的时候卡顿 它就有用了 间隔就变了 例如 1 1.1 1.21....)
enableRandomizedWait: false #下次重试的时间间隔是否随机
retryExceptions: #需要重试的异常
- java.lang.Exception
ignoreExceptions: #忽略的异常
- java.lang.IllegalStateException
instances:
retryA:
baseConfig: myConfig
circuitbreaker: #断路器
circuit-breaker-aspect-order: 397
configs: #通用配置
myConfig: # 断路器策略的命名 断路器系统默认配置
ringBufferSizeInClosedState: 5 #断路器关闭状态下,环形缓冲区的大小
ringBufferSizeInHalfOpenState: 3 #断路器处于 HalfOpen 状态下,环形缓冲区的大小
failureRateThreshold: 50 #失败率,错误率达到或高于该值则进入open状态
slowCallDurationThreshold: 60s #慢调用阀值,请求执行的时间大于该值时会标记为慢调用
slowCallRateThreshold: 100 #慢调用熔断阀值,当慢调用率达到或高于该值时,进入open状态
slidingWindowSize: 100 #状态滚动收集器大小,close状态时收集多少请求状态,用于计算失败率。
registerHealthIndicator: true #是否开启健康检测
#状态收集器类型
#COUNT_BASED:根据数量计算,slidingWindowSize为次数
#TIME_BASED:根据时间计算,slidingWindowSize为秒数
slidingWindowType: COUNT_BASED
minimumNumberOfCalls: 10 #计算错误率的最小请求数,不足最小调用次数不会触发任何变化。
automaticTransitionFromOpenToHalfOpenEnabled: false #是否自动进入halfOpen状态,默认false-一定时间后进入halfopen,ture-需要通过接口执行。
permittedNumberOfCallsInHalfOpenState: 10 #进入halfOpen状态时,可以被调用次数,就算这些请求的失败率,低于设置的失败率变为close状态,否则变为open。
waitDurationInOpenState: 60s #open状态变为half状态需要等待的时间,即熔断多久后开始尝试访问被熔断的服务。
eventConsumerBufferSize: 10 #事件缓冲区大小
# recordFailurePredicate: com.kittlen.cloud.config.RecordFailurePredicate #什么样的异常会被认定为请求失败 ??
recordExceptions: #被计为失败的异常集合,默认情况下所有异常都为失败。
- java.lang.Exception
ignoreExceptions: #不会被计为失败的异常集合,优先级高于recordExceptions。
- java.lang.IllegalStateException
instances: #实例化熔断器,用于aop式调用
backendA:
baseConfig: myConfig
minimumNumberOfCalls: 3 #计算错误率的最小请求数,不足最小调用次数不会触发任何变化。
waitDurationInOpenState: 6s #open状态变为half状态需要等待的时间,即熔断多久后开始尝试访问被熔断的服务。
3.配置全局异常
package cloud.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorProperties;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.reactive.error.DefaultErrorWebExceptionHandler;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.context.ApplicationContext;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.RouterFunctions;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import util.JsonUtil;
import java.nio.charset.StandardCharsets;
import java.util.Map;
import static org.springframework.web.reactive.function.server.RequestPredicates.all;
/**
* @author kittlen
* @version 1.0
* @date 2021/7/7 0007
*/
@Slf4j
public class JsonExceptionHandler extends DefaultErrorWebExceptionHandler {
public JsonExceptionHandler(ErrorAttributes errorAttributes,
ResourceProperties resourceProperties,
ErrorProperties errorProperties,
ApplicationContext applicationContext) {
super(errorAttributes, resourceProperties, errorProperties,
applicationContext);
}
/**
* 获取异常属性
*/
@Override
protected Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Throwable error = super.getError(request);
Map<String, Object> errorAttributes = super.getErrorAttributes(request, includeStackTrace);
log.error("网关出现异常了,异常为:{} ,异常类型为:{},请求为:{}", error.getMessage(), error.getCause(), JsonUtil.defaultMapper().toJson(errorAttributes));
return GlobalExceptionHandler.errorMap((Integer) errorAttributes.get("status"));
}
/**
* 指定响应处理方法为JSON处理的方法
*
* @param errorAttributes
*/
@Override
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
// 根据客户端 `accpet` 请求头决定返回什么资源
return RouterFunctions.route(all(), this::renderErrorResponse);
}
/**
* 根据code获取对应的HttpStatus
*
* @param errorAttributes
*/
@Override
protected int getHttpStatus(Map<String, Object> errorAttributes) {
return (int) errorAttributes.get("code");
}
}
package cloud.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.web.ResourceProperties;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.web.reactive.error.ErrorAttributes;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.codec.ServerCodecConfigurer;
import org.springframework.web.reactive.result.view.ViewResolver;
import java.util.Collections;
import java.util.List;
/**
* 自定义全局异常处理
*
* @author kittlen
* @version 1.0
* @date 2021/7/7 0007
*/
@Slf4j
@Configuration
@EnableConfigurationProperties({ServerProperties.class, ResourceProperties.class})
public class ErrorHandlerConfig {
private final ServerProperties serverProperties;
private final ApplicationContext applicationContext;
private final ResourceProperties resourceProperties;
private final List<ViewResolver> viewResolvers;
private final ServerCodecConfigurer serverCodecConfigurer;
public ErrorHandlerConfig(ServerProperties serverProperties,
ResourceProperties resourceProperties,
ObjectProvider<List<ViewResolver>>
viewResolversProvider,
ServerCodecConfigurer
serverCodecConfigurer,
ApplicationContext applicationContext) {
this.serverProperties = serverProperties;
this.applicationContext = applicationContext;
this.resourceProperties = resourceProperties;
this.viewResolvers =
viewResolversProvider.getIfAvailable(Collections::emptyList);
this.serverCodecConfigurer = serverCodecConfigurer;
}
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
JsonExceptionHandler exceptionHandler = new JsonExceptionHandler(
errorAttributes,
this.resourceProperties,
this.serverProperties.getError(),
this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
}
package cloud.config;
import lombok.extern.slf4j.Slf4j;
import java.util.HashMap;
import java.util.Map;
/**
* 全局的的异常
*
*/
@Slf4j
public class GlobalExceptionHandler {
private final static String SERVER_ERROR_TXT = "服务器内部错误";
private final static String ARGUMENTS_ERROR_TXT = "参数错误";
private final static String SERVICE_NOT_FOUND_TXT = "找不到服务";
private final static String BAD_REQUEST_TXT = "错误的请求";
public static Map<String,Object> errorMap(Integer status){
Map<String,Object> map=new HashMap<>();
if(status==null){
map.put("code",500);
map.put("message",SERVER_ERROR_TXT);
}else {
map.put("code",status);
switch (status) {
case 500:
map.put("message",SERVER_ERROR_TXT);
break;
case 404:
map.put("message",SERVICE_NOT_FOUND_TXT);
break;
case 400:
map.put("message",ARGUMENTS_ERROR_TXT);
break;
}
}
return map;
}
}
4.配置Resilience4j作为断路器
package cloud.config;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.timelimiter.TimeLimiterConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.circuitbreaker.resilience4j.ReactiveResilience4JCircuitBreakerFactory;
import org.springframework.cloud.circuitbreaker.resilience4j.Resilience4JConfigBuilder;
import org.springframework.cloud.client.circuitbreaker.Customizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.Duration;
/**
* gateway使用resilience4j作为断路器时进行的配置
*
* @author kittlen
* @version 1.0
* @date 2021/7/8 0008
*/
@Slf4j
@Configuration
public class Resilience4jConfiguration {
/**
* @param circuitBreakerRegistry
* @return
*/
@Bean
public Customizer<ReactiveResilience4JCircuitBreakerFactory> defaultCustomizer(CircuitBreakerRegistry circuitBreakerRegistry) {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.circuitBreakerConfig(circuitBreakerRegistry.getConfiguration("myConfig").get())//通过名字来获取
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(10)).build()).build());//设置超时时长,默认单位为秒(s)
}
}
package cloud.controller;
import entities.Result;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/** 降级熔断
* @author kittlen
* @version 1.0
* @date 2021/7/7 0007
*/
@RestController
@RequestMapping("/circuitBreaker")
public class FallbackController {
public static int count=1;
@RequestMapping("/fallback")
public Result fallback() {
return Result.error("服务暂时不可用");
}
}
5.各种gateway过滤器
1.输出body
package com.kittlen.cloud.filter;
import com.kittlen.cloud.dto.ReleaseMatcherByAntPathMatcher;
import entities.Result;
import com.kittlen.cloud.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import util.JsonUtil;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @author kittlen
* @version 1.0
* @date 2021/7/6 0006
*/
@Slf4j
@Component
public class MainFilter implements GlobalFilter, Ordered {
public static final String TOKEN_HEADER_TAG = "access_token";
public static final String TOKEN_PARAM = "token";
@Autowired
TokenService tokenService;
@Autowired
ReleaseMatcherByAntPathMatcher releaseMatcher;
/**
* 过滤器核心方法
*
* @param exchange
* @param chain
* @return
*/
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
String method = request.getMethodValue();//请求方式
if (HttpMethod.POST.matches(method)) {
return DataBufferUtils.join(exchange.getRequest().getBody())
.flatMap(dataBuffer -> {
byte[] bytes = new byte[dataBuffer.readableByteCount()];
dataBuffer.read(bytes);
String bodyString = new String(bytes, StandardCharsets.UTF_8);
logtrace(exchange, bodyString);
exchange.getAttributes().put("POST_BODY", bodyString);
DataBufferUtils.release(dataBuffer);
Flux<DataBuffer> cachedFlux = Flux.defer(() -> {
DataBuffer buffer = exchange.getResponse().bufferFactory()
.wrap(bytes);
return Mono.just(buffer);
});
ServerHttpRequest mutatedRequest = new ServerHttpRequestDecorator(
exchange.getRequest()) {
@Override
public Flux<DataBuffer> getBody() {
return cachedFlux;
}
};
return chain.filter(exchange.mutate().request(mutatedRequest)
.exchange());
});
} else if (HttpMethod.GET.matches(method)) {
Map m = request.getQueryParams();
logtrace(exchange, m.toString());
}
return chain.filter(exchange);
}
/**
* 优先级 越小优先级越高
*
* @return
*/
@Override
public int getOrder() {
return 0;
}
public Mono<Void> out(ServerHttpResponse response,String msg) {
Result result = Result.error("鉴权失败");
byte[] bits = JsonUtil.toJson(result).getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = response.bufferFactory().wrap(bits);
//指定编码,否则在浏览器中会中文乱码
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
return response.writeWith(Mono.just(buffer));
}
/**
* 日志信息
*
* @param exchange
* @param param 请求参数
*/
private void logtrace(ServerWebExchange exchange, String param) {
ServerHttpRequest serverHttpRequest = exchange.getRequest();
String path = serverHttpRequest.getURI().getPath();
String method = serverHttpRequest.getMethodValue();
String headers = serverHttpRequest.getHeaders().entrySet()
.stream()
.map(entry -> " " + entry.getKey() + ": [" + String.join(";", entry.getValue()) + "]")
.collect(Collectors.joining("\n"));
log.info("\n" + "---------------- ---------------- ---------------->>\n" +
"HttpMethod : {}\n" +
"Uri : {}\n" +
"Param : {}\n" +
"Headers : \n" +
"{}\n" +
"\"<<---------------- ---------------- ----------------"
, method, path, param, headers);
}
public String getToken(ServerHttpRequest request) {
List<String> list = request.getHeaders().get(TOKEN_HEADER_TAG);
if (list == null || list.isEmpty()) {
list = request.getHeaders().get(TOKEN_PARAM);
}
if (list == null || list.isEmpty()) {
return "";
}
return list.get(0);
}
}
2.Headers中添加属性
package com.kittlen.cloud.filter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author kittlen
* @version 1.0
* @date 2021/9/17 0017
*/
public class HeadersFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
ServerHttpRequest host = request.mutate().headers(headers -> {
headers.add("auth", "kittlen");//headers中添加属性
headers.remove("name");//headers中删除属性
}).build();
ServerWebExchange build = exchange.mutate().request(host).build();
return chain.filter(build);
}
@Override
public int getOrder() {
return 4;
}
}
3.白名单
package com.kittlen.cloud.filter;
import com.kittlen.cloud.dto.ReleaseMatcherByAntPathMatcher;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/**
* @author kittlen
* @version 1.0
* @date 2021/9/17 0017
*/
@Slf4j
public class WhiteListFilter implements GlobalFilter, Ordered {
@Autowired
ReleaseMatcherByAntPathMatcher releaseMatcher;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//白名单
if (releaseMatcher.release(request.getURI().getPath())) {
log.info("地址在白名单");
return chain.filter(exchange);
}
log.info("不在白名单,进行其他判断");
return chain.filter(exchange);
}
@Override
public int getOrder() {
return -1;
}
}
package com.kittlen.cloud.config;
import com.kittlen.cloud.dto.ReleaseMatcherByAntPathMatcher;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.List;
/**
* @author kittlen
* @version 1.0
* @date 2021/7/14 0014
*/
@Configuration
@ConfigurationProperties(prefix = "gateway.matcher")
public class MatcherConfig {
private List<String> whitelist;
public void setWhitelist(List<String> whitelist) {
this.whitelist = whitelist;
}
@Bean
public ReleaseMatcherByAntPathMatcher releaseMatcherByAntPathMatcher() {
return new ReleaseMatcherByAntPathMatcher(whitelist);
}
}
package com.kittlen.cloud.dto;
import org.springframework.util.AntPathMatcher;
import util.StringUtil;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;
/** 利用AntPathMatcher利进行判断
* @author kittlen
* @version 1.0
* @date 2021/7/14 0014
*/
public class ReleaseMatcherByAntPathMatcher {
private static AntPathMatcher antPathMatcher = new AntPathMatcher();
/**
* 必须是在yml文件gateway的routes中配置了的地址进行访问时才会进行地址拦截过滤和白名单
* **代表多级地址(任意地址的任意子地址)
* *代表单级地址或任意字符(任意地址)
*/
//如:/consul-feign/**/describe/* 请求的地址为consul-feign开头倒数第二级地址为describe,结尾为任意
private List<String> urls;
public List<String> getUrls() {
return urls;
}
public void setUrls(List<String> urls) {
this.urls = urls;
}
public ReleaseMatcherByAntPathMatcher() {
}
public ReleaseMatcherByAntPathMatcher(List<String> urls) {
if (urls == null) {
this.urls = new ArrayList<>();
} else {
this.urls = urls;
}
}
public ReleaseMatcherByAntPathMatcher(List<String> urls, List<Pattern> Patterns) {
this.urls = urls;
}
public boolean release(String path) {
if (urls == null || urls.isEmpty()|| StringUtil.isEmpty(path)) {
return false;
}
if (!"/".equals(path.substring(0, 1))) {
path = "/" + path;
}
for (int i = 0; i < urls.size(); i++) {
if(antPathMatcher.match(urls.get(i), path)){
return true;
}
}
return false;
}
}