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

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

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

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

将所有端点都暴露出来,增加下面的配置:

management.endpoints.web.exposure.include=*

访问 http://localhost:2103/actuator/routes 可以显示所有路由信息:

1{ 2 "/cxytiandi/**": "http://cxytiandi.com", 3 "/hystrix-api/**": "hystrix-feign-demo", 4 "/api/**": "forward:/local", 5 "/hystrix-feign-demo/**": "hystrix-feign-demo" 6}

2. /filters 端点

/fliters端点会返回Zuul中所有过滤器的信息。可以清楚的了解Zuul中目前有哪些过滤器,哪些被禁用了等详细信息。

访问 http://localhost:2103/actuator/filters 可以显示所有过滤器信息:

1{ 2 "error": [ 3 { 4 "class": "com.cxytiandi.zuul_demo.filter.ErrorFilter", 5 "order": 100, 6 "disabled": false, 7 "static": true 8 } 9 ], 10 "post": [ 11 { 12 "class": "org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter", 13 "order": 1000, 14 "disabled": false, 15 "static": true 16 } 17 ], 18 "pre": [ 19 { 20 "class": "com.cxytiandi.zuul_demo.filter.IpFilter", 21 "order": 1, 22 "disabled": false, 23 "static": true 24 } 25 ], 26 "route": [ 27 { 28 "class": "org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter", 29 "order": 10, 30 "disabled": false, 31 "static": true 32 } 33 ] 34} 35

3. 文件上传

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

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

1@RestController 2public class FileController { 3 4 @PostMapping("/file/upload") 5 public String fileUpload(@RequestParam(value = "file") MultipartFile file) throws IOException { 6 byte[] bytes = file.getBytes(); 7 File fileToSave = new File(file.getOriginalFilename()); 8 FileCopyUtils.copy(bytes, fileToSave); 9 return fileToSave.getAbsolutePath(); 10 } 11 12}

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

图7-4

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

图7-5

可以看到报错了(如图7-5所示),通过Zuul上传文件,如果超过1M需要配置上传文件的大小, Zuul和上传的服务都要加上配置:

1spring.servlet.multipart.max-file-size=1000Mb 2spring.servlet.multipart.max-request-size=1000Mb

配置加完后重新上传就可以成功了,如图7-6所示。

图7-6

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

1# 正常的地址 2http://localhost:2103/zuul-file-demo/file/upload 3# 绕过的地址 4http://localhost:2103/zuul/zuul-file-demo/file/upload

通过加上/zuul前缀可以让Zuul服务不用配置文件上传大小,但是接收文件的服务还是需要配置文件上传大小,否则文件还是会上传失败。

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

1ribbon.ConnectTimeout=3000 2ribbon.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获取请求信息

1HttpServletRequest req = (HttpServletRequest)RequestContext.getCurrentContext().getRequest(); 2System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort()); 3StringBuilder params = new StringBuilder("?"); 4// 获取URL参数 5Enumeration<String> names = req.getParameterNames(); 6if( req.getMethod().equals("GET") ) { 7 while (names.hasMoreElements()) { 8 String name = (String) names.nextElement(); 9 params.append(name); 10 params.append("="); 11 params.append(req.getParameter(name)); 12 params.append("&"); 13 } 14} 15 16if (params.length() > 0) { 17 params.delete(params.length()-1, params.length()); 18} 19 20System.err.println("REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol()); 21Enumeration<String> headers = req.getHeaderNames(); 22while (headers.hasMoreElements()) { 23 String name = (String) headers.nextElement(); 24 String value = req.getHeader(name); 25 System.err.println("REQUEST:: > " + name + ":" + value); 26} 27 28final RequestContext ctx = RequestContext.getCurrentContext(); 29// 获取请求体参数 30if (!ctx.isChunkedRequestBody()) { 31 ServletInputStream inp = null; 32 try { 33 inp = ctx.getRequest().getInputStream(); 34 String body = null; 35 if (inp != null) { 36 body = IOUtils.toString(inp); 37 System.err.println("REQUEST:: > " + body); 38 } catch (IOException e) { 39 e.printStackTrace(); 40 } 41 } 42}

