Spring Cloud Zuul 那些你不知道的功能点

本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。 #1. /routes 端点web

当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。spring

借助这个端点,能够方便、直观地查看以及管理Zuul的路由。api

将全部端点都暴露出来,增长下面的配置:bash

management.endpoints.web.exposure.include=*
复制代码

访问 http://localhost:2103/actuator/routes 能够显示全部路由信息:app

{
  "/cxytiandi/**": "http://cxytiandi.com", 
  "/hystrix-api/**": "hystrix-feign-demo", 
  "/api/**": "forward:/local", 
  "/hystrix-feign-demo/**": "hystrix-feign-demo"
}
复制代码

2. /filters 端点

/fliters端点会返回Zuul中全部过滤器的信息。能够清楚的了解Zuul中目前有哪些过滤器,哪些被禁用了等详细信息。框架

访问 http://localhost:2103/actuator/filters 能够显示全部过滤器信息:ide

{
  "error": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.ErrorFilter", 
      "order": 100, 
      "disabled": false, 
      "static": true
    }
  ], 
  "post": [
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", 
      "order": 1000, 
      "disabled": false, 
      "static": true
    }
  ], 
  "pre": [
    {
      "class": "com.cxytiandi.zuul_demo.filter.IpFilter", 
      "order": 1, 
      "disabled": false, 
      "static": true
    }
  ], 
  "route": [ 
    {
      "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", 
      "order": 10, 
      "disabled": false, 
      "static": true
    }
  ]
}

复制代码

3. 文件上传

建立一个新的Maven项目zuul-file-demo,编写一个文件上传的接口,如代码清单7-20所示。微服务

代码清单 7-20 文件上传接口post

@RestController
public class FileController {

  @PostMapping("/file/upload")
  public String fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException {
      byte[] bytes = file.getBytes();
      File fileToSave = new File(file.getOriginalFilename());
      FileCopyUtils.copy(bytes, fileToSave);
      return fileToSave.getAbsolutePath();
  }

}
复制代码

将服务注册到Eureka中,服务名称为zuul-file-demo,经过PostMan来上传文件,如图7-4所示学习

图7-4

能够看到接口正常返回了文件上传以后的路径,接下来咱们换一个大一点的文件,文件大小为1.7MB。

图7-5

能够看到报错了(如图7-5所示),经过Zuul上传文件,若是超过1M须要配置上传文件的大小, Zuul和上传的服务都要加上配置:

spring.servlet.multipart.max-file-size=1000Mb
spring.servlet.multipart.max-request-size=1000Mb
复制代码

配置加完后从新上传就能够成功了,如图7-6所示。

图7-6

第二种解决办法是在网关的请求地址前面加上/zuul,就能够绕过Spring DispatcherServlet进行上传大文件。

# 正常的地址
http://localhost:2103/zuul-file-demo/file/upload
# 绕过的地址
http://localhost:2103/zuul/zuul-file-demo/file/upload
复制代码

经过加上/zuul前缀可让Zuul服务不用配置文件上传大小,可是接收文件的服务仍是须要配置文件上传大小,不然文件仍是会上传失败。

在上传大文件的时候,时间比较会比较长,这个时候须要设置合理的超时时间来避免超时。

ribbon.ConnectTimeout=3000
ribbon.ReadTimeout=60000
复制代码

在Hystrix隔离模式为线程下zuul.ribbon-isolation-strategy=thread,须要设置Hystrix超时时间。

hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000
复制代码

4. 请求响应信息输出

系统在生产环境出现问题时,排查问题最好的方式就是查看日志了,日志的记录尽可能详细,这样你才能快速定位问题。

下面带你们学习如何在Zuul中输出请求响应的信息来辅助咱们解决一些问题。

熟悉Zuul的朋友都知道,Zuul中有4种类型过滤器,每种都有特定的使用场景,要想记录响应数据,那么必须是在请求路由到了具体的服务以后,返回了才有数据,这种需求就适合用post过滤器来实现了。如代码清单7-21所示。

代码清单 7-21 Zull获取请求信息

HttpServletRequest req = (HttpServletRequest)RequestContext.getCurrentContext().getRequest();
System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort());
StringBuilder params = new StringBuilder("?");
// 获取URL参数
Enumeration<String> names = req.getParameterNames();
if( req.getMethod().equals("GET") ) {
   while (names.hasMoreElements()) {
         String name = (String) names.nextElement();
         params.append(name);
         params.append("=");
         params.append(req.getParameter(name));
         params.append("&");
   }
}
        
if (params.length() > 0) {
    params.delete(params.length()-1, params.length());
}
        
System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol());
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
      String name = (String) headers.nextElement();
      String value = req.getHeader(name);
      System.err.println("REQUEST:: > " + name + ":" + value);
}
        
final RequestContext ctx = RequestContext.getCurrentContext();
// 获取请求体参数
if (!ctx.isChunkedRequestBody()) {
    ServletInputStream inp = null;
    try {
         inp = ctx.getRequest().getInputStream();
         String body = null;
         if (inp != null) {
            body = IOUtils.toString(inp);
            System.err.println("REQUEST:: > " + body);  
         } catch (IOException e) {
                e.printStackTrace();
         }
    }
}
复制代码

输出效果以下:

获取响应内容第一种方式,如代码清单7-22所示。

代码清单 7-22 获取响应内容(一)

try {
     Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
     if (zuulResponse != null) {
          RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse;
          String body = IOUtils.toString(resp.getBody());
          System.err.println("RESPONSE:: > " + body);
          resp.close();
          RequestContext.getCurrentContext().setResponseBody(body);
     }
} catch (IOException e) {
     e.printStackTrace();
}
复制代码

