Spring Cloud Zuul过滤器介绍及使用(传递数据、拦截请求和异常处理)

news2024/11/19 10:20:44

在教程《Zuul网关的介绍及使用》中一开始就介绍过,Zuul 可以实现很多高级的功能,比如限流、认证等。想要实现这些功能,必须要基于 Zuul 给我们提供的核心组件“过滤器”。下面我们一起来了解一下 Zuul 的过滤器。

过滤器类型

Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。

而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。

1)pre

可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。

2)route

在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。

3)post

在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。

4)error

处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。

请求生命周期

可以通过图 1 看出整个过滤器的执行生命周期,此图来自 Zuul GitHub wiki 主页。


图 1 过滤器生命周期

通过上面的图可以清楚地知道整个执行的顺序,请求发过来首先到 pre 过滤器,再到 routing 过滤器,最后到 post 过滤器,任何一个过滤器有异常都会进入 error 过滤器。

通过 com.netflix.zuul.http.ZuulServlet 也可以看出完整执行顺序,ZuulServlet 类似 Spring-Mvc 的 DispatcherServlet,所有的 Request 都要经过 ZuulServlet 的处理。

ZuulServlet 源码如下所示:

  1. @Override
  2. public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse)
  3. throws ServletException, IOException {
  4. try {
  5. init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
  6. RequestContext context = RequestContext.getCurrentContext();
  7. context.setZuulEngineRan();
  8. try {
  9. preRoute();
  10. } catch (ZuulException e) {
  11. error(e);
  12. postRoute();
  13. return;
  14. }
  15. try {
  16. route();
  17. } catch (ZuulException e) {
  18. error(e);
  19. postRoute();
  20. return;
  21. }
  22. try {
  23. postRoute();
  24. } catch (ZuulException e) {
  25. error(e);
  26. return;
  27. }
  28. } catch (Throwable e) {
  29. error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
  30. } finally {
  31. RequestContext.getCurrentContext().unset();
  32. }
  33. }

使用过滤器

我们创建一个 pre 过滤器,来实现 IP 黑名单的过滤操作,代码如下所示。

  1. public class IpFilter extends ZuulFilter {
  2. // IP黑名单列表
  3. private List<String> blackIpList = Arrays.asList("127.0.0.1");
  4. public IpFilter() {
  5. super();
  6. }
  7. @Override
  8. public boolean shouldFilter() {
  9. return true
  10. }
  11. @Override
  12. public String filterType() {
  13. return "pre";
  14. }
  15. @Override
  16. public int filterOrder() {
  17. return 1;
  18. }
  19. @Override
  20. public Object run() {
  21. RequestContext ctx = RequestContext.getCurrentContext();
  22. String ip = IpUtils.getIpAddr(ctx.getRequest());
  23. // 在黑名单中禁用
  24. if (StringUtils.isNotBlank(ip) && blackIpList.contains(ip)) {
  25. ctx.setSendZuulResponse(false);
  26. ResponseData data = ResponseData.fail("非法请求 ", ResponseCode.NO_AUTH_CODE.getCode());
  27. ctx.setResponseBody(JsonUtils.toJson(data));
  28. ctx.getResponse().setContentType("application/json; charset=utf-8");
  29. return null;
  30. }
  31. return null;
  32. }
  33. }

由代码可知,自定义过滤器需要继承 ZuulFilter,并且需要实现下面几个方法:

1)shouldFilter

是否执行该过滤器,true 为执行,false 为不执行,这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器。

2)filterType

过滤器类型,可选值有 pre、route、post、error。

3)filterOrder

过滤器的执行顺序,数值越小,优先级越高。

4)run

执行自己的业务逻辑,本段代码中是通过判断请求的 IP 是否在黑名单中,决定是否进行拦截。blackIpList 字段是 IP 的黑名单,判断条件成立之后,通过设置 ctx.setSendZuulResponse(false),告诉 Zuul 不需要将当前请求转发到后端的服务了。通过 setResponseBody 返回数据给客户端。

过滤器定义完成之后我们需要配置过滤器才能生效,IP 过滤器配置代码如下所示。

  1. @Configuration
  2. public class FilterConfig {
  3. @Bean
  4. public IpFilter ipFilter() {
  5. return new IpFilter();
  6. }
  7. }

过滤器禁用

