zoukankan      html  css  js  c++  java
  • 白话SpringCloud | 第十章:路由网关(Zuul)进阶:过滤器、异常处理

    前言

    简单介绍了关于Zuul的一些简单使用以及一些路由规则的简单说明。而对于一个统一网关而言,需要处理各种各类的请求,对不同的url进行拦截,或者对调用服务的异常进行二次处理等等。今天,我们就来了解下这方面的相关知识点。

    一点知识

    开始实践前,我们先来了解下Zuul默认的过滤器(注意,这里讲解的Zuul都是1.X版本的)。上一章节,也提到了Zuul的核心就是一系列过滤器。现在我们来看看Zuul的过滤器相关信息。

    过滤器的定义

    Zuul中定义了四种标准过滤器类型,这些过滤器类型对应于请求的典型生命周期。

    • PRE:可以在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
    • ROUTING:在路由请求时候被调用。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClientNetfilx Ribbon请求微服务。
    • POST:在routingerror过滤器之后被调用。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
    • ERROR:处理请求时发生错误时被调用。

    现在看下官网wiki提供的四种过滤器的生命周期图。

    zuul请求生命周期

    一个请求会先按顺序通过所有的前置过滤器,之后在路由过滤器中转发给后端应用,得到响应后又会通过所有的后置过滤器,最后响应给客户端。在整个流程中如果发生了异常则会跳转到错误过滤器中。

    一般来说,如果需要在请求到达后端应用前就进行处理的话,会选择pre(前置过滤器),例如鉴权、请求转发、增加请求参数等行为。在请求完成后需要处理的操作放在(post)后置过滤器中完成,例如统计返回值和调用时间、记录日志、增加跨域头等行为。路由过滤器一般只需要选择 Zuul 中内置的即可,错误过滤器一般只需要一个,这样可以在遇到错误逻辑时直接抛出异常中断流程,并直接统一处理返回结果

    说下error过滤器:prerouting的任意一个阶段如果抛异常了,则执行error过滤器,然后再执行post给出响应。而post异常了,就直接调用error了。

    过滤器接口定义

    知道了过滤器的定义,我们看看过滤器是怎么被定义的。查看类com.netflix.zuul.ZuulFilter类,可知其个抽象类:
    以下为需要实现的方法,其他具体的可自行查阅下

    //过滤器类型
    String filterType();
    //执行顺序 越小越先执行
    int  filterOrder();
    //是否执行 返回false 不执行此过滤器
    boolean  shouldFilter();
    //过滤器执行逻辑
    Object run();
    
    

    具体说明下:

    • filterType:该函数需要返回一个字符串来代表过滤器的类型,而这个类型就是在HTTP请求过程中定义的各个阶段。在Zuul中默认定义了四种不同生命周期的过滤器类型,具体如下:
      • pre:可以在请求被路由之前调用。
      • routing:在路由请求时候被调用。
      • post:在routing和error过滤器之后被调用。
      • error:处理请求时发生错误时被调用。
    • filterOrder:通过int值来定义过滤器的执行顺序,数值越小优先级越高。
    • shouldFilter:返回一个boolean类型来判断该过滤器是否要执行。我们可以通过此方法来指定过滤器的有效范围。
    • run:过滤器的具体逻辑。在该函数中,我们可以实现自定义的过滤逻辑,来确定是否要拦截当前的请求,不对其进行后续的路由,或是在请求路由返回结果之后,对处理结果做一些加工等。

    所以,了解了过滤器抽象类的定义,自定义抽象类就简单了。

    zuul自带过滤器

    通过IDE我们来看下已经实现ZuulFilter的过滤器类。具体的类在:

    看看已经提供的过滤器:

    自带的filter

    可以看见,Spring cloud zuul提供了很多过滤器,基本上就开箱即用了。简单说明下:

    类型 顺序 过滤器 功能
    pre -3 ServletDetectionFilter 标记处理Servlet的类型
    pre -2 Servlet30WrapperFilter 包装HttpServletRequest请求
    pre -1 FormBodyWrapperFilter 包装请求体
    pre 1 DebugFilter 标记调试标志
    pre 5 PreDecorationFilter 处理请求上下文供后续使用
    route 10 RibbonRoutingFilter serviceId请求转发
    route 100 SimpleHostRoutingFilter url请求转发
    route 500 SendForwardFilter forward请求转发
    error 0 SendErrorFilter 处理有错误的请求响应
    post 1000 SendResponseFilter 处理正常的请求响应

    禁用过滤器

    组件实现的过滤器,满足执行条件时都是会执行的,若我们想禁用某个过滤器时,可以在配置文件中配置。
    规则:zuul.<SimpleClassName>.<filterType>.disable=true
    说明:SimpleClassName类名filterType过滤器类型

    #禁用DebugFilter过滤器
    zuul.DebugFilter.pre.disable=true
    

    Zuul进阶示例

    为了区分不混淆,创建一个新的项目进行示例:spring-cloud-zuul-advanced
    对于通用部分,如pom依赖等都是和项目spring-cloud-zuul一样的,不一样的会具体指出的。大家可查看《第九章:路由网关(Zuul)的使用》,这里就不重复贴了。

    自定义filter

    通过以上几个小节的说明,我们通过继承ZuulFilter类进行自定义过滤器的编写。这里直接校验请求的参数是否带有token,若无此参数时,直接进行请求拦截。

    /**
     * 自定义过滤器-校验请求参数是否合法:包含token参数
     * @author oKong
     *
     */
    @Slf4j
    public class AccessZuulFilter extends ZuulFilter{
    
        @Override
        public boolean shouldFilter() {
            //此方法可以根据请求的url进行判断是否需要拦截
            return true;
        }
    
        @Override
        public Object run() throws ZuulException {
            //获取请求的上下文类 注意是:com.netflix.zuul.context包下的
            RequestContext ctx = RequestContext.getCurrentContext();
            //获取request对象
            HttpServletRequest request = ctx.getRequest();
            //避免中文乱码
            ctx.addZuulResponseHeader("Content-type", "text/json;charset=UTF-8");
            ctx.getResponse().setCharacterEncoding("UTF-8");
            //打印日志
            log.info("请求方式:{},地址:{}", request.getMethod(),request.getRequestURI());
            String token = request.getParameter("token");
            if(StringUtils.isBlank(token)) {
                //使其不进行转发 自定义route类型时,在shouldFilter中也需要进行此参数判断。
               ctx.setSendZuulResponse(false);
               ctx.setResponseBody("{"code":"999500","msg":"非法访问"}");
               ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());//401
               //或者添加一个额外参数也可以 传递参数可以使用
    //           ctx.set("checkAuth",false);
            }
            //这返回值没啥用 
            return null;
        }
    
        @Override
        public String filterType() {
            //前置过滤器
            return PRE_TYPE;
        }
    
        @Override
        public int filterOrder() {
            //执行顺序  0 靠前执行 在spring cloud zuul提供的pre过滤器之后执行,默认的是小于0的。    
            //除了参数校验类的过滤器 一般上直接放在 PreDecoration前
            //即:PRE_DECORATION_FILTER_ORDER - 1;
            //常量类都在:org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 下
            return 0;
        }
    
    }
    

    同时在启动类中使用@Bean标记,使其生效。

        @Bean
        public AccessZuulFilter accessZuulFilter() {
            return new AccessZuulFilter();
        }
    

    注意:Spring cloud为我们提供了常量类:org.springframework.cloud.netflix.zuul.filters.support.FilterConstants静态引入对于的常量即可。里面包含了各过滤器的执行顺序值、过滤器类型常量以及一些头部参数或者变量参数名:请求服务ID请求URI等。这些参数都是很有用的,比如请求服务ID,若为空,则直接使用SimpleHostRoutingFilter进行请求转发,否则是RibbonRoutingFilter进行服务转发。这些变量都是通过PreDecorationFilter前置过滤器进行赋值处理的。

    启动应用,访问:http://127.0.0.1:8889/myapi/hello?name=oKong 可以看见,请求被拦截了,返回了非法访问提示。

    未带token

    接着,我们请求参数带上tokenhttp://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,可以看见请求被正常转发了。

    异常处理

    从目前的文件中,我们可以知晓:目前可以通过serviceIdurl进行请求转发,根据PreDecorationFilter前置过滤器鉴别不同的类型,最后通过ribbon或者常规的http访问目标服务。在访问目标服务,发生异常是在正常不过的了。从第一小节我们可以获悉,当过滤器发生异常时,会调用error过滤器进行异常信息处理,默认情况下就是:SendErrorFilter。首先,我们看看,默认情况下,以上两种异常是如何进行异常信息展现的。

    首先,我们spring-cloud-eureka-client服务停止了,之后访问下:http://127.0.0.1:8889/eureka/hello?name=oKong&token=okong ,可以看见返回的就是正常boot默认异常,即:/error页面。

    error

    接着,访问下:http://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,相同的都是跳转至/error页面。

    error

    可以发现,第二种错误信息更加直观也更有用,可以获悉是服务不可用造成的。

    现在,我们来看看,SendErrorFilter类的run方法。

    sendErrorFilter

    可以获悉,其主要的生效条件是包含异常对象:throwable ,而第二个条件只是为了避免二次执行。为了了解下其调用关系,我们查看下com.netflix.zuul.http.ZuulServlet类的service方法,这个类它定义了Zuul处理外部请求过程时,各个类型过滤器的执行逻辑。

    ZuulServlet类service方法

    以上截图了此类的service方法,可以看见,每调用一个过滤器类型时,外部都是用try..catch包裹了,异常发生时都调用了error方法,现在我们看看error()方法。

    error方法

    可以看见,当一个触发器发生异常时,统一设置了异常对象throwable,而后去调用error类型的过滤器。

    针对网关自己的api接口时,和普通的web应用是一样的了。也是跳转至/error上,此时可以使用@ControllerAdvice进行统一异常处理。关于统一异常的处理,可以查看《SpringBoot | 第八章:统一异常、数据校验处理》,这里就不阐述了。

    服务异常回退

    通过前一章节,我们值得可以通过注册中心的服务ID进行自动转发,当远程服务不可用时,我们可以通过Hystrix进行服务回退处理。官网文档也说明了,只需实现FallbackProvider接口类即可。

    Providing Hystrix Fallbacks For Routes

    创建一个服务eureka-client的异常回退类:myEurekaClientFallback

    /**
     * 服务 eureka-client 的异常退回处理类
     * @author oKong
     
     */
    public class MyEurekaClientFallback implements FallbackProvider {
    
        @Override
        public String getRoute() {
            // TODO Auto-generated method stub
            return "eureka-client";
        }
    
        @Override
        public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
            //标记不同的异常为不同的http状态值
            if (cause instanceof HystrixTimeoutException) {
                return response(HttpStatus.GATEWAY_TIMEOUT);
            } else {
                //可继续添加自定义异常类
                return response(HttpStatus.INTERNAL_SERVER_ERROR);
            }
        }
        //处理
        private ClientHttpResponse response(final HttpStatus status) {
            return new ClientHttpResponse() {
                @Override
                public HttpStatus getStatusCode() throws IOException {
                    return status;
                }
    
                @Override
                public int getRawStatusCode() throws IOException {
                    return status.value();
                }
    
                @Override
                public String getStatusText() throws IOException {
                    return status.getReasonPhrase();
                }
    
                @Override
                public void close() {
                }
    
                @Override
                public InputStream getBody() throws IOException {
                    //可替换成相应的json串的 看业务规定了
                    return new ByteArrayInputStream("{"code":"999999","msg":"服务暂时不可用"}".getBytes());
                }
    
                @Override
                public HttpHeaders getHeaders() {
                    HttpHeaders headers = new HttpHeaders();
                    headers.setContentType(MediaType.APPLICATION_JSON);
                    return headers;
                }
            };
        }
    }
    

    同时在启动类中使用@Bean标记,使其生效。

        @Bean
        public MyEurekaClientFallback eurekaClientFallback() {
            return new MyEurekaClientFallback();
        }
    

    此时,我们停止spring-cloud-eureka-client服务,访问:http://127.0.0.1:8889/eureka/hello?name=oKong&token=okong ,可以看见看见已经正确返回错误信息了。

    服务回退

    另外,需要细化异常的,可对fallbackResponseThrowable进行异常判断的,以获取具体的异常信息,如超时、处理异常等等。而且,设置了服务回退,此时对于route过滤器而言是正常调用,未发生异常,所以也就不会调用error过滤器了。

    常规http请求异常

    当使用Ribbon进行服务调用时,我们可以使用FallbackProvider进行调用,而当我们常规的使用url进行转发时,我们也应该进行异常结果处理,以保持返回值一致。已经知道,发生异常时,会调用SendErrorFilter异常过滤器,对异常经常处理,同时重定向至/error中,所以,一般上我们可以自定义ErrorController类或者参照SendErrorFilter进行二次开发,对返回值进行个性化处理即可。这里简单演示下通过自定义异常过滤器进行异常处理。

    /**
     * 自定义异常类 过滤器 直接扩展 SendErrorFilter 类
     * @author oKong
     *
     */
    @Slf4j
    public class CustomErrorFilter extends SendErrorFilter{
    
        @Override
        public Object run() {
            //重写 run方法        
            try{
                RequestContext ctx = RequestContext.getCurrentContext();
                //直接复用异常处理类
                ExceptionHolder exception = findZuulException(ctx.getThrowable());
                log.info("异常信息:{}", exception.getThrowable());
                //这里可对不同异常返回不同的错误码
                HttpServletResponse response = ctx.getResponse();
                response.getOutputStream().write(("{"code":"999999","msg":"" + exception.getErrorCause() + ""}").getBytes());
                     
            }catch (Exception ex) {
                ReflectionUtils.rethrowRuntimeException(ex);
            }
            return null;
        }
    
    }
    

    同时,禁用SendErrorFilter过滤器。

    ## 停用默认的异常处理器SendErrorFilter
    zuul.SendErrorFilter.error.disable=true
    

    在启动类,使用@Bean生效自定义过滤器。

        @Bean
        @ConditionalOnProperty(name="zuul.SendErrorFilter.error.disable")
        public CustomErrorFilter customErrorFilter() {
            return new CustomErrorFilter();
        }
    

    重启应用,访问:http://127.0.0.1:8889/myapi/hello?name=oKong&token=okong ,可以看见已经是按自定义返回值返回了。

    error

    另外注意的是,前面也有提到,当访问不存在的路径或者转发路径时,依旧是普通的异常,可通过统一异常进行拦截,返回值拼装的。

    参考资料

    1. https://cloud.spring.io/spring-cloud-static/Finchley.SR1/single/spring-cloud.html#_router_and_filter_zuul

    2. https://github.com/Netflix/zuul/wiki/How-it-Works

    总结

    本章节主要介绍了关于Zuul过滤器和相关异常处理的相关知识点。可能还是存在不完整的情况,大家在遇见相关问题时,可查阅下官方文档的。Zuul本身还有一些其他的高级功能的,本人也用的不多,相关配置也是看了官方文档时才知道如何配置和使用的。所以,不知道相关配置时,可以去查阅下相关文档,比如一些忽略头部信息、忽略服务等等配置,都未涉及。主要还是用的不多。。原来我们都是自建一个restful服务进行统一网关调用的,当频繁修改api时此方法就有点麻烦需要多次变动了。主要看业务需求吧,这东西可大可小的。最简单当然创建个简单的web就行了,而当需要实现一些高级功能,比如灰度发布,动态引流时可能就需要考虑下使用Zuul或者gateway。有时间去看看gateway,据说性能好呀。关于网关的暂时就告一段落了,接下来会分享一些服务之间调用异常处理的,敬请期待~

    最后

    目前互联网上大佬都有分享SpringCloud系列教程,内容可能会类似,望多多包涵了。原创不易,码字不易,还希望大家多多支持。若文中有错误之处,还望提出,谢谢。

    老生常谈

    • 个人QQ:499452441
    • 微信公众号:lqdevOps

    公众号

    个人博客:http://blog.lqdev.cn

    源码示例:https://github.com/xie19900123/spring-cloud-learning

    原文地址:http://blog.lqdev.cn/2018/10/17/SpringCloud/chapter-ten/

  • 相关阅读:
    [日常] 使用TCPDUMP和Ethereal抓包分析HTTP请求中的异常情况
    [视频教程] 基于redis的消息队列实现与思考
    [日常] 项目中业务绑定手机验证手机号的实现
    [视频教程] 使用docker的方式安装redis
    [前端] js中call方法的理解和思考
    [日常] Redis中set集合的使用思考
    [日常] 跨语言的POST请求问题的解决
    [PHP] 基于redis的分布式锁防止高并发重复请求
    [日常] 安装windows+deepin双系统
    [视频教程]利用SSH隧道进行远程腾讯云服务器项目xdebug调试
  • 原文地址:https://www.cnblogs.com/okong/p/springcloud-ten.html
Copyright © 2011-2022 走看看