19、Zuul 功能原理

75 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第19天,点击查看活动详情

19、Zuul 功能原理

5、请求上下文

Http 请求的全部信息都封装在一个 RequestContext 对象中,该对象继承 ConcurrentHashMap。可将 RequestContext 看作一个 Map, RequestContext 维护着当前线程的全部请求变量,例如请求的URI, serviceId,主机等信息。 本小节将以RequestContext 为基础,编写一个自定义的过滤器,使用 RestTemplate 来调用集群服务。

新建一个过滤器。

/**
 * 请求上下文
 */
public class RestTemplateFilter extends ZuulFilter {

    private RestTemplate restTemplate;

    public RestTemplateFilter(RestTemplate restTemplate) {
        this.restTemplate = restTemplate;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        // 获取请求的 uri
        String uri = request.getRequestURI();
        // 为了不影响其它路由,url 中含有 rest-tpl-sale 才执行本路由器
        if (uri.indexOf("rest-tpl-sale") != -1) {
            return true;
        } else {
            return false;
        }
    }

    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        // 获取需要调用的服务Id
        String serviceId = (String) context.get("serviceId");
        // 获取请求的uri
        String uri = (String) context.get("requestURI");

        // 组装成 url 给 RestTemplate 调用
        String url = "http://" + serviceId + uri;
        System.out.println("执行RestTemplateFilter,调用的url:" + url);
        // 调用并获取结果
        String result = this.restTemplate.getForObject(url, String.class);
        // 设置路由状态,表示已经进行路由
        context.setResponseBody(result);
        // 设置响应标识
        context.sendZuulResponse();

        return null;
    }

    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 2;
    }
}

RestTemplateFilter 的主要功能是使用 RestTemplate 来调用集群服务。过滤器中的shouldFilter 方法从 RequestContext 中获取 HttpServletRequest ,再得到请求的 uri, 如果 uri含有 rest-tpI-sale 字符串,才执行本过滤器,这样做是为了避免影响其他例子的运行效果。

RestTemplateFilter 实现的 filterType 方法表示该过滤器将在 routing 阶段执行,执行顺序为2,也就是比 Spring Cloud 自带的过滤器( routing 阶段)都要优先执行。

在RestTemplateFilter 的执行方法中,从 RequestContext 中获取了 serviceId 以及请求的uri, 再组合成一个 url 给 RestTemplate 执行,执行返回的结果被设置到 RequestContext 中。

需要注意的是 ,最后调用了 RequestContext 的 sendZuulResponse 方法来设置响应标识。调用了该方法后, Spring Cloud 自带的 Ribbon 路由过滤器( RibbonRoutingFilter )、简路由过滤器( SimpleHostRoutingFilter )将不会执行

将 RestTemplateFilter 加入配置中。

@Bean
public RestTemplateFilter restTemplateFilter(RestTemplate restTemplate){
    return new RestTemplateFilter(restTemplate);
}

@LoadBalanced
@Bean
public RestTemplate getRestTemplate(){
    return new RestTemplate();
}

配置文件中,建立对应的路由规则:

#请求上下文 -- 自定义过滤器 
zuul.routes.restTestRoute.path=/rest-tpl-sale/**
zuul.routes.restTestRoute.service-id=invoker-server

以上配置片断,设置路由的 path 为 /rest-tpl-sale ,当访问该地址时,将会执行前面的RestTemplateFilter。 启动集群,访问以下地址 http://localhost:8080/rest-tpl-sale/hello 浏览器输出返回的字符串 Hello World -- invoker,控制台输出如下: 证明执行了自定义的RestTemplate 过滤器。

执行RestTemplateFilter,调用的url:http://invoker-server/hello

根据结果可知,我们自定义的过滤器将请求路由到集群的 invoker-server 服务。本例的作用,除了再次展示如何编写过滤器之外,主要还想让大家了解 RequestContext 所维护的相关信息。

6、@EnableZuulServer 注解

在本章前面的网关项目中 ,使用了 @EnableZuulProxy 来开启 Zuul 的功能。 除了该注解外,还可以使用 @EnableZuulServer ,该注解更像一个“低配版”的@EnableZuulProxy 。使 用 @EnableZuulServer 后, SimpleHostRoutingFilter,RibbonRoutingFilter 等过滤器将不会被启用。 下图展示了使用 @EnableZuulServer 注解后各阶段的过滤器。

s's'ssss

如图所示,使用@EnableZuulServer 后, pre 阶段的 PreDecorationFilter, routing 阶段的 RibbonRoutingFilter 和 SimpleHostRoutingFilter 将不会启用。换言之,默认情况下 Zuul 不具备调用集群服务的能力,也不具备简单路由的功能。 如果在实际项目中不希望使用 Spring Cloud 的 RibbonRoutingFilter和SimpleHostRoutingFilter, 而想像 7.6.4 节那样,自己编写过滤器来调用服务,可以考虑使用@EnableZuulServer 注解。

7、error 过滤器

各阶段的过滤器执行时,抛出的异常会被捕获,然后调用 RequestContext的 setThrowable 方法设置异常。 error 阶段的 SendErrorFilter 过滤器会判断 RequestContext 中是否存在异常( getThrowable 是否为 null ),如果存在,才会执行 SendErrorFilter 过滤器。

SendErrorFilter 过滤器在执行时,会将异常信息设置到 HttpServletRequest 中,再调用 RequestDispatcher 的 forward 方法,默认跳转到 /error 页面。代码清单 7-16 编写了一个自定义的过滤器,该过滤器会抛出异常。

/**
 * 测试 error 过滤器
 */
