前言
我們在上一篇文章基于壓測進行Feign調(diào)優(yōu)完成的服務間調(diào)用的性能調(diào)優(yōu),此時我們也關注到一個問題,如果我們統(tǒng)一從網(wǎng)關調(diào)用服務,但是網(wǎng)關因為某些原因報錯或者沒有找到服務怎么辦呢?
如下所示,筆者通過網(wǎng)關調(diào)用account
服務,但是account
服務還沒起來。此時請求還沒有到達account
就報錯了,這就意味著我們服務中編寫的@RestControllerAdvice
對網(wǎng)關沒有任何作用。
curl 127.0.0.1:8090/account/getByCode/zsy
響應結果如下,可以看到響應結果如下所示,要知道現(xiàn)如今的開發(fā)模式為前后端分離模式,前后端交互完全是基于協(xié)商好的格式,如果網(wǎng)關響應格式與我們規(guī)定的格式完全不一致,前端就需要特殊處理,這使得代碼不僅會變得丑陋,對于后續(xù)的功能擴展的交互復雜度也會增加,而gateway默認響應錯誤如下:
{
"timestamp":"2023-02-09T15:22:20.278+0000",
"path":"/account/getByCode/zsy",
"status":500,
"error":"Internal Server Error",
"message":"Connection refused: no further information: /192.168.43.73:9000"
}
網(wǎng)關異常默認處理
所以我們必須了解一下是什么原因導致網(wǎng)關報錯會響應這個值。
我們在gateway
源碼中找到ErrorWebFluxAutoConfiguration
這個自動裝配類,可以看到下面這段代碼,我們從中得知網(wǎng)關報錯時默認使用DefaultErrorWebExceptionHandler
來返回結果,所以我們不妨看看這個類做了那些事情。
@Bean
@ConditionalOnMissingBean(value = ErrorWebExceptionHandler.class, search = SearchStrategy.CURRENT)
@Order(-1)
public ErrorWebExceptionHandler errorWebExceptionHandler(ErrorAttributes errorAttributes) {
//網(wǎng)關默認異常處理的handler
DefaultErrorWebExceptionHandler exceptionHandler = new DefaultErrorWebExceptionHandler(errorAttributes,
this.resourceProperties, this.serverProperties.getError(), this.applicationContext);
exceptionHandler.setViewResolvers(this.viewResolvers);
exceptionHandler.setMessageWriters(this.serverCodecConfigurer.getWriters());
exceptionHandler.setMessageReaders(this.serverCodecConfigurer.getReaders());
return exceptionHandler;
}
我們不妨基于debug
了解一下這個類,當我們服務沒有注冊到nacos
,并通過網(wǎng)關調(diào)用報錯時,代碼就會走到下方,route
方法第一個參數(shù)是RequestPredicate
謂詞,而后者則是謂詞的處理,進行renderErrorView
,andRoute
同理將報錯的請求通過renderErrorResponse
返回錯誤結果
@Override
//route 方法第一個參數(shù)是RequestPredicate謂詞,而后者則是謂詞的處理,進行renderErrorView,然后通過然后通過andRoute將報錯的請求通過renderErrorResponse返回錯誤結果
protected RouterFunction<ServerResponse> getRoutingFunction(ErrorAttributes errorAttributes) {
return route(acceptsTextHtml(), this::renderErrorView).andRoute(all(), this::renderErrorResponse);
}
我們不妨看看renderErrorResponse
,可以看到一行getErrorAttributes
,一旦步入我們就可以看到上文請求錯誤的結果格式
protected Mono<ServerResponse> renderErrorResponse(ServerRequest request) {
boolean includeStackTrace = isIncludeStackTrace(request, MediaType.ALL);
Map<String, Object> error = getErrorAttributes(request, includeStackTrace);
return ServerResponse.status(getHttpStatus(error)).contentType(MediaType.APPLICATION_JSON_UTF8)
.body(BodyInserters.fromObject(error));
}
getErrorAttributes
源碼,可以看到組裝的key
值就是我們調(diào)試時響應的參數(shù):
@Override
public Map<String, Object> getErrorAttributes(ServerRequest request, boolean includeStackTrace) {
Map<String, Object> errorAttributes = new LinkedHashMap<>();
errorAttributes.put("timestamp", new Date());
errorAttributes.put("path", request.path());
Throwable error = getError(request);
HttpStatus errorStatus = determineHttpStatus(error);
errorAttributes.put("status", errorStatus.value());
errorAttributes.put("error", errorStatus.getReasonPhrase());
errorAttributes.put("message", determineMessage(error));
handleException(errorAttributes, determineException(error), includeStackTrace);
return errorAttributes;
}
自定義異常處理
了解的默認錯誤處理,我們就可以改造,返回一個和普通服務一樣的格式給前端告知網(wǎng)關報錯。從上文我們可知網(wǎng)關默認錯誤處理時DefaultErrorWebExceptionHandler
,通過類圖我們可以發(fā)現(xiàn)它繼承了一個ErrorWebExceptionHandler
,所以我們也可以繼承這個類重寫一個Handler
。
以筆者的代碼如下,可以看到筆者使用Order
注解強制獲得最高異常處理優(yōu)先級,然后使用bufferFactory.wrap
方法傳遞自定義錯誤格式返回給前端。
@Slf4j
@Order(-1)
@Configuration
@RequiredArgsConstructor(onConstructor = @__(@Autowired))
public class GlobalErrorWebExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
// 設置返回值類型為json
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
//設置返回編碼
if (ex instanceof ResponseStatusException) {
response.setStatusCode(((ResponseStatusException) ex).getStatus());
}
return response.writeWith(Mono.fromSupplier(() -> {
DataBufferFactory bufferFactory = response.bufferFactory();
try {
//writeValueAsBytes 組裝錯誤響應結果
return bufferFactory.wrap(objectMapper.writeValueAsBytes(ResultData.fail(500, "網(wǎng)關捕獲到異常:" + ex.getMessage())));
} catch (JsonProcessingException e) {
log.error("Error writing response", ex);
return bufferFactory.wrap(new byte[0]);
}
}));
}
}
最終返回的結果如下所示,可以看到結果和一般的服務調(diào)用報錯格式一模一樣,這樣一來前端就無需為了網(wǎng)關報錯加一個特殊處理的邏輯了
curl 127.0.0.1:8090/account/getByCode/zsy
輸出結果
{
"status":500,
"message":"網(wǎng)關捕獲到異常:503 SERVICE_UNAVAILABLE \"Unable to find instance for account-service\"",
"data":null,
"success":false,
"timestamp":1675959617386
}
請求響應日志監(jiān)控
對于微服務架構來說,監(jiān)控是很重要的,在高并發(fā)場景情況下,很多問題我們都可以在網(wǎng)關請求響應中定位到,所以我們希望能有這么一種方式將用戶日常請求響應的日志信息記錄下來,便于日常運維和性能監(jiān)控。
查閱了網(wǎng)上的資料發(fā)現(xiàn),基于MongoDB
進行網(wǎng)關請求響應數(shù)據(jù)采集是一種不錯的方案,所以筆者本篇文章整理一下筆者如何基于網(wǎng)關過濾器結合MongoDB
完成請求日志采集。
本篇文章可能會涉及MongoDB
相關的知識,不了解的讀者可以參考筆者的這篇文章:
MongoDB快速入門
gateway整合MongoDB采集日志步驟
- 添加
MongoDB
依賴并完成MongoDB
配置:
首先在gateway
中添加MongoDB
依賴,需要注意的是,筆者后續(xù)的過濾器某些代碼段會用到hutool
的工具類,所以這里也添加了hutool
的依賴。
<!--mongodb依賴-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
然后我們在gateway
的配置中添加MongoDB
的連接參數(shù)配置:
# mongodb的ip地址
spring.data.mongodb.host=ip
# mongodb端口號
spring.data.mongodb.port=27017
# mongodb數(shù)據(jù)庫名稱
spring.data.mongodb.database=accesslog
# 用戶名
spring.data.mongodb.username=xxxx
# 密碼
spring.data.mongodb.password=xxx
- 編寫
MongoDB
保存邏輯:
我們希望保存網(wǎng)關響應的內(nèi)容到mongodb中,所以我們要把我們需要的內(nèi)容封裝成一個對象,如下GatewayLog
@Data
public class GatewayLog {
/**
* 請求相對路徑
*/
private String requestPath;
/**
*請求方法 :get post
*/
private String requestMethod;
/**
*請求協(xié)議:http rpc
*/
private String schema;
/**
*請求體內(nèi)容
*/
private String requestBody;
/**
*響應內(nèi)容
*/
private String responseBody;
/**
*ip地址
*/
private String ip;
/**
* 請求時間
*/
private String requestTime;
/**
*響應時間
*/
private String responseTime;
/**
*執(zhí)行時間 單位:毫秒
*/
private Long executeTime;
}
完成對象定義后,我們就可以編寫service層接口和實現(xiàn)類的邏輯了:
public interface AccessLogService {
/**
* 保存AccessLog
* @param gatewayLog 請求響應日志
* @return 響應日志
*/
GatewayLog saveAccessLog(GatewayLog gatewayLog);
}
實現(xiàn)類代碼如下,可以看到筆者完全基于mongoTemplate
的save
方法將日志數(shù)據(jù)存到gatewayLog
表中。
@Service
public class AccessLogServiceImpl implements AccessLogService {
@Autowired
private MongoTemplate mongoTemplate;
//collection名稱
private final String collectionName="gatewayLog" ;
@Override
public GatewayLog saveAccessLog(GatewayLog gatewayLog) {
GatewayLog result = mongoTemplate.save(gatewayLog, collectionName);
return result;
}
}
- 基于
gateway
過濾器完成請求相應日志采集,代碼比較長,首先是CachedBodyOutputMessage
,由于筆者用的是Spring boot 2.x
版本,沒有CachedBodyOutputMessage
這個類,所以筆者從網(wǎng)上找了一份。讀者可以根據(jù)注釋進行復制修改即可。
public class CachedBodyOutputMessage implements ReactiveHttpOutputMessage {
private final DataBufferFactory bufferFactory;
private final HttpHeaders httpHeaders;
private Flux<DataBuffer> body = Flux.error(new IllegalStateException("The body is not set. Did handling complete with success? Is a custom \"writeHandler\" configured?"));
private Function<Flux<DataBuffer>, Mono<Void>> writeHandler = this.initDefaultWriteHandler();
public CachedBodyOutputMessage(ServerWebExchange exchange, HttpHeaders httpHeaders) {
this.bufferFactory = exchange.getResponse().bufferFactory();
this.httpHeaders = httpHeaders;
}
public void beforeCommit(Supplier<? extends Mono<Void>> action) {
}
public boolean isCommitted() {
return false;
}
public HttpHeaders getHeaders() {
return this.httpHeaders;
}
private Function<Flux<DataBuffer>, Mono<Void>> initDefaultWriteHandler() {
return (body) -> {
this.body = body.cache();
return this.body.then();
};
}
public DataBufferFactory bufferFactory() {
return this.bufferFactory;
}
public Flux<DataBuffer> getBody() {
return this.body;
}
public void setWriteHandler(Function<Flux<DataBuffer>, Mono<Void>> writeHandler) {
Assert.notNull(writeHandler, "'writeHandler' is required");
this.writeHandler = writeHandler;
}
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
return Mono.defer(() -> {
return (Mono)this.writeHandler.apply(Flux.from(body));
});
}
public Mono<Void> writeAndFlushWith(Publisher<? extends Publisher<? extends DataBuffer>> body) {
return this.writeWith(Flux.from(body).flatMap((p) -> {
return p;
}));
}
public Mono<Void> setComplete() {
return this.writeWith(Flux.empty());
}
}
過濾器代碼如下,筆者將核心內(nèi)容都已注釋了,讀者可以基于此代碼進行修改
@Slf4j
@Component
public class AccessLogGlobalFilter implements GlobalFilter, Ordered {
private final List<HttpMessageReader<?>> messageReaders = HandlerStrategies.withDefaults().messageReaders();
//todo 存在線程安全問題,后續(xù)需要優(yōu)化掉
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
@Autowired
private AccessLogService accessLogService;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
GatewayLog gatewayLog = new GatewayLog();
ServerHttpRequest request = exchange.getRequest();
//獲取請求的ip,url,method,body
String requestPath = request.getPath().pathWithinApplication().value();
String clientIp = request.getRemoteAddress().getHostString();
String scheme = request.getURI().getScheme();
String method = request.getMethodValue();
//數(shù)據(jù)記錄到gatwayLog中
gatewayLog.setSchema(scheme);
gatewayLog.setRequestMethod(method);
gatewayLog.setRequestPath(requestPath);
gatewayLog.setRequestTime(simpleDateFormat.format(new Date().getTime()));
gatewayLog.setIp(clientIp);
MediaType contentType = request.getHeaders().getContentType();
if (MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType) || MediaType.APPLICATION_JSON.isCompatibleWith(contentType)) {
return writeBodyLog(exchange, chain, gatewayLog);
} else {
//寫入日志信息到mongoDb
return writeBasicLog(exchange, chain, gatewayLog);
}
}
private Mono<Void> writeBasicLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog accessLog) {
StringBuilder builder = new StringBuilder();
MultiValueMap<String, String> queryParams = exchange.getRequest().getQueryParams();
for (Map.Entry<String, List<String>> entry : queryParams.entrySet()) {
builder.append(entry.getKey()).append("=").append(StringUtils.join(entry.getValue(), ","));
}
//記錄響應內(nèi)容
accessLog.setRequestBody(builder.toString());
// 獲取響應體
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, accessLog);
return chain.filter(exchange.mutate().response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
//打印日志
writeAccessLog(accessLog);
}));
}
/**
* 解決request body 只能讀取一次問題
*
* @param exchange
* @param chain
* @param gatewayLog
* @return
*/
private Mono writeBodyLog(ServerWebExchange exchange, GatewayFilterChain chain, GatewayLog gatewayLog) {
ServerRequest serverRequest = ServerRequest.create(exchange, messageReaders);
Mono<String> modifiedBody = serverRequest.bodyToMono(String.class)
.flatMap(body -> {
gatewayLog.setRequestBody(body);
return Mono.just(body);
});
// 通過 BodyInsert 插入 body(支持修改body), 避免 request body 只能獲取一次
BodyInserter bodyInserter = BodyInserters.fromPublisher(modifiedBody, String.class);
HttpHeaders headers = new HttpHeaders();
headers.putAll(exchange.getRequest().getHeaders());
headers.remove(HttpHeaders.CONTENT_LENGTH);
CachedBodyOutputMessage outputMessage = new CachedBodyOutputMessage(exchange, headers);
return bodyInserter.insert(outputMessage, new BodyInserterContext())
.then(Mono.defer(() -> {
// 重新封裝請求
ServerHttpRequest decoratedRequest = requestDecorate(exchange, headers, outputMessage);
// 記錄響應日志
ServerHttpResponseDecorator decoratedResponse = recordResponseLog(exchange, gatewayLog);
// 記錄普通的
return chain.filter(exchange.mutate().request(decoratedRequest).response(decoratedResponse).build())
.then(Mono.fromRunnable(() -> {
// 打印日志
writeAccessLog(gatewayLog);
}));
}));
}
/**
* 打印日志并將日志內(nèi)容寫入mongodb
*
* @param gatewayLog
*/
private void writeAccessLog(GatewayLog gatewayLog) {
log.info("寫入網(wǎng)關日志,日志內(nèi)容:" + JSON.toJSONString(gatewayLog));
accessLogService.saveAccessLog(gatewayLog);
}
/**
* 請求裝飾器,重新計算 headers
*
* @param exchange
* @param headers
* @param outputMessage
* @return
*/
private ServerHttpRequestDecorator requestDecorate(ServerWebExchange exchange, HttpHeaders headers,
CachedBodyOutputMessage outputMessage) {
return new ServerHttpRequestDecorator(exchange.getRequest()) {
@Override
public HttpHeaders getHeaders() {
long contentLength = headers.getContentLength();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.putAll(super.getHeaders());
if (contentLength > 0) {
httpHeaders.setContentLength(contentLength);
} else {
httpHeaders.set(HttpHeaders.TRANSFER_ENCODING, "chunked");
}
return httpHeaders;
}
@Override
public Flux<DataBuffer> getBody() {
return outputMessage.getBody();
}
};
}
/**
* 記錄響應日志
*
* @param exchange
* @param gatewayLog
* @return
*/
private ServerHttpResponseDecorator recordResponseLog(ServerWebExchange exchange, GatewayLog gatewayLog) {
ServerHttpResponse response = exchange.getResponse();
DataBufferFactory bufferFactory = response.bufferFactory();
return new ServerHttpResponseDecorator(response) {
@SneakyThrows
@Override
public Mono<Void> writeWith(Publisher<? extends DataBuffer> body) {
if (body instanceof Flux) {
String responseTime = simpleDateFormat.format(new Date().getTime());
gatewayLog.setResponseTime(responseTime);
// 計算執(zhí)行時間
long executeTime = (simpleDateFormat.parse(responseTime).getTime() - simpleDateFormat.parse(gatewayLog.getRequestTime()).getTime());
gatewayLog.setExecuteTime(executeTime);
// 獲取響應類型,如果是 json 就打印
String originalResponseContentType = exchange.getAttribute(ServerWebExchangeUtils.ORIGINAL_RESPONSE_CONTENT_TYPE_ATTR);
if (ObjectUtils.equals(this.getStatusCode(), HttpStatus.OK)
&& StringUtils.isNotBlank(originalResponseContentType)
&& originalResponseContentType.contains("application/json")) {
Flux<? extends DataBuffer> fluxBody = Flux.from(body);
return super.writeWith(fluxBody.buffer().map(dataBuffers -> {
// 合并多個流集合,解決返回體分段傳輸
DataBufferFactory dataBufferFactory = new DefaultDataBufferFactory();
DataBuffer join = dataBufferFactory.join(dataBuffers);
byte[] content = new byte[join.readableByteCount()];
join.read(content);
// 釋放掉內(nèi)存
DataBufferUtils.release(join);
String responseResult = new String(content, StandardCharsets.UTF_8);
gatewayLog.setResponseBody(responseResult);
return bufferFactory.wrap(content);
}));
}
}
return super.writeWith(body);
}
};
}
/**
* 調(diào)小優(yōu)先級使得該過濾器最先執(zhí)行
* @return
*/
@Override
public int getOrder() {
return -100;
}
}
- 測試
以筆者項目為例,通過網(wǎng)關調(diào)用order
服務:
curl 127.0.0.1:8090/order/getByCode/zsy
可以看到響應成功了,接下來我們就確認一下mongoDb中是否有存儲網(wǎng)關請求響應信息
{"status":100,"message":"操作成功","data":{"id":1,"accountCode":"zsy","accountName":"zsy","amount":10000.00},"success":true,"timestamp":1676439102837}
通過數(shù)據(jù)庫連接工具查詢,可以看到網(wǎng)關請求響應日志也成功存儲到MongoDB
中。
參考文獻
SpringCloud Alibaba微服務實戰(zhàn)二十四 - SpringCloud Gateway的全局異常處理
軟件開發(fā)設計中的上游與下游
SpringCloud Alibaba實戰(zhàn)二十九 | SpringCloud Gateway 請求響應日志
MongoDB 數(shù)據(jù)查詢操作
實戰(zhàn) | MongoDB的安裝配置文章來源:http://www.zghlxwxcb.cn/news/detail-759363.html
spring cloud gateway中實現(xiàn)請求、響應參數(shù)日志打印文章來源地址http://www.zghlxwxcb.cn/news/detail-759363.html
到了這里,關于Gateway全局異常處理及請求響應監(jiān)控的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!