有的场景下,我们需要禁用过滤器,此时可以采取下面的两种方式来实现:

  • 利用 shouldFilter 方法中的 return false 让过滤器不再执行
  • 通过配置方式来禁用过滤器,格式为“zuul. 过滤器的类名.过滤器类型 .disable=true”。如果我们需要禁用“使用过滤器”部分中的 IpFilter,可以用下面的配置:
  1. zuul.IpFilter.pre.disable=true

过滤器中传递数据

项目中往往会存在很多的过滤器,执行的顺序是根据 filterOrder 决定的,那么肯定有一些过滤器是在后面执行的,如果你有这样的需求:第一个过滤器需要告诉第二个过滤器一些信息,这个时候就涉及在过滤器中怎么去传递数据给后面的过滤器。

实现这种传值的方式笔者第一时间就想到了用 ThreadLocal,既然我们用了 Zuul,那么 Zuul 肯定有解决方案,比如可以通过 RequestContext 的 set 方法进行传递,RequestContext 的原理就是 ThreadLocal。

  1. RequestContext ctx = RequestContext.getCurrentContext();
  2. ctx.set("msg", "你好吗");

后面的过滤就可以通过 RequestContext 的 get 方法来获取数据:

  1. RequestContext ctx = RequestContext.getCurrentContext();
  2. ctx.get("msg");

上面我们说到 RequestContext 的原理就是 ThreadLocal,这不是笔者自己随便说的,而是笔者看过源码得出来的结论,下面请看源码,代码如下所示。

  1. protected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
  2. @Override
  3. protected RequestContext initialValue() {
  4. try {
  5. return contextClass.newInstance();
  6. } catch (Throwable e) {
  7. throw new RuntimeException(e);
  8. }
  9. }
  10. };
  11. public static RequestContext getCurrentContext() {
  12. if (testContext != null)
  13. return testContext;
  14. RequestContext context = threadLocal.get();
  15. return context;
  16. }

过滤器拦截请求

在过滤器中对请求进行拦截是一个很常见的需求,本节的“使用过滤器”部分中讲解的 IP 黑名单限制就是这样的一个需求。如果请求在黑名单中,就不能让该请求继续往下执行,需要对其进行拦截并返回结果给客户端。

拦截和返回结果只需要 5 行代码即可实现,代码如下所示。

  1. RequestContext ctx = RequestContext.getCurrentContext();
  2. ctx.setSendZuulResponse(false);
  3. ctx.set("sendForwardFilter.ran", true);
  4. ctx.setResponseBody("返回信息");
  5. return null;

ctx.setSendZuulResponse(false) 告诉 Zuul 不需要将当前请求转发到后端的服务。原理体现在 shouldFilter() 方法上,源码在 org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter 中的 shouldFilter() 方法里,代码如下所示。

  1. @Override
  2. public boolean shouldFilter() {
  3. RequestContext ctx = RequestContext.getCurrentContext();
  4. return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null && ctx.sendZuulResponse());
  5. }

代码“ctx.set(“sendForwardFilter.ran”,true);”是用来拦截本地转发请求的,当我们配置了 forward:/local 的路由,ctx.setSendZuulResponse(false) 对 forward 是不起作用的,需要设置 ctx.set(“sendForwardFilter.ran”,true) 才行。

对应实现的源码体现在 org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter的shouldFilter() 方法中,代码如下所示。

  1. protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran";
  2. @Override
  3. public boolean shouldFilter() {
  4. RequestContext ctx = RequestContext.getCurrentContext();
  5. return ctx.containsKey(FORWARD_TO_KEY) && !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
  6. }

到这一步之后,当前的过滤器中确实将请求进行拦截了,并且可以给客户端返回信息。但是当你的项目中有多个过滤器的时候,假如你需要过滤的那个过滤器是第一个执行的,发现非法请求,然后进行拦截,以笔者之前使用 javax.servlet.Filter 的经验,进行拦截之后,在 chain.doFilter 之前进行返回就可以让过滤器不往下执行了。

但是 Zuul 中的过滤器不一样,即使你刚刚通过 ctx.setSendZuulResponse(false) 设置了不路由到服务,并且返回 null,那只是当前的过滤器执行完成了,后面还有很多过滤器在等着执行。

通过源码可以看出,Zuul 中 Filter 的执行逻辑如下:在 ZuulServlet 中的 service 方法中执行对应的 Filter,比如 preRoute()。preRoute 中会通过 ZuulRunner 来执行(代码如下所示)。

  1. void preRoute() throws ZuulException {
  2. zuulRunner.preRoute();
  3. }

