1.添加依赖
<!-- spring cloud alibaba nacos discovery 依赖 -->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId><version>2.2.3.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
2.nacos配置内容
1.新建配置
2.Data Id:butool-cloud-gateway-router
3.Group: butool-cloud
4.配置格式: JSON
5.配置内容
//配置不需要加注释
//获取路由对象,被网关反序列化成List<RouteDefinition>
[{"id":"butool-cloud-test", //路由配置主键唯一,自定义即可"order":0, //优先级越小代表的优先级越高"predicates":[ //路由匹配{"args":{"pattern":"/api/butool-cloud-test/**" //先映射到网关api/butool-cloud-test 工程路径前缀 server.servlet.context-path},"name":"Path" //路径匹配}],"uri":"lb://butool-test", 转发到哪个微服务serviceid ’lb://’开头 spring.application.name"filters":[{"name":"HeaderToken" //header携带token验证过滤器,寻找HeaderTokenGatewayFilterFactor,GatewayFilterFactor前面的名称},{"name":"StripPrefix", //跳过前缀过滤器"args":{"parts":"1" //跳过1个前缀,往后的路径实现转发} }]}
]
3.bootstrap.yal
server:port: 9001servlet:context-path: /api
spring:application:name: api-butool-gatewaycloud:nacos:# 服务注册发现discovery:enabled: true # 如果不想使用 Nacos 进行服务注册和发现, 设置为 false 即可server-addr: 127.0.0.1:8848,127.0.0.1:8849,127.0.0.1:8850namespace: fdf5864c-f4da-47c3-9ef6-898a60cfff3b# 接入到springBootAdminmetadata:management:context-path: ${server.servlet.context-path}/actuator
# 这个地方独立配置, 是网关的数据, 代码 GatewayConfig.java 中读取被监听
nacos:gateway:route:config:data-id: butool-cloud-gateway-routergroup: butool-cloud
# 暴露端点
management:endpoints:web:exposure:include: '*'endpoint:health:show-details: always
4.配置类
package cn.butool.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
/*** 配置类:读取Nacos相关的配置项,用于配置监听器*/
@SuppressWarnings("all")
@Configuration
public class GatewayConfig {/*** 读取配置的超时时间*/public static final long DEFAULT_TIMEOUT = 30000;/*** Nacos 服务器地址*/public static String NACOS_SERVER_ADDR;
/*** Nacos 命名空间*/public static String NACOS_NAMESPACE;/*** nacos 配置列表中的dataid*/public static String NACOS_ROUTE_DATE_ID;/*** nacos分组id*/public static String NACOS_GROUP_ID;
@Value("${spring.cloud.nacos.discovery.server-addr}")public void setNacosServerAddr(String nacosServerAddr) {NACOS_SERVER_ADDR = nacosServerAddr;}
@Value("${spring.cloud.nacos.discovery.namespace}")public void setNacosNamespace(String nacosNamespace) {NACOS_NAMESPACE = nacosNamespace;}
@Value("${nacos.gateway.route.config.data-id}")public void setNacosRouteDateId(String nacosRouteDateId) {NACOS_ROUTE_DATE_ID = nacosRouteDateId;}
@Value("${nacos.gateway.route.config.group}")public void setNacosGroupId(String nacosGroupId) {NACOS_GROUP_ID = nacosGroupId;}
}
5.事件推送 Aware:
package cn.butool.config;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionLocator;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import java.util.List;
/*** 事件推送 Aware: 动态更新路由网关 Service* */
@Slf4j
@Service
@SuppressWarnings("all")
public class DynamicRouteServiceImpl implements ApplicationEventPublisherAware {
/** 写路由定义 */private final RouteDefinitionWriter routeDefinitionWriter;/** 获取路由定义 */private final RouteDefinitionLocator routeDefinitionLocator;
/** 事件发布 */private ApplicationEventPublisher publisher;
public DynamicRouteServiceImpl(RouteDefinitionWriter routeDefinitionWriter,RouteDefinitionLocator routeDefinitionLocator) {this.routeDefinitionWriter = routeDefinitionWriter;this.routeDefinitionLocator = routeDefinitionLocator;}
@Overridepublic void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {// 完成事件推送句柄的初始化this.publisher = applicationEventPublisher;}
/*** 从nacos读取路由配hi,写道gateway中* <h2>增加路由定义</h2>* */public String addRouteDefinition(RouteDefinition definition) {
log.info("gateway add route: [{}]", definition);
// 保存路由配置并发布routeDefinitionWriter.save(Mono.just(definition)).subscribe();// 发布事件通知给 Gateway, 同步新增的路由定义this.publisher.publishEvent(new RefreshRoutesEvent(this));
return "success";}
/*** <h2>更新路由</h2>* */public String updateList(List<RouteDefinition> definitions) {
log.info("更新网关路由: [{}]", definitions);
// 先拿到当前 Gateway 中存储的路由定义List<RouteDefinition> routeDefinitionsExits =routeDefinitionLocator.getRouteDefinitions().buffer().blockFirst();if (!CollectionUtils.isEmpty(routeDefinitionsExits)) {// 清除掉之前所有的 "旧的" 路由定义routeDefinitionsExits.forEach(rd -> {deleteById(rd.getId());log.info("清除掉之前所有的 旧的 路由定义: [{}]", rd);});}
// 把更新的路由定义同步到 gateway 中definitions.forEach(definition -> updateByRouteDefinition(definition));return "success";}
/*** <h2>根据路由 id 删除路由配置</h2>* */private String deleteById(String id) {
try {log.info("要删除的路由id: [{}]", id);this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();// 发布事件通知给 gateway 更新路由定义this.publisher.publishEvent(new RefreshRoutesEvent(this));return "删除成功";} catch (Exception ex) {log.error("删除网关路由失败: [{}]", ex.getMessage(), ex);return "删除失败";}}
/*** <h2>更新路由</h2>* 更新的实现策略比较简单: 删除 + 新增 = 更新* */private String updateByRouteDefinition(RouteDefinition definition) {
try {log.info("更新网关路由: [{}]", definition);this.routeDefinitionWriter.delete(Mono.just(definition.getId()));} catch (Exception ex) {return "更新失败,没有查到更新的网关路由id: " + definition.getId();}
try {this.routeDefinitionWriter.save(Mono.just(definition)).subscribe();this.publisher.publishEvent(new RefreshRoutesEvent(this));return "成功";} catch (Exception ex) {return "更新路由失败!";}}
}
6.通过Nacos下发的动态配置,监听nacos中路由配置
package cn.butool.config;
import cn.hutool.core.collection.CollectionUtil;
import com.alibaba.fastjson.JSON;
import com.alibaba.nacos.api.NacosFactory;
import com.alibaba.nacos.api.config.ConfigService;
import com.alibaba.nacos.api.config.listener.Listener;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.context.annotation.DependsOn;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.Collections;
import java.util.List;
import java.util.Properties;
import java.util.concurrent.Executor;
/*** 通过Nacos下发的动态配置,监听nacos中路由配置*/
@Slf4j
@Component
//再另外一个Bean初始化之后再去初始化当前类
@DependsOn({"gatewayConfig"})
@SuppressWarnings("all")
public class DynamicRouteServiceImplNacos {
private ConfigService configService;
private final DynamicRouteServiceImpl dynamicRouteService;
public DynamicRouteServiceImplNacos(DynamicRouteServiceImpl dynamicRouteService) {this.dynamicRouteService = dynamicRouteService;}
/*** 初始化ConfigService** @return*/private ConfigService initConfigService() {try {Properties properties = new Properties();properties.setProperty("serverAddr", GatewayConfig.NACOS_SERVER_ADDR);properties.setProperty("namespace", GatewayConfig.NACOS_NAMESPACE);return configService = NacosFactory.createConfigService(properties);} catch (Exception ex) {log.error("初始化配置服务:[{}]", ex.getMessage());return null;}}
/*** bean在容器中构造完成之后会立即执行当前的init方法* 加载路由信息,注册监听器*/@PostConstructpublic void init(){log.info("网关路由初始化...");
try {//初始化nacos配置客户端configService = initConfigService();if(configService==null){log.error("初始化配置服务异常,配置服务是null!");return;}//通过 nacos config 并指定路由配置路径去获取路由配置String configInfo = configService.getConfig(GatewayConfig.NACOS_ROUTE_DATE_ID,GatewayConfig.NACOS_GROUP_ID,GatewayConfig.DEFAULT_TIMEOUT);log.info("当前网关配置信息:[{}]:",configInfo);//反序列化List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);if(CollectionUtil.isNotEmpty(routeDefinitions)){//新增for (RouteDefinition routeDefinition : routeDefinitions) {log.info("初始化 路由定义对象 信息:[{}]:",routeDefinition);dynamicRouteService.addRouteDefinition(routeDefinition);}
}} catch (Exception e) {log.error("网关路由初始化失败:[{}]",e.getMessage());}//设置监听器dynamicRouteServiceImplNacosByListener(GatewayConfig.NACOS_ROUTE_DATE_ID,GatewayConfig.NACOS_GROUP_ID);}
/*** 监听 Nacos下发的动态路由配置** @param dataId* @param group*/private void dynamicRouteServiceImplNacosByListener(String dataId, String group) {try {//给Nacos config 客户端增加一个监听器configService.addListener(dataId, group, new Listener() {/*** 自己提供线程池执行操作* @return*/@Overridepublic Executor getExecutor() {return null;}
/*** 监听器收到接收到配置变更信息* @param configInfo nacos 中最新配置信息*/@Overridepublic void receiveConfigInfo(String configInfo) {log.info("接收的配置信息:[{}]", configInfo);List<RouteDefinition> routeDefinitions = JSON.parseArray(configInfo, RouteDefinition.class);dynamicRouteService.updateList(routeDefinitions);log.info("更新后的路由配置信息:[{}]", routeDefinitions.toString());}});} catch (Exception e) {log.error("监听 Nacos下发的动态路由配置异常:[{}]", e.getMessage());}}
}
7.局部过滤器
7.1过滤器实现
package cn.butool.filter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
/*** header携带token验证过滤器*/
public class HeaderTokenGateWayFilter implements GatewayFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {//从HttpHeader中寻找key为token,value为butool的键值对String token = exchange.getRequest().getHeaders().getFirst("token");if("butool".equals(token)){return chain.filter(exchange);}// 标记此次请求没有权限,并且结束这次请求exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);//请求结束return exchange.getResponse().setComplete();}
/*** 优先级* @return*/@Overridepublic int getOrder() {return HIGHEST_PRECEDENCE+2;}
}
7.2工厂实现类
package cn.butool.filter.factory;
import cn.butool.filter.HeaderTokenGateWayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.stereotype.Component;
@Component
public class HeaderTokenGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Overridepublic GatewayFilter apply(Object config) {return new HeaderTokenGateWayFilter();}
}
7.3配置文件中进行配置
//注意命名规则
"filters":[{"name":"HeaderToken" //header携带token验证过滤器,寻找HeaderTokenGatewayFilterFactor,GatewayFilterFactor前面的名称},{"name":"StripPrefix", //跳过前缀过滤器"args":{"parts":"1" //跳过1个前缀,往后的路径实现转发} }]
7.4局部过滤器步骤
1.新建过滤器实现
2.新建工厂类实现
3.配置文件中进行配置
8.缓存请求body的全局过滤器
package cn.butool.constan;
/*** 网关常量定于*/
@SuppressWarnings("all")
public class GatewayConstant {/**登录的url*/public static final String LOGIN_URL = "/butool/login";
/** 注册的url*/public static final String REGISTER_URL = "/butool/register";
/** 去授权中心拿到登录 token 的 uri 格式化接口 */public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT ="http://%s:%s/butool-cloud-Authority-center/authority/token";
/** 去授权中心注册并拿到 token 的 uri 格式化接口 */public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT ="http://%s:%s/butool-cloud-Authority-center/authority/register";
}
package cn.butool.filter;
import cn.butool.constan.GatewayConstant;
import lombok.extern.slf4j.Slf4j;
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.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpRequestDecorator;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/*** <h1>全局缓存请求Body过滤器</h1>* 将请求数据缓存方便后面的过滤器拿取* spring WebFlux*/
@Slf4j
@Component
public class GlobalCacheRequestBodyFilter implements GlobalFilter, Ordered {
@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {String path = exchange.getRequest().getURI().getPath();boolean isLoginOrRegister =path.contains(GatewayConstant.LOGIN_URL) || path.contains(GatewayConstant.REGISTER_URL);// 如果没有请求头或者不是登录注册接口,不用去缓存了,往下走下面的过滤器if (null == exchange.getRequest().getHeaders().getContentType() || !isLoginOrRegister) {return chain.filter(exchange);}return DataBufferUtils.join(exchange.getRequest().getBody()) //获取请求数据.flatMap(dataBuffer -> {// 确保数据缓冲区不被释放, 必须要 DataBufferUtils.retainDataBufferUtils.retain(dataBuffer);// defer、just 都是去创建数据源, 得到当前数据的副本Flux<DataBuffer> cachedFlux = Flux.defer(() ->Flux.just(dataBuffer.slice(0, dataBuffer.readableByteCount())));// 重新包装 ServerHttpRequest, 重写 getBody 方法, 能够返回请求数据ServerHttpRequest mutatedRequest =new ServerHttpRequestDecorator(exchange.getRequest()) {@Overridepublic Flux<DataBuffer> getBody() {return cachedFlux;}};// 将包装之后的 ServerHttpRequest 向下继续传递return chain.filter(exchange.mutate().request(mutatedRequest).build());});}
/*** 下面过滤器要拿到当前过滤器缓存内容优先级要比当前返回数值要大** @return*/@Overridepublic int getOrder() {return HIGHEST_PRECEDENCE + 1;}
}
9.全局登录、鉴权过滤器
9.1注入RestTemplate对象
package cn.butool.conf;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
/*** 网关需要注入到容器中的bean*/
@Configuration
public class GatewayBeanConf {@Beanpublic RestTemplate restTemplate(){return new RestTemplate();}
}
9.2鉴权授权登陆注册接口常量
package cn.butool.constan;
/*** 网关常量定于*/
@SuppressWarnings("all")
public class GatewayConstant {/**登录的url*/public static final String LOGIN_URL = "/butool/login";public static final String LOGINL = "/authority/login";
/** 注册的url*/public static final String REGISTER_URL = "/butool/register";public static final String REGISTER = "/authority/register";
/** 去授权中心拿到登录 token 的 uri 格式化接口 */public static final String AUTHORITY_CENTER_TOKEN_URL_FORMAT ="http://%s:%s/butool-cloud-Authority-center/authority/token";
/** 去授权中心注册并拿到 token 的 uri 格式化接口 */public static final String AUTHORITY_CENTER_REGISTER_URL_FORMAT ="http://%s:%s/butool-cloud-Authority-center/authority/register";
}
9.3登录鉴权过滤器
package cn.butool.filter;
import cn.butool.constan.GatewayConstant;
import cn.butool.constant.CommonConstant;
import cn.butool.util.TokenParseUtil;
import cn.butool.vo.JwtToken;
import cn.butool.vo.LoginUserInfo;
import cn.butool.vo.UserRegisterInfo;
import com.alibaba.fastjson.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
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.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.nio.CharBuffer;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.atomic.AtomicReference;
/*** <h1>全局登录、鉴权过滤器</h1>* spring WebFlux*/
@Slf4j
@Component
public class GlobalLoginOrRegisterFilter implements GlobalFilter, Ordered {/*** 注册中心客户端, 可以从注册中心中获取服务实例信息*/private final LoadBalancerClient loadBalancerClient;/*** 发送http请求*/private final RestTemplate restTemplate;
public GlobalLoginOrRegisterFilter(LoadBalancerClient loadBalancerClient,RestTemplate restTemplate) {this.loadBalancerClient = loadBalancerClient;this.restTemplate = restTemplate;}
/*** 登录注册鉴权* 1.如果时登录注册,则去授权中心拿到token 并返回给客户端* 2.如果是访问其他服务,则鉴权,没权限返回 401* @param exchange* @param chain* @return*/@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {ServerHttpRequest request = exchange.getRequest();ServerHttpResponse response = exchange.getResponse();//1.如果是登录if(request.getURI().getPath().contains(GatewayConstant.LOGIN_URL)){//去授权中心拿tokenString token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_TOKEN_URL_FORMAT);// header 中不能设置 nullresponse.getHeaders().add(CommonConstant.JWT_USER_INFO_KEY,null==token?"null":token);response.setStatusCode(HttpStatus.OK);return response.setComplete();}else if(request.getURI().getPath().contains(GatewayConstant.REGISTER_URL)){//去授权中心拿tokenString token = getTokenFromAuthorityCenter(request, GatewayConstant.AUTHORITY_CENTER_REGISTER_URL_FORMAT);// header 中不能设置 nullresponse.getHeaders().add(CommonConstant.JWT_USER_INFO_KEY,null==token?"null":token);response.setStatusCode(HttpStatus.OK);return response.setComplete();}// 3.访问其他服务,则鉴权,校验是否能从token中解析出用户信息HttpHeaders headers = request.getHeaders();String token = headers.getFirst(CommonConstant.JWT_USER_INFO_KEY);LoginUserInfo loginUserInfo = null;try {loginUserInfo = TokenParseUtil.parseUserInfoFromToken(token);} catch (Exception ex) {log.error("parse user info from token error: [{}]", ex.getMessage(), ex);}if(null == loginUserInfo){response.setStatusCode(HttpStatus.UNAUTHORIZED);return response.setComplete();}//解析通过则放行return chain.filter(exchange);}
/*** <h2>从授权中心获取 Token</h2>*/private String getTokenFromAuthorityCenter(ServerHttpRequest request, String uriFormat) {
// service id 就是服务名字, 负载均衡ServiceInstance serviceInstance = loadBalancerClient.choose(CommonConstant.AUTHORITY_CENTER_SERVICE_ID);log.info("Nacos Client Info: [{}], [{}], [{}]",serviceInstance.getServiceId(), serviceInstance.getInstanceId(),JSON.toJSONString(serviceInstance.getMetadata()));
String requestUrl = String.format(uriFormat, serviceInstance.getHost(), serviceInstance.getPort());JwtToken token = null;if(uriFormat.contains(GatewayConstant.LOGINL)){LoginUserInfo requestBody = JSON.parseObject(parseBodyFromRequest(request), LoginUserInfo.class);log.info("login request url and body: [{}], [{}]", requestUrl,JSON.toJSONString(requestBody));
HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);token = restTemplate.postForObject(requestUrl,new HttpEntity<>(JSON.toJSONString(requestBody), headers),JwtToken.class);}else if(uriFormat.contains(GatewayConstant.REGISTER)){UserRegisterInfo requestBody = JSON.parseObject(parseBodyFromRequest(request), UserRegisterInfo.class);log.info("注册请求url地址和请求body: [{}], [{}]", requestUrl,JSON.toJSONString(requestBody));HttpHeaders headers = new HttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);token = restTemplate.postForObject(requestUrl,new HttpEntity<>(JSON.toJSONString(requestBody), headers),JwtToken.class);}if (null != token) {return token.getToken();}return null;}
/*** 从post请求中获取请数据** @param request* @return*/private String parseBodyFromRequest(ServerHttpRequest request) {// 获取请求体Flux<DataBuffer> body = request.getBody();AtomicReference<String> bodyRef = new AtomicReference<>();
// 订阅缓冲区去消费请求体中的数据body.subscribe(buffer -> {CharBuffer charBuffer = StandardCharsets.UTF_8.decode(buffer.asByteBuffer());// 一定要使用 DataBufferUtils.release 释放掉, 否则, 会出现内存泄露DataBufferUtils.release(buffer);bodyRef.set(charBuffer.toString());});
// 获取 request bodyreturn bodyRef.get();}
/*** 下面过滤器要拿到当前过滤器缓存内容优先级要比当前返回数值要大** @return*/@Overridepublic int getOrder() {return HIGHEST_PRECEDENCE + 2;}
}
10.全局接口耗时过滤器
package cn.butool.filter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.time.StopWatch;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.concurrent.TimeUnit;
/*** <h1>全局接口耗时日志过滤器</h1>* */
@Slf4j
@Component
public class GlobalElapsedLogFilter implements GlobalFilter, Ordered {
@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 前置逻辑StopWatch sw = StopWatch.createStarted();String uri = exchange.getRequest().getURI().getPath();
return chain.filter(exchange).then(// 后置逻辑Mono.fromRunnable(() ->log.info("[{}] elapsed: [{}ms]",uri, sw.getTime(TimeUnit.MILLISECONDS))));}@Overridepublic int getOrder() {return HIGHEST_PRECEDENCE;}
}