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

1. /routes 端点

当@EnableZuulProxy与Spring Boot Actuator配合使用时,Zuul会暴露一个路由管理端点/routes。
借助这个端点,可以方便、直观地查看以及管理Zuul的路由。
将所有端点都暴露出来,增加下面的配置:
  1. 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. }

3. 文件上传

创建一个新的Maven项目zuul-file-demo,编写一个文件上传的接口,如代码清单7-20所示。
代码清单 7-20 文件上传接口
  1. @RestController
  2. publicclassFileController{
  3. @PostMapping("/file/upload")
  4. publicString fileUpload(@RequestParam(value ="file")MultipartFile file)throwsIOException{
  5. byte[] bytes = file.getBytes();
  6. File fileToSave =newFile(file.getOriginalFilename());
  7. FileCopyUtils.copy(bytes, fileToSave);
  8. return fileToSave.getAbsolutePath();
  9. }
  10. }
将服务注册到Eureka中,服务名称为zuul-file-demo,通过PostMan来上传文件,如图7-4所示
可以看到接口正常返回了文件上传之后的路径,接下来我们换一个大一点的文件,文件大小为1.7MB。
可以看到报错了(如图7-5所示),通过Zuul上传文件,如果超过1M需要配置上传文件的大小, Zuul和上传的服务都要加上配置:
  1. spring.servlet.multipart.max-file-size=1000Mb
  2. spring.servlet.multipart.max-request-size=1000Mb
配置加完后重新上传就可以成功了,如图7-6所示。
第二种解决办法是在网关的请求地址前面加上/zuul,就可以绕过Spring DispatcherServlet进行上传大文件。
  1. # 正常的地址
  2. http://localhost:2103/zuul-file-demo/file/upload
  3. # 绕过的地址
  4. http://localhost:2103/zuul/zuul-file-demo/file/upload
通过加上/zuul前缀可以让Zuul服务不用配置文件上传大小,但是接收文件的服务还是需要配置文件上传大小,否则文件还是会上传失败。
在上传大文件的时候,时间比较会比较长,这个时候需要设置合理的超时时间来避免超时。
  1. ribbon.ConnectTimeout=3000
  2. ribbon.ReadTimeout=60000
在Hystrix隔离模式为线程下zuul.ribbon-isolation-strategy=thread,需要设置Hystrix超时时间。
  1. hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=60000

4. 请求响应信息输出