输出效果如下:

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

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

1try { 2 Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse"); 3 if (zuulResponse != null) { 4 RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse; 5 String body = IOUtils.toString(resp.getBody()); 6 System.err.println("RESPONSE:: > " + body); 7 resp.close(); 8 RequestContext.getCurrentContext().setResponseBody(body); 9 } 10} catch (IOException e) { 11 e.printStackTrace(); 12}

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

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

1InputStream stream = RequestContext.getCurrentContext().getResponseDataStream(); 2try { 3 if (stream != null) { 4 String body = IOUtils.toString(stream); 5 System.err.println("RESPONSE:: > " + body); 6 RequestContext.getCurrentContext().setResponseBody(body); 7 } 8} catch (IOException e) { 9 e.printStackTrace(); 10}

为什么上面两种方式可以取到响应内容?

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

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

1public Object run() { 2 RequestContext context = RequestContext.getCurrentContext(); 3 this.helper.addIgnoredHeaders(); 4 try { 5 RibbonCommandContext commandContext = buildCommandContext(context); 6 ClientHttpResponse response = forward(commandContext); 7 setResponse(response); 8 return response; 9 } 10 catch (ZuulException ex) { 11 throw new ZuulRuntimeException(ex); 12 } 13 catch (Exception ex) { 14 throw new ZuulRuntimeException(ex); 15 } 16}

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

代码清单 7-25 setResponse(一)

1protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException { 2 RequestContext.getCurrentContext().set("zuulResponse", resp); 3 this.helper.setResponse(resp.getStatusCode().value(), 4 resp.getBody() == null ? null : resp.getBody(), resp.getHeaders()); 5}

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

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

代码清单 7-26 setResponse(二)

1 public void setResponse(int status, InputStream entity, 2 MultiValueMap<String, String> headers) throws IOException { 3 RequestContext context = RequestContext.getCurrentContext(); 4 context.setResponseStatusCode(status); 5 if (entity != null) { 6 context.setResponseDataStream(entity); 7 } 8 9 // ..... 10}

5. Zuul自带的Debug功能

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

代码清单 7-27 DebugFilter run方法

1@Override 2public Object run() { 3 RequestContext ctx = RequestContext.getCurrentContext(); 4 ctx.setDebugRouting(true); 5 ctx.setDebugRequest(true); 6 return null; 7}

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

1@Override 2public boolean shouldFilter() { 3 HttpServletRequest request = RequestContext.getCurrentContext().getRequest(); 4 if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) { 5 return true; 6 } 7 return ROUTING_DEBUG.get(); 8}

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

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

1private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory 2 .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启用参数(二)

1private static final DynamicBooleanProperty ROUTING_DEBUG = DynamicPropertyFactory 2 .getInstance().getBooleanProperty(ZuulConstants.ZUUL_DEBUG_REQUEST, false); 3

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

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

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

1if (RequestContext.getCurrentContext().debugRouting()) { 2 Debug.addRoutingDebug("Invoking {" + sType + "} type filters"); 3}

当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信息设置响应

1private void addResponseHeaders() { 2 RequestContext context = RequestContext.getCurrentContext(); 3 HttpServletResponse servletResponse = context.getResponse(); 4 if (this.zuulProperties.isIncludeDebugHeader()) { 5 @SuppressWarnings("unchecked") 6 List<String> rd = (List<String>) context.get(ROUTING_DEBUG_KEY); 7 if (rd != null) { 8 StringBuilder debugHeader = new StringBuilder(); 9 for (String it : rd) { 10 debugHeader.append("[[[" + it + "]]]"); 11 } 12 servletResponse.addHeader(X_ZUUL_DEBUG_HEADER, debugHeader.toString()); 13 } 14 } 15}

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

zuul.include-debug-header=true

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

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

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )