Zuul网关的请求与响应日志打印、服务回退和异常处理

前言:

Zuul是在SpringCloud微服务框架中经常使用网关组件,它提供了认证、鉴权、限流、动态路由、监控、弹性、安全、负载均衡等功能,平时我们在项目中经常Zuul路由、鉴权、监控、负载均衡等功能。我们今天就详细介绍一下请求与响应日志打印、服务回退和异常处理这个三个功能的配置方法。

一、请求与响应日志打印:

请求与响应日志打印主要是基于Zuul的filter过滤器实现的。Zuul一共有五种类型的filter过滤器,它们分别是:PRE(在请求被路由之前调用)、ROUTING(将请求路由到微服务)、POST(在路由到微服务以后执行)、ERROR(在其他阶段发送错误时执行该过滤器)、SATAIC(自定义类的过滤器)。这里请求日志打印是基于PRE类型的filter过滤器实现,响应日志打印则基于POST类型的filter过滤器实现的。此外Zuul网关中许多的功能都通过filter过滤器实现,filter过滤器可以算是Zuul网关的灵魂。filter过滤器的相关知识请大家自行百度。

Zuul网关的请求与响应日志打印、服务回退和异常处理_第1张图片

 我在处理请求与响应日志打印这块时,遇到两个比较棘手的问题,问题如下:

问题1:跨域问题引起的请求两次的问题(第一次OPTION请求,第二次真实请求),导致打印两边日志的问题。该问题一开始都没有注意到,我以为就是请求太频繁的问题。我这里的解决方法是,首先判断请求类型,如果是OPTION请求直接返回不进行打印。OPTION请求一般都是预检请求,不会携带业务信息,日志打印的目的是为了统一记录业务信息,所以OPTION请求不必打印。

问题2:响应日志打印流程是将响应流转化成字符串进行打印,然后再字符串赋值到响应体中完成响应输出。看似正常操作却蕴含着危机,如果响应内容是图片、Word、Excel的等文件,经过上述的一波操作都会变成乱码。最后,我经过分析,业务上只需要响应的"Content-Type"类型为application/json才需要打印响应日志,而其他"Content-Type"类型没有必要输出响应日志。

具体代码:

1.请求日志打印过滤器:

RequestLogFilter.java

package com.hanxiaozhang.filter;

import com.alibaba.fastjson.JSON;
import com.hanxiaozhang.constant.Constant;
import com.hanxiaozhang.filter.body.BodyReaderHttpServletRequestWrapper;
import com.hanxiaozhang.util.JsonUtil;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.http.HttpServletRequestWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;