系统在生产环境出现问题时,排查问题最好的方式就是查看日志了,日志的记录尽量详细,这样你才能快速定位问题。
下面带大家学习如何在Zuul中输出请求响应的信息来辅助我们解决一些问题。
熟悉Zuul的朋友都知道,Zuul中有4种类型过滤器,每种都有特定的使用场景,要想记录响应数据,那么必须是在请求路由到了具体的服务之后,返回了才有数据,这种需求就适合用post过滤器来实现了。如代码清单7-21所示。
代码清单 7-21 Zull获取请求信息
  1. HttpServletRequest req =(HttpServletRequest)RequestContext.getCurrentContext().getRequest();
  2. System.err.println("REQUEST:: "+ req.getScheme()+" "+ req.getRemoteAddr()+":"+ req.getRemotePort());
  3. StringBuilderparams=newStringBuilder("?");
  4. // 获取URL参数
  5. Enumeration<String> names = req.getParameterNames();
  6. if( 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. if(params.length()>0){
  16. params.delete(params.length()-1,params.length());
  17. }
  18. System.err.println("REQUEST:: > "+ req.getMethod()+" "+ req.getRequestURI()+params+" "+ req.getProtocol());
  19. Enumeration<String> headers = req.getHeaderNames();
  20. while(headers.hasMoreElements()){
  21. String name =(String) headers.nextElement();
  22. String value = req.getHeader(name);
  23. System.err.println("REQUEST:: > "+ name +":"+ value);
  24. }
  25. finalRequestContext ctx =RequestContext.getCurrentContext();
  26. // 获取请求体参数
  27. if(!ctx.isChunkedRequestBody()){
  28. ServletInputStream inp =null;
  29. try{
  30. inp = ctx.getRequest().getInputStream();
  31. String body =null;
  32. if(inp !=null){
  33. body =IOUtils.toString(inp);
  34. System.err.println("REQUEST:: > "+ body);
  35. }catch(IOException e){
  36. e.printStackTrace();
  37. }
  38. }
  39. }
输出效果如下:
获取响应内容第一种方式,如代码清单7-22所示。
代码清单 7-22 获取响应内容(一)
  1. try{
  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 获取响应内容(二)
  1. InputStream stream =RequestContext.getCurrentContext().getResponseDataStream();
  2. try{
  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 响应内容获取源码
  1. publicObject 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. thrownewZuulRuntimeException(ex);
  12. }
  13. catch(Exception ex){
  14. thrownewZuulRuntimeException(ex);
  15. }
  16. }
forward()方法对服务调用,拿到响应结果,通过setResponse()方法进行响应的设置,如代码清单7-25所示。
代码清单 7-25 setResponse(一)
  1. protectedvoid setResponse(ClientHttpResponse resp)throwsClientException,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. publicvoid setResponse(int status,InputStream entity,
  2. MultiValueMap<String,String> headers)throwsIOException{
  3. RequestContext context =RequestContext.getCurrentContext();
  4. context.setResponseStatusCode(status);
  5. if(entity !=null){
  6. context.setResponseDataStream(entity);
  7. }
  8. // .....
  9. }

5. Zuul自带的Debug功能

Zuul中自带了一个DebugFilter,一开始我也没明白这个DebugFilter有什么用,看名称很容易理解,用来调试的,可是你看它源码几乎没什么逻辑,就set了两个值而已,如代码清单7-27所示。
代码清单 7-27 DebugFilter run方法
  1. @Override
  2. publicObject run(){
  3. RequestContext ctx =RequestContext.getCurrentContext();
  4. ctx.setDebugRouting(true);
  5. ctx.setDebugRequest(true);
  6. returnnull;
  7. }
要想让这个过滤器执行就得研究下它的shouldFilter()方法,如代码清单7-28所示。
代码清单 7-28 DebugFilter shouldFilter 方法
  1. @Override
  2. publicboolean shouldFilter(){
  3. HttpServletRequest request =RequestContext.getCurrentContext().getRequest();
  4. if("true".equals(request.getParameter(DEBUG_PARAMETER.get()))){
  5. returntrue;
  6. }
  7. return ROUTING_DEBUG.get();
  8. }
只要满足两个条件中的任何一个就可以开启这个过滤器,第一个条件是请求参数中带了某个参数=true就可以开启,这个参数名是通过下面的代码获取的,如代码清单7-29所示。
代码清单 7-29 DebugFilter启用参数(一)
  1. privatestaticfinalDynamicStringProperty 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启用参数(二)
  1. privatestaticfinalDynamicBooleanProperty ROUTING_DEBUG =DynamicPropertyFactory
  2. .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信息添加
  1. if(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信息设置响应
  1. privatevoid 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 =newStringBuilder();
  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中的调试信息作为响应头输出,在配置文件中增加下面的配置即可:
  1. zuul.include-debug-header=true
最后在请求的响应头中可以看到调试内容,如图7-7所示。
本文摘自于 《Spring Cloud微服务 入门 实战与进阶》 一书。
本站所有文章均由网友分享,仅用于参考学习用,请勿直接转载,如有侵权,请联系网站客服删除相关文章。若由于商用引起版权纠纷,一切责任均由使用者承担
极客文库 » Spring Cloud Zuul 那些你不知道的功能点

Leave a Reply

欢迎加入「极客文库」,成为原创作者从这里开始!

立即加入 了解更多