zuulRunner 中通过调用 FilterProcessor 来执行 Filter(代码如下所示)。

  1. public void preRoute() throws ZuulException {
  2. FilterProcessor.getInstance().preRoute();
  3. }

FilterProcessor 通过过滤器类型获取所有过滤器,并循环执行(代码如下所示)。

  1. public Object runFilters(String sType) throws Throwable {
  2. if (RequestContext.getCurrentContext().debugRouting()) {
  3. Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
  4. }
  5. boolean bResult = false;
  6. List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
  7. if (list != null) {
  8. for (int i = 0; i < list.size(); i++) {
  9. ZuulFilter zuulFilter = list.get(i);
  10. Object result = processZuulFilter(zuulFilter);
  11. if (result != null && result instanceof Boolean) {
  12. bResult |= ((Boolean) result);
  13. }
  14. }
  15. }
  16. return bResult;
  17. }

通过上面的讲解,我们大致知道了为什么所有的过滤器都会执行,解决这个问题的办法就是通过 shouldFilter 来处理,即在拦截之后通过数据传递的方式告诉下一个过滤器是否要执行。

改造上面的拦截代码,增加一行数据传递的代码:

  1. ctx.set("isSuccess", false);

在 RequestContext 中设置一个值来标识是否成功,当为 true 的时候,后续的过滤器才执行,若为 false 则不执行。

利用这种方法,在后面的过滤器就需要用到这个值来决定自己此时是否需要执行,此时只需要在 shouldFilter 方法中加上如下所示的代码即可。

  1. public boolean shouldFilter() {
  2. RequestContext ctx = RequestContext.getCurrentContext();
  3. Object success = ctx.get("isSuccess");
  4. return success == null ? true : Boolean.parseBoolean(success.toString());
  5. }

过滤器中异常处理

对于异常来说,无论在哪个地方都需要处理。过滤器中的异常主要发生在 run 方法中,可以用 try catch 来处理。Zuul 中也为我们提供了一个异常处理的过滤器,当过滤器在执行过程中发生异常,若没有被捕获到,就会进入 error 过滤器中。

我们可以定义一个 error 过滤器来记录异常信息,代码如下所示。

  1. public class ErrorFilter extends ZuulFilter {
  2. private Logger log = LoggerFactory.getLogger(ErrorFilter.class);
  3. @Override
  4. public String filterType() {
  5. return "error";
  6. }
  7. @Override
  8. public int filterOrder() {
  9. return 100;
  10. }
  11. @Override
  12. public boolean shouldFilter() {
  13. return true;
  14. }
  15. @Override
  16. public Object run() {
  17. RequestContext ctx = RequestContext.getCurrentContext();
  18. Throwable throwable = ctx.getThrowable();
  19. log.error("Filter Erroe : {}", throwable.getCause().getMessage());
  20. return null;
  21. }
  22. }

然后我们在其他过滤器中模拟一个异常信息,改造本节“使用过滤器”部分中的 IpFilter 代码,在 run 方法中增加下面的代码来模拟 java.lang.ArithmeticException:/by zero。

 
  1. System.out.println(2/0);

访问我们的服务接口可以看到图 2 所示的内容,500 错误信息表示控制台也有异常日志输出。


图 2 500错误页面

我们后端的接口服务都是 REST 风格的API,返回的数据都有固定的 Json 格式,现在变成这样一个页面了,让客户端那边怎么处理?我们通过实现 ErrorController 来解决这个问题。

ErrorController 的代码如下所示:

  1. @RestController
  2. public class ErrorHandlerController implements ErrorController {
  3. @Autowired
  4. private ErrorAttributes errorAttributes;
  5. @Override
  6. public String getErrorPath() {
  7. return "/error";
  8. }
  9. @RequestMapping("/error")
  10. public ResponseData error(HttpServletRequest request) {
  11. Map<String, Object> errorAttributes = getErrorAttributes(request);
  12. String message = (String) errorAttributes.get("message");
  13. String trace = (String) errorAttributes.get("trace");
  14. if (StringUtils.isNotBlank(trace)) {
  15. message += String.format("and trace %s", trace);
  16. }
  17. return ResponseData.fail(message, ResponseCode.SERVER_ERROR_CODE.getCode());
  18. }
  19. private Map<String, Object> getErrorAttributes(HttpServletRequest request) {
  20. return errorAttributes.getErrorAttributes(new ServletWebRequest(request), true);
  21. }
  22. }

我们再次访问之前的接口,这次就不是一个错误页面了,而是我们固定好的 Json 格式的数据,如图 3 所示。


图 3 Json 格式 500 错误页面

之前我们讲解过 Spring Boot 中统一进行异常处理的办法,也就是把页面的错误转换成了统一的 Json 格式数据返回给调用方,为什么这里还要用另一种办法来实现呢?

因为 @ControllerAdvice 注解主要用来针对 Controller 中的方法做处理,作用于 @RequestMapping 标注的方法上,只对我们定义的接口异常有效,在 Zuul 中是无效的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/82205.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

问题来了,拔掉网线几秒,再插回去,原本的 TCP 连接还存在吗?

今天&#xff0c;聊一个有趣的问题&#xff1a;拔掉网线几秒&#xff0c;再插回去&#xff0c;原本的 TCP 连接还存在吗&#xff1f; 可能有的同学会说&#xff0c;网线都被拔掉了&#xff0c;那说明物理层被断开了&#xff0c;那在上层的传输层理应也会断开&#xff0c;所以原…

MarkDown 项目中如何引入开源MarkDown? 史上最简单教程

目录 一、少不了的东西 editor.md ① 下载链接 ② 将其引入到自己的项目中 引入依赖 二、代码部分 一些小细节 1. 编辑页 2. 展示页 一、少不了的东西 如果想要在一个页面中使用MarkDown &#xff0c;那么你首先就要引入MarkDown editor.md ① 下载链接 GitHub下…

Flutter和Rust如何优雅的交互

前言 文章的图片链接都是在github上&#xff0c;可能需要...你懂得&#xff1b;本文含有大量关键步骤配置图片&#xff0c;强烈建议在合适环境下阅读 Flutter直接调用C层还是蛮有魅力&#xff0c;想想你练习C&#xff0c;然后直接能用flutter在上层展示出效果&#xff0c;是不…

【中级ECharts技术】transform进行数据转换和dataZoom在项目中的使用(可视化非常的强劲)

transform 进行数据转换 数据转换是这样一个公式:outData=f(inputData)。F是转换方法,例如filter、sort、region、boxplot、cluster、aggregate(todo)等。有了数据转换功能,我们至少可以做到以下几点: 将数据分成多个部分,并在不同的饼图中显示它们。 执行一些数据统计…

C++ 注释

&#x1f4d2;博客主页&#xff1a; ​​开心档博客主页​​ &#x1f389;欢迎关注&#x1f50e;点赞&#x1f44d;收藏⭐留言&#x1f4dd; &#x1f4cc;本文由开心档原创&#xff01; &#x1f4c6;51CTO首发时间&#xff1a;&#x1f334;2022年12月12日&#x1f334; ✉…

Httpd服务进阶知识-HTTP协议详解

一.WEB开发概述 1>.C/S编程 CS即客户端、服务器编程。 客户端、服务端之间需要使用Socket&#xff0c;约定协议、版本(往往使用的协议是TCP或者UDP)&#xff0c;指定地址和端口&#xff0c;就可以通信了。客户端、服务端传输数据&#xff0c;数据可以有一定的格式&#xff…

Go开发中配置一个Logger日志的功能实现(结合zap日志库)

为什么需要Logger 一般在开发项目的时候我们都是需要一个存储日志的文件&#xff0c;因为在部署项目以后&#xff0c;我们只能通过去筛查日志进行检索问题&#xff0c;这时候日志是否可以呈现清晰这个对于我们进行排查工作是十分重要的&#xff0c;所以Logger能否展示出我们最…

基于PHP的中华诗歌网的设计与实现

目 录 Abstract 2 目 录 3 1 绪论 5 1.1 研究背景 5 1.2诗歌鉴赏网站的意义 5 1.3网站开发的设计思想 5 2 系统相关技术 7 2.1 MySQL数据库介绍 7 2.2 PHP技术介绍 8 3 系统需求分析 10 3.1 系统需求分析 10 3.2系统可行性分析 10 3.3 系统用例分析 11 4 系统的详细设计 12 4.1…

QT 短时间大量图片传输,实现监控效果 (实时视屏传输) (暴力模式)

1.首先需要知道的知识 1.我使用的是 TCP 协议传输 &#xff0c;因为传输数据准确一点&#xff0c; 2. 然后是 套接字的 信号 , readyRead( ) ,这个信号的功能是只要套接字里面的 缓存区有数据&#xff0c;他就会发出这个信号。 3.我们传输数据有时候&#xff0c;会出现 粘…