/**
 * 功能描述: 
* 〈请求参数日志过滤器〉 * * @Author:hanxinghua * @Date: 2020/7/4 */ @Slf4j @Component public class RequestLogFilter extends ZuulFilter { /** * 返回过滤器的类型 * 类型:pre、route、post、error等 * * @return */ @Override public String filterType() { return FilterConstants.PRE_TYPE; } /** * 返回一个int值来指定同一类型过滤器的执行顺序,不同的过滤器允许返回相同的数字 * * @return */ @Override public int filterOrder() { return FilterConstants.PRE_DECORATION_FILTER_ORDER - 1; } /** * 返回一个boolean值来判断该过滤器是否需要执行 * * @return */ @Override public boolean shouldFilter() { return true; } /** * 过滤器的具体逻辑 * * @return */ @Override public Object run() { RequestContext currentContext = RequestContext.getCurrentContext(); HttpServletRequest serverHttpRequest = currentContext.getRequest(); String method = serverHttpRequest.getMethod(); // 处理跨域发送两次请求,打印两遍日志问题 if (Constant.OPTIONS.equals(method)) { return null; } log.info("-------------------REQUEST-START-----------------"); // 记录下请求内容 log.info("URL: [{}]", serverHttpRequest.getRequestURL().toString()); log.info("HTTP-METHOD: [{}]", method); log.info("Content-type: [{}]", serverHttpRequest.getHeader(Constant.CONTENT_TYPE)); log.info("AUTHORIZATION: [{}]", serverHttpRequest.getHeader(Constant.AUTHORIZATION)); if (Constant.APP_JSON.equals(serverHttpRequest.getHeader(Constant.CONTENT_TYPE))) { //记录application/json时的传参 HttpServletRequestWrapper httpServletRequestWrapper = (HttpServletRequestWrapper) serverHttpRequest; BodyReaderHttpServletRequestWrapper wrapper = (BodyReaderHttpServletRequestWrapper) httpServletRequestWrapper.getRequest(); log.info("Request Parameters: \n [{}]", wrapper.getBody()); } else { //记录请求的键值对 log.info("Request Parameters: \n [{}]", handleInputParameters(serverHttpRequest.getParameterMap())); } return null; } /** * 处理参数 * * @param map * @return */ private String handleInputParameters(Map map){ if (map.isEmpty()) { return null; } return JSON.toJSONString(map); } }

 BodyReaderHttpServletRequestWrapper.java

package com.hanxiaozhang.filter.body;

import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
import java.io.*;

/**
 * 功能描述: 
* 〈BodyReaderHttpServletRequestWrapper〉 * * @Author:hanxinghua * @Date: 2020/7/4 */ public class BodyReaderHttpServletRequestWrapper extends HttpServletRequestWrapper { private final String body; public BodyReaderHttpServletRequestWrapper(HttpServletRequest request) throws IOException { super(request); StringBuilder stringBuilder = new StringBuilder(); BufferedReader bufferedReader = null; try { InputStream inputStream = request.getInputStream(); if (inputStream != null) { bufferedReader = new BufferedReader(new InputStreamReader(inputStream)); char[] charBuffer = new char[128]; int bytesRead = -1; while ((bytesRead = bufferedReader.read(charBuffer)) > 0) { stringBuilder.append(charBuffer, 0, bytesRead); } } else { stringBuilder.append(""); } } finally { if (bufferedReader != null) { bufferedReader.close(); } } body = stringBuilder.toString(); } @Override public ServletInputStream getInputStream() throws IOException { final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes()); ServletInputStream servletInputStream = new ServletInputStream() { @Override public boolean isFinished() { return false; } @Override public boolean isReady() { return false; } @Override public void setReadListener(ReadListener readListener) { } @Override public int read() throws IOException { return byteArrayInputStream.read(); } }; return servletInputStream; } @Override public BufferedReader getReader() throws IOException { return new BufferedReader(new InputStreamReader(this.getInputStream())); } public String getBody() { return this.body; } }

 HttpServletRequestWrapperFilter.java

package com.hanxiaozhang.filter.body;



import com.hanxiaozhang.constant.Constant;
import org.springframework.context.annotation.Configuration;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

/**
 * 功能描述: 
* 〈HttpServletRequestWrapperFilter〉 * * @Author:hanxinghua * @Date: 2020/7/4 */ @Configuration @WebFilter(filterName = "httpServletRequestWrapperFilter", urlPatterns = "/*") public class HttpServletRequestWrapperFilter implements Filter { @Override public void init(FilterConfig filterConfig) throws ServletException { } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { ServletRequest requestWrapper = null; String contentType = request.getContentType(); if (contentType != null && contentType.contains(Constant.APP_JSON)) { if (request instanceof HttpServletRequest) { requestWrapper = new BodyReaderHttpServletRequestWrapper((HttpServletRequest) request); } if (null == requestWrapper) { chain.doFilter(request, response); } else { chain.doFilter(requestWrapper, response); } }else { chain.doFilter(request, response); } } @Override public void destroy() { } }

 2.响应日志打印过滤器:

ResponseLogFilter.java 

package com.hanxiaozhang.filter;

import com.hanxiaozhang.constant.Constant;
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.ribbon.RibbonHttpResponse;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.stream.Collectors;


/**
 * 功能描述: 
* 〈响应参数日志过滤器〉 * * @Author:hanxinghua * @Date: 2020/7/4 */ @Slf4j @Component public class ResponseLogFilter extends ZuulFilter { @Override public String filterType() { return FilterConstants.POST_TYPE; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() { try { RequestContext currentContext = RequestContext.getCurrentContext(); String method = currentContext.getRequest().getMethod(); // 处理跨域发送两次请求,打印两遍日志问题 if (Constant.OPTIONS.equals(method)) { return null; } List contentTypeList = currentContext.getZuulResponseHeaders().stream(). filter(x -> x.first().equals(Constant.CONTENT_TYPE)).map(x -> x.second()).collect(Collectors.toList()); if (contentTypeList != null && !contentTypeList.isEmpty()) { String contentType = contentTypeList.get(0); log.info("Response Content-type: [{}]", contentType); InputStream responseDataStream = currentContext.getResponseDataStream(); if (responseDataStream != null && contentType !=null && contentType.contains(Constant.APP_JSON)) { String body = read(responseDataStream); responseDataStream.close(); log.info("Response Parameters: \n [{}]", body); currentContext.setResponseBody(body); } } } catch (IOException e) { e.printStackTrace(); } log.info("-------------------REQUEST-END-----------------"); return null; } private String read(InputStream inputStream) throws IOException { ByteArrayOutputStream result = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int length; while ((length = inputStream.read(buffer)) != -1) { result.write(buffer, 0, length); } return result.toString(StandardCharsets.UTF_8.name()); } }

二、服务回退:

Zuul层的服务回退,需要实现FallbackProvider接口,然后再指定那个微服务提供回退即可。服务回退可以有效的避免服务雪崩情况的发生 ,我这里没有具体为每一个服务指定回退,它们全部使用一个回退方法,粒度算是比较大的。具体代码如下:

AllServerFallback.java

package com.hanxiaozhang.fallback;

import com.hanxiaozhang.exception.ResultCode;
import com.hanxiaozhang.result.Result;
import com.hanxiaozhang.util.JsonUtil;
import com.netflix.hystrix.exception.HystrixTimeoutException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

/**
 * 功能描述: 
* 〈Zuul回退服务〉 * * @Author:hanxinghua * @Date: 2020/7/7 */ @Slf4j @Component public class AllServerFallback implements FallbackProvider { /** * 获取回退路由规则 * * @return */ @Override public String getRoute() { // *表示为所有微服务提供回退 return "*"; } /** * 根据执行失败的原因提供回退响应 * (可以获得造成回退的原因) * * @param cause * @return */ @Override public ClientHttpResponse fallbackResponse(Throwable cause) { log.info("into AllServerFallback.fallbackResponse(Throwable cause)"); if (cause instanceof HystrixTimeoutException) { return response(HttpStatus.GATEWAY_TIMEOUT); } else { return this.fallbackResponse(); } } /** * 提供回退响应 * * @return */ @Override public ClientHttpResponse fallbackResponse() { return this.response(HttpStatus.INTERNAL_SERVER_ERROR); } /** * 声明ClientHttpResponse * * @param status * @return */ private ClientHttpResponse response(final HttpStatus status) { return new ClientHttpResponse() { /** * fallback时的状态码 * * @return * @throws IOException */ @Override public HttpStatus getStatusCode() throws IOException { return status; } /** * 数字类型的状态码,本例返回的其实就是200,详见HttpStatus * * @return * @throws IOException */ @Override public int getRawStatusCode() throws IOException { return status.value(); } /** * 状态文本,本例返回的其实就是OK,详见HttpStatus * * @return * @throws IOException */ @Override public String getStatusText() throws IOException { return status.getReasonPhrase(); } @Override public void close() { } /** * 响应体 * * @return * @throws IOException */ @Override public InputStream getBody() throws IOException { String content = null; if (HttpStatus.GATEWAY_TIMEOUT.equals(status)) { content = JsonUtil.beanToJson(Result.error(ResultCode.SERVER_REQUEST_TIMEOUT)); } else { content = JsonUtil.beanToJson(Result.error(ResultCode.SERVER_ERROR)); } return new ByteArrayInputStream(content.getBytes()); } /** * headers设定 * * @return */ @Override public HttpHeaders getHeaders() { HttpHeaders headers = new HttpHeaders(); MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8")); headers.setContentType(mt); return headers; } }; } }

三、Zuul网关层的异常处理:

Zuul网关自己出现了异常也需要进行处理,并友好的提示用户。Zuul网关自己出现异常的方式有两种:一是自定义error错误页面,二是禁用zuul默认的异常处理filter(SendErrorFilter),自定义ErrorFilter。我选择的是方式一,具体实现如下:

package com.hanxiaozhang.exception;

import com.hanxiaozhang.result.Result;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.ErrorController;
import org.springframework.cloud.netflix.zuul.util.ZuulRuntimeException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.HashMap;
import java.util.Map;

/**
 * 功能描述: 
* 〈处理Zuul的异常〉 * * * @Author:hanxinghua * @Date: 2020/7/16 */ @Slf4j @RestController public class ErrorHandlerController implements ErrorController { @RequestMapping(value = "/error") public ResponseEntity error(HttpServletRequest request) { log.info("into ErrorHandlerController.error(HttpServletRequest request)"); RequestContext ctx = RequestContext.getCurrentContext(); if (ctx.isEmpty() || ctx.getThrowable() == null) { Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code"); HashMap map = new HashMap(2){{ put("status",statusCode); }}; return new ResponseEntity(Result.error(ResultCode.ZUUL_EXCEPTION,map), HttpStatus.valueOf(statusCode)); } ZuulException exception = findZuulException(ctx.getThrowable()); return new ResponseEntity<>(Result.error(ResultCode.ZUUL_EXCEPTION, exception, HttpStatus.BAD_GATEWAY), HttpStatus.BAD_GATEWAY); } @Override public String getErrorPath() { return "/error"; } ZuulException findZuulException(Throwable throwable) { // 这是由一个本地筛选器引发的故障 if (throwable.getCause() instanceof ZuulRuntimeException) { return (ZuulException) throwable.getCause().getCause(); } if (throwable.getCause() instanceof ZuulException) { return (ZuulException) throwable.getCause(); } // zuul生命周期引发的异常 if (throwable instanceof ZuulException) { return (ZuulException) throwable; } // fallback return new ZuulException(throwable, HttpServletResponse.SC_INTERNAL_SERVER_ERROR, null); } }

你可能感兴趣的:(#SpringCloud,Spring,#Springboot,restful,java,http)