Zuul是在SpringCloud微服务框架中经常使用网关组件,它提供了认证、鉴权、限流、动态路由、监控、弹性、安全、负载均衡等功能,平时我们在项目中经常Zuul路由、鉴权、监控、负载均衡等功能。我们今天就详细介绍一下请求与响应日志打印、服务回退和异常处理这个三个功能的配置方法。
请求与响应日志打印主要是基于Zuul的filter过滤器实现的。Zuul一共有五种类型的filter过滤器,它们分别是:PRE(在请求被路由之前调用)、ROUTING(将请求路由到微服务)、POST(在路由到微服务以后执行)、ERROR(在其他阶段发送错误时执行该过滤器)、SATAIC(自定义类的过滤器)。这里请求日志打印是基于PRE类型的filter过滤器实现,响应日志打印则基于POST类型的filter过滤器实现的。此外Zuul网关中许多的功能都通过filter过滤器实现,filter过滤器可以算是Zuul网关的灵魂。filter过滤器的相关知识请大家自行百度。
我在处理请求与响应日志打印这块时,遇到两个比较棘手的问题,问题如下:
问题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网关自己出现异常的方式有两种:一是自定义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);
}
}