[附源码]计算机毕业设计公共台账管理系统Springboot程序

项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis Maven Vue 等等组成&#xff0c;B/S模式 M…

基于intel+fpga平台实现设备AI 和数据转换智能化

通过 AI 和数据转换实现智能设备数字化 数据收集、可视化、自动控制和 AI 分析有效促进了设备智能化 &#xff08;M2I&#xff09; 和工业 AI的螺丝&#xff0c;是推动智能制造向前发展的两个齿轮。通过"设备智能化 "和"工业AI"赋能智能设备功能&#xff…

Kafka如何实现延迟队列?

Kafka并没有使⽤JDK⾃带的Timer或者DelayQueue来实现延迟的功能&#xff0c;⽽是基于时间轮自定义了⼀个⽤于实现延迟功能的定时器&#xff08;SystemTimer&#xff09;。JDK的Timer和DelayQueue插⼊和删除操作的平均时间复杂度为O(nlog(n))&#xff0c;并不能满⾜Kafka的⾼性能…

centOS配置ss5并解决部分出现的问题

文章目录代理服务器socks5协议搭建ss5代理服务器安装ss5修改配置文件添加用户名和密码修改ss5端口修改ss5启动权限启动ss5服务配置socks全局使用qq验证参考代理服务器 实际的工作的有时候需要用到代理服务器&#xff0c;通过代理服务器可以一定程度上隐藏自己的真实IP&#xf…

聚焦 | 千海金与大工科技园“喜结连理” 正式达成战略合作

为贯彻国家碳达峰碳中和相关工作要求&#xff0c;加快实施可再生能源替代行动&#xff0c;推动可再生能源加快步入高质量跃升发展新阶段。12月9日&#xff0c;由千海金集团、大连理工大学科技园联合成立的低碳科技与绿色金融研究中心举行揭牌仪式。 大连市西岗区科工贸副局长李…

nacos--基础--1.3--理论--架构

nacos–基础–1.3–理论–架构 1、基本架构及概念 1.1、服务 (Service) 是指一个或一组软件功能(例如特定信息的检索或一组操作的执行)&#xff0c;其目的是不同的客户端可以为不同的目的重用(例如通过跨进程的网络调用)。Nacos 支持主流的服务生态&#xff0c;举例如下 Kuber…

[2022-12-11]神经网络与深度学习 hw12 - 小作业

contentshw12 - 不知道该起个什么名字task1题目内容题目分析题目解答题目总结task2题目内容题目分析题目解答题目总结task3题目内容题目分析题目解答题目总结写在最后hw12 - 不知道该起个什么名字 task1 题目内容 在小批量梯度下降中&#xff0c;尝试分析为什么学习率要和批…

苦卷28天,P9大佬给我的Alibaba面试手册!终于成功踹开字节大门

怎么说呢&#xff0c;今年真的是寒气逼人啊&#xff01;在这个大环境下&#xff0c;裁员已经不算是特别的事情&#xff0c;粗暴裁员也许是未来一种趋势…在职的卷的起飞&#xff0c;离职的找不到好工作。 做点能做的&#xff1a;跑跑步骑骑车多锻炼&#xff1b;当当上面正版书…

[附源码]Python计算机毕业设计大学生心理健康管理系统Django(程序+LW)

该项目含有源码、文档、程序、数据库、配套开发软件、软件安装教程 项目运行 环境配置&#xff1a; Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术&#xff1a; django python Vue 等等组成&#xff0c;B/S模式 pychram管理等…

eNSP综合实验合集(eNSP综合大作业合集)_可先收藏

作者&#xff1a;BSXY_19计科_陈永跃BSXY_信息学院注&#xff1a;未经允许禁止转发任何内容**注&#xff1a;在该文章中就只对ensp综合实验做一个总结和归纳&#xff0c;只给出相应的topo图和需求说明和对应的文章的连接。有什么问题也可以私信我&#xff0c;看到都会回复的。文…

picoCTF 密码学方向RSA算法做题记录

RSA算法原理&#xff1a; https://blog.csdn.net/qq_45894840/article/details/128204460?spm1001.2014.3001.5502Mind your Ps and Qs 题目描述&#xff1a;In RSA, a small e value can be problematic, but what about N? Can you decrypt this? 下载题目 在这里可以看…