获取响应内容第二种方式,如代码清单7-23所示。

代码清单 7-23 获取响应内容(二)

InputStream stream = RequestContext.getCurrentContext().getResponseDataStream();
try {
      if (stream != null) {
          String body = IOUtils.toString(stream);
          System.err.println("RESPONSE:: > " + body);
          RequestContext.getCurrentContext().setResponseBody(body);
      }    
} catch (IOException e) {
      e.printStackTrace();
}
复制代码

为何上面两种方式能够取到响应内容?

在RibbonRoutingFilter或者SimpleHostRoutingFilter中能够看到下面一段代码,如代码清单7-24所示。

代码清单 7-24 响应内容获取源码

public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    }
    catch (ZuulException ex) {
        throw new ZuulRuntimeException(ex);
    }
    catch (Exception ex) {
        throw new ZuulRuntimeException(ex);
    }
}
复制代码

forward()方法对服务调用,拿到响应结果,经过setResponse()方法进行响应的设置,如代码清单7-25所示。

代码清单 7-25 setResponse(一)

protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException {
    RequestContext.getCurrentContext().set("zuulResponse", resp);
    this.helper.setResponse(resp.getStatusCode().value(),
    resp.getBody() == null ? null : resp.getBody(), resp.getHeaders());
}
复制代码

上面第一行代码就能够解释咱们的第一种获取的方法,这边直接把响应内容加到了RequestContext中。

第二种方式的解释就在helper.setResponse的逻辑里面了,如代码清单7-26所示。

代码清单 7-26 setResponse(二)

public void setResponse(int status, InputStream entity,
            MultiValueMap<String, String> headers) throws IOException {
    RequestContext context = RequestContext.getCurrentContext();
    context.setResponseStatusCode(status);
    if (entity != null) {
        context.setResponseDataStream(entity);
    }

    // .....
}
复制代码

5. Zuul自带的Debug功能

Zuul中自带了一个DebugFilter,一开始我也没明白这个DebugFilter有什么用,看名称很容易理解,用来调试的,但是你看它源码几乎没什么逻辑,就set了两个值而已,如代码清单7-27所示。

代码清单 7-27 DebugFilter run方法

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ctx.setDebugRouting(true);
    ctx.setDebugRequest(true);
    return null;
}
复制代码

要想让这个过滤器执行就得研究下它的shouldFilter()方法,如代码清单7-28所示。 代码清单 7-28 DebugFilter shouldFilter 方法

@Override
public boolean shouldFilter() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
      return true;
    }
    return ROUTING_DEBUG.get();
}
复制代码

只要知足两个条件中的任何一个就能够开启这个过滤器,第一个条件是请求参数中带了某个参数=true就能够开启,这个参数名是经过下面的代码获取的,如代码清单7-29所示。

代码清单 7-29 DebugFilter启用参数(一)

private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory
      .getInstance().getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug");
复制代码

DynamicStringProperty是Netflix的配置管理框架Archaius提供的API,能够从配置中心获取配置,因为Netflix没有开源Archaius的服务端,因此这边用的就是默认值debug,若是你们想动态去获取这个值的话能够用携程开源的Apollo来对接Archaius,这个在后面章节给你们讲解。

能够在请求地址后面追加debug=true来开启这个过滤器,参数名称debug也能够在配置文件中进行覆盖,用zuul.debug.parameter指定,不然就是从Archaius中获取,没有对接Archaius那就是默认值debug。

第二个条件代码,如代码清单7-30所示。

代码清单 7-30 DebugFilter启用参数(二)

private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory
      .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false);
  
复制代码

是经过配置zuul.debug.request来决定的,能够在配置文件中配置zuul.debug.request=true开启DebugFilter过滤器。

DebugFilter过滤器开启后,并没什么效果,在run方法中只是设置了DebugRouting和DebugRequest两个值为true,因而继续看源码,发如今不少地方有这么一段代码,好比com.netflix.zuul.FilterProcessor.runFilters(String)中,如代码清单7-31所示。

代码清单 7-31 Debug信息添加

if (RequestContext.getCurrentContext().debugRouting()) {
    Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
}
复制代码

当debugRouting为true的时候就会添加一些Debug信息到RequestContext中。如今明白了DebugFilter中为何要设置DebugRouting和DebugRequest两个值为true。

到这步后发现仍是很迷茫,通常咱们调试信息的话确定是用日志输出来的,日志级别就是Debug,但这个Debug信息只是累加起来存储到RequestContext中,没有对使用者展现。

继续看代码吧,功夫不负有心人,在org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter.addResponseHeaders()这段代码中看到了但愿。如代码清单7-32所示。

代码清单 7-32 Debug信息设置响应

private void addResponseHeaders() {
     RequestContext context = RequestContext.getCurrentContext();
     HttpServletResponse servletResponse = context.getResponse();
     if (this.zuulProperties.isIncludeDebugHeader()) {
         @SuppressWarnings("unchecked")
         List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY);
         if (rd != null) {
            StringBuilder debugHeader = new StringBuilder();
            for (String it : rd) {
               debugHeader.append("[[[" + it + "]]]");
            }
            servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString());
         }
     }
}
复制代码

核心代码在于this.zuulProperties.isIncludeDebugHeader(),只有知足这个条件才会把RequestContext中的调试信息做为响应头输出,在配置文件中增长下面的配置便可:

zuul.include-debug-header=true
复制代码

最后在请求的响应头中能够看到调试内容,如图7-7所示。

图7-7

本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。

相关文章
相关标签/搜索