public class ExceptionFilter extends ZuulFilter {


    @Override
    public boolean shouldFilter() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        // 获取请求的uri
        String uri = request.getRequestURI();
        // 为了不影响其它例子,uri 含有 exceptionTest 才执行本过滤器
        if (uri.indexOf("exceptionTest") != -1) {
            return true;
        }
        return false;
    }

    @Override
    public Object run() {
        System.out.println("执行 ExceptionFileter ,抛出异常 ");
        throw new ZuulRuntimeException(new ZuulException("exception msg", 201, "my cause"));
    }

    @Override
    public String filterType() {
        return FilterConstants.ROUTE_TYPE;
    }

    @Override
    public int filterOrder() {
        return 3;
    }
}

配置
    @Bean
    public ExceptionFilter getExceptionFilter() {
        return new ExceptionFilter();
    }

在 ExceptionFilter 的 shouldFilter 方法中,遇到 exceptionTest 的 uri 才会执行,目的是不影响本章其他例子的执行。在run方法中,简单进行控制台打印,再抛出一个ZuulRuntimeException,该异常实例包装了一个 ZuulException 。为了查看异常输出的信息,新建一个控制器, 主要在控制台中输出这些信息。

@Controller
public class MyErrorController extends BasicErrorController {
    public MyErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes, new ErrorProperties());
    }

    @Override
    public ModelAndView errorHtml(HttpServletRequest request,
                                  HttpServletResponse response) {
        System.out.println("=========== 输出异常信息 =========");

        System.out.println(request.getAttribute("javax.servlet.error.status_code"));
        System.out.println(request.getAttribute("javax.servlet.error.exception"));
        System.out.println(request.getAttribute("javax.servlet.error.message"));

        return super.errorHtml(request,response);

    }
}

**MyErrorController 继承了 BasicErrorController, BasicErrorController 是 Spring Boot 中用于处理错误的控制器基类。在过滤器抛出异常后, SendErrorFilter 会跳转到 error 路径,然后执行 MyErrorController的 errorHtml 方法返回到错误页面。意味着跳转到error路径就会使用 MyErrorController 控制器进行处理 ** 在本例中我们不进行处理,只在方法体中输出此处得到的异常信息。启动整个集群,访问以下地址,http://localhost:8080/exceptionTest/test ,可以看到网关项目的控制台输出如下:

需要配置路径: 不然的话,访问该路径 404 ,http://localhost:8080/exceptionTest/test 个人认为 zuul 没有解析到 该路径,直接抛出异常,使用 MyErrorController 进行处理。 自定义的ExceptionFilter 不会执行,因为没到这一步,就已经抛出异常了。

# error过滤器 -- 自定义过滤器 抛异常测试 error 过滤器
zuul.routes.exceptionTest.path=/exceptionTest/**
zuul.routes.exceptionTest.service-id=invoker-server
执行 ExceptionFileter ,抛出异常 
2019-01-09 20:18:42.374  WARN 13076 --- [nio-8080-exec-1] o.s.c.n.z.filters.post.SendErrorFilter   : Error during filtering
省略异常信息

=========== 输出异常信息 =========
201
com.netflix.zuul.exception.ZuulException: exception msg
my cause

根据输出结果可知,过滤器抛出的异常信息可以在错误处理的控制器中获取。

SpringBoot-ErrorController 详细见下面连接: blog.csdn.net/qianyiyidin…

8、动态路由

在前面章节中,所有的路由规则都在 application. yml 中进行配置,在实际应用中,可 能一个模块就有一份路由配置文件,而且这些配置文件的 内容都在不停变化。如果因为部 分变化而重启网关,这是无法想象的。因此,路由规则的动态刷新功能在实际应用中非常 重要。 路由的动态刷新需要以配置文件的更新、配置项的刷新为前提,这部分内容将在 Spring Cloud Config 章节中讲解,因此动态路由的实现,也在那一章中讲解,本章不进行讲述。