Spring Cloud Zuul 精进

Posted rickiyang

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Cloud Zuul 精进相关的知识,希望对你有一定的参考价值。

接着上一篇继续讲Zuul,上一篇搭建了Zuul的环境简单说明了怎么使用,本篇查缺补漏将一些常用的配置贴出来。文末我们会一起分析一下Zuul的源码,升华一下本篇的格调!

忽略所有微服务或某些微服务

默认情况下,只要引入了zuul后,就会自动一个默认的路由配置,但有些时候我们可能不想要默认的路由配置规则,想自己进行定义,忽略所有微服务:(后面写 * ):

zuul:  
  ignored-services: "*"  

忽略某些微服务:(直接写微服务的名字=>可以理解为spring.application.name的值,多个以都好分隔)

zuul:  
  ignored-services: product-provider,product-consumer-8201  

忽略所有为服务,只路由指定的微服务

zuul:
  # 排除所有的服务,只由指定的服务进行路由
  ignored-services: "*"
  routes:
    eureka-client:
      path: /client1/**
      serviceId: eureka-client

通过path和url访问到具体的某台机器上

有时候我们测试的时候需要访问到具体的某台机器上,而不希望负载均衡到别的机器上或者需要访问到第三方的某台机器上:

zuul:  
  routes:  
    product-provider:  
      path: /product/**  
      url: http://localhost:8202/  

注意:

  1. product-provider 这个值可以随便写,即使是一个不存在的值;
  2. 这种方式访问不会作为 HystrixCommand 来进行访问;
  3. url 里面也不可以写多个url

敏感头的传递(比如Cookie等)全局设置和某个微服务设置

有些时候我们微服务上游可能想传递一些请求头到下游的服务,比如Token、Cookie等值,默认情况下,zuul 不会将 Cookie,Set-Cookie,Authorization 这三个头传递到下游服务,如果需要传递,就需要忽略这些敏感头。

zuul:
  #所有服务路径前统一加上前缀
  prefix: /api
  # 排除某些路由, 支持正则表达式
  ignored-patterns:
    - /**/modify/pwd
  # 排除服务
  ignored-services: user-center
  routes:
    eureka-client:
      path: /client1/**
      serviceId: eureka-client
      sensitiveHeaders:  #当前这个路由的header为空
  sensitiveHeaders: Cookie,Set-cookie #全局路由都带这些header

Zuul 源码浅析

开启Zuul很简单,在启动类上添加Zuul 开启注解:

@EnableZuulProxy
/**
 * Sets up a Zuul server endpoint and installs some reverse proxy filters in it, so it can
 * forward requests to backend servers. The backends can be registered manually through
 * configuration or via DiscoveryClient.
 *
 * @see EnableZuulServer for how to get a Zuul server without any proxying
 *
 * @author Spencer Gibb
 * @author Dave Syer
 * @author Biju Kunjummen
 */
@EnableCircuitBreaker
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ZuulProxyMarkerConfiguration.class)
public @interface EnableZuulProxy {
}

上面的注释上有一句话:EnableZuulServer 是不使用代理功能来获取Zuul server。开启Zuul 网关有两种方式:

  • @EnableZuulServer : 普通网关,只支持基本的route与filter;
  • @EnableZuulProxy :配合上服务发现与熔断开关,是 EnableZuulServer 的增强版,具有反向代理功能。

简单来说,@EnableZuulProxy可理解为@EnableZuulServer的增强版,当Zuul与Eureka、Ribbon等组件配合使用时,我们使用@EnableZuulProxy。

接着看 EnableZuulProxy,在类头引用了ZuulProxyMarkerConfiguration,ZuulProxyAutoConfiguration 的作用是开启 ZuulProxyAutoConfiguration的标记。

ZuulProxyAutoConfiguration 继承了 ZuulServerAutoConfiguration,是 ZuulServerAutoConfiguration 的超集。该类注入了DiscoveryClient、RibbonCommandFactoryConfiguration用作负载均衡相关的。注入了一些列的filters,比如PreDecorationFilter、RibbonRoutingFilter、SimpleHostRoutingFilter。

ZuulServerAutoConfiguration更为重要:

@Configuration
@EnableConfigurationProperties({ ZuulProperties.class })
@ConditionalOnClass({ZuulServlet.class, ZuulServletFilter.class})
@ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class)
// Make sure to get the ServerProperties from the same place as a normal web app would
// FIXME @Import(ServerPropertiesAutoConfiguration.class)
public class ZuulServerAutoConfiguration {

	@Autowired
	protected ZuulProperties zuulProperties;

	@Autowired
	protected ServerProperties server;

	@Autowired(required = false)
	private ErrorController errorController;

	private Map<String, CorsConfiguration> corsConfigurations;

	@Autowired(required = false)
	private List<WebMvcConfigurer> configurers = emptyList();

	@Bean
	public HasFeatures zuulFeature() {
		return HasFeatures.namedFeature("Zuul (Simple)", ZuulServerAutoConfiguration.class);
	}

	@Bean
	@Primary
	public CompositeRouteLocator primaryRouteLocator(
			Collection<RouteLocator> routeLocators) {
		return new CompositeRouteLocator(routeLocators);
	}

 /**
  * 路由定位器
  *
  */
	@Bean
	@ConditionalOnMissingBean(SimpleRouteLocator.class)
	public SimpleRouteLocator simpleRouteLocator() {
		return new SimpleRouteLocator(this.server.getServlet().getContextPath(),
				this.zuulProperties);
	}

  /**
  * Zuul创建的一个Controller,用于将请求交由ZuulServlet处理
  *
  */
	@Bean
	public ZuulController zuulController() {
		return new ZuulController();
	}

  /**
  * 会添加到SpringMvc的HandlerMapping链中,
  *只有选择了ZuulHandlerMapping的请求才能出发到Zuul的后续流程
  *
  */
	@Bean
	public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) {
		ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController());
		mapping.setErrorController(this.errorController);
		mapping.setCorsConfigurations(getCorsConfigurations());
		return mapping;
	}

/**
  * ZuulServlet是整个流程的核心
  *
  *
  */
	@Bean
	@ConditionalOnMissingBean(name = "zuulServlet")
	@ConditionalOnProperty(name = "zuul.use-filter", havingValue = "false", matchIfMissing = true)
	public ServletRegistrationBean zuulServlet() {
		ServletRegistrationBean<ZuulServlet> servlet = new ServletRegistrationBean<>(new ZuulServlet(),
				this.zuulProperties.getServletPattern());
		// The whole point of exposing this servlet is to provide a route that doesn\'t
		// buffer requests.
		servlet.addInitParameter("buffer-requests", "false");
		return servlet;
	}

	
	......
    ......
    ......
    
}

在 ZuulServerAutoConfiguration 中定义了几个核心对象:

  • ZuulController:所有 路由的默认Controller;
  • ZuulHandlerMapping:Zuul 路由Mapping映射器;
  • ZuulServlet:继承自HttpServlet,过滤逻辑从这里始,到这里终。

ZuulServlet是整个流程的核心,大致的请求过程如下:

当Zuulservlet收到请求后, 会创建一个ZuulRunner对象,该对象中初始化了RequestContext:存储请求的ServletRequest 和 ServletResponse对象,并被当前请求链上的所有Zuulfilter共享;

ZuulRunner中还有一个 FilterProcessor,FilterProcessor作为执行所有的Zuulfilter的管理器;

FilterProcessor从filterloader 中获取zuulfilter,而zuulfilter是被filterFileManager所加载,并支持groovy热加载,采用了轮询的方式热加载;

有了这些filter之后,zuulservelet首先执行的Pre类型的过滤器,再执行route类型的过滤器, 最后执行的是post 类型的过滤器,如果在执行这些过滤器有错误的时候则会执行error类型的过滤器;

执行完这些过滤器,最终将请求的结果返回给客户端。

RequestContext就是会一直跟着整个请求周期的上下文对象,filters之间有什么信息需要传递就set一些值进去就行了。

ZuulServlet 掌控所有url的流转,我们先看它做了什么工作:

public class ZuulServlet extends HttpServlet {

    private static final long serialVersionUID = -3374242278843351500L;
    private ZuulRunner zuulRunner;


    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);

        String bufferReqsStr = config.getInitParameter("buffer-requests");
        boolean bufferReqs = bufferReqsStr != null && bufferReqsStr.equals("true") ? true : false;

        zuulRunner = new ZuulRunner(bufferReqs);
    }

    @Override
    public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
        try {
            init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

            // Marks this request as having passed through the "Zuul engine", as opposed to servlets
            // explicitly bound in web.xml, for which requests will not have the same data attached
            RequestContext context = RequestContext.getCurrentContext();
            context.setZuulEngineRan();

            try {
                preRoute();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                route();
            } catch (ZuulException e) {
                error(e);
                postRoute();
                return;
            }
            try {
                postRoute();
            } catch (ZuulException e) {
                error(e);
                return;
            }

        } catch (Throwable e) {
            error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
        } finally {
            RequestContext.getCurrentContext().unset();
        }
    }

    /**
     * executes "post" ZuulFilters
     *
     * @throws ZuulException
     */
    void postRoute() throws ZuulException {
        zuulRunner.postRoute();
    }

    /**
     * executes "route" filters
     *
     * @throws ZuulException
     */
    void route() throws ZuulException {
        zuulRunner.route();
    }

    /**
     * executes "pre" filters
     *
     * @throws ZuulException
     */
    void preRoute() throws ZuulException {
        zuulRunner.preRoute();
    }

    /**
     * initializes request
     *
     * @param servletRequest
     * @param servletResponse
     */
    void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {
        zuulRunner.init(servletRequest, servletResponse);
    }

    /**
     * sets error context info and executes "error" filters
     *
     * @param e
     */
    void error(ZuulException e) {
        RequestContext.getCurrentContext().setThrowable(e);
        zuulRunner.error();
    }

    @RunWith(MockitoJUnitRunner.class)
    public static class UnitTest {

        @Mock
        HttpServletRequest servletRequest;
        @Mock
        HttpServletResponseWrapper servletResponse;
        @Mock
        FilterProcessor processor;
        @Mock
        PrintWriter writer;

        @Before
        public void before() {
            MockitoAnnotations.initMocks(this);
        }

        @Test
        public void testProcessZuulFilter() {

            ZuulServlet zuulServlet = new ZuulServlet();
            zuulServlet = spy(zuulServlet);
            RequestContext context = spy(RequestContext.getCurrentContext());


            try {
                FilterProcessor.setProcessor(processor);
                RequestContext.testSetCurrentContext(context);
                when(servletResponse.getWriter()).thenReturn(writer);

                zuulServlet.init(servletRequest, servletResponse);
                verify(zuulServlet, times(1)).init(servletRequest, servletResponse);
                assertTrue(RequestContext.getCurrentContext().getRequest() instanceof HttpServletRequestWrapper);
                assertTrue(RequestContext.getCurrentContext().getResponse() instanceof HttpServletResponseWrapper);

                zuulServlet.preRoute();
                verify(processor, times(1)).preRoute();

                zuulServlet.postRoute();
                verify(processor, times(1)).postRoute();
//                verify(context, times(1)).unset();

                zuulServlet.route();
                verify(processor, times(1)).route();
                RequestContext.testSetCurrentContext(null);

            } catch (Exception e) {
                e.printStackTrace();
            }


        }
    }

}

ZuulServlet 继承了HttpServlet,主要的作用就是对HTTP请求进行拦截做对应的处理。直接看实现方法 service()中的实现:

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException {
  try {
    init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);

    //这里从RequestContext中取出当前线程中封装好的对象然后在该对象上打上zuul处理的标记
    RequestContext context = RequestContext.getCurrentContext();
    context.setZuulEngineRan();

    try {
      preRoute();
    } catch (ZuulException e) {
      error(e);
      postRoute();
      return;
    }
    try {
      route();
    } catch (ZuulException e) {
      error(e);
      postRoute();
      return;
    }
    try {
      postRoute();
    } catch (ZuulException e) {
      error(e);
      return;
    }

  } catch (Throwable e) {
    error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName()));
  } finally {
    RequestContext.getCurrentContext().unset();
  }
}


第一句init方法:

public void init(HttpServletRequest servletRequest, HttpServletResponse servletResponse) {

  RequestContext ctx = RequestContext.getCurrentContext();
  if (bufferRequests) {
    ctx.setRequest(new HttpServletRequestWrapper(servletRequest));
  } else {
    ctx.setRequest(servletRequest);
  }

  ctx.setResponse(new HttpServletResponseWrapper(servletResponse));
}

在这里调用了一个RequestContext类将HttpServletRequest保存进去,而 RequestContext 类本身也比较特殊:

public class RequestContext extends ConcurrentHashMap<String, Object> {

  rotected static final ThreadLocal<? extends RequestContext> threadLocal = new ThreadLocal<RequestContext>() {
    @Override
    protected RequestContext initialValue() {
      try {
        return contextClass.newInstance();
      } catch (Throwable e) {
        throw new RuntimeException(e);
      }
    }
  };
  
  public static RequestContext getCurrentContext() {
    if (testContext != null) return testContext;

    RequestContext context = threadLocal.get();
    return context;
  }
  
}

它本身就是一个Map,需要注意的是,该对象的使用方式并不是直接new 一个新对象,而是调用getCurrentContext()方法,该方法中返回的是 threadLocal 封装的反射生成new RequestContext的方式来创建对象。确保每个创建的 RequestContext 只在当前线程内有效,即在当前线程内,getCurrentContext()方法取出的是同一个 RequestContext对象。

继续回到service()方法,拿到了封装好了的RequestContext方法之后,下面进入四个route中,上节已经讲过这4个route都属于Filter的生命周期,在这里完成请求的过滤,转发,后置逻辑处理。route完成之后,最后的finally方法中调用了RequestContext.getCurrentContext().unset()方法,既然使用了threadLocal,必然使用完要清除,不然很可能就内存泄漏。

小憩一会:

分析到 ZuulServlet,不知你是否发现Zuul的核心。对于Zuul实现网关的功能其实就是围绕着HttpServlet拿到ServletRequest,对请求做过滤操作,拿到ServletResponse 对返回结果做后置处理操作。HttpServlet是单实例多线程的处理模型,如果存在某一个请求比较耗时,那么该线程就会一直阻塞直到处理完成返回成功才结束。假若这样的请求很多,对Zuul所在的服务器压力还是不小。

Zuul如何处理一个请求

上面已经分析得出Zuul是基于Servlet这一套逻辑来做的,往下跟就变得简单。SpringMVC是如何处理请求的呢?大家应该都比较熟悉,浏览器发出一个请求到达服务端,首先到达DispatcherServlet,Servlet容器将请求交给HandlerMapping,找到对应的Controller访问路径和处理方法对应关系,接着交由HandlerAdapter路由到真实的处理逻辑中去进行处理。

上面我贴出来 ZuulServerAutoConfiguration#ZuulHandlerMapping,定义了ZuulHandlerMapping bean对象。

public class ZuulHandlerMapping extends AbstractUrlHandlerMapping {

}

ZuulHandlerMapping 自身继承了AbstractUrlHandlerMapping,即通过url来查找对应的处理器。判断的核心逻辑在 lookupHandler方法中:

@Override
protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
  if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) {
    return null;
  }
  //判断urlPath是否被忽略,如果忽略则返回null
  if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null;
  RequestContext ctx = RequestContext.getCurrentContext();
  if (ctx.containsKey("forward.to")) {
    return null;
  }
  if (this.dirty) {
    synchronized (this) {
      if (this.dirty) {
        //如果没有加载过路由或者路由有刷新,则加载路由
        registerHandlers();
        this.dirty = false;
      }
    }
  }
  return super.lookupHandler(urlPath, request);
}


private void registerHandlers() {
  Collection<Route> routes = this.routeLocator.getRoutes();
  if (routes.isEmpty()) {
    this.logger.warn("No routes found from RouteLocator");
  }
  else {
    for (Route route : routes) {
      //调用父类,注册处理器,这里所有路径的处理器都是ZuulController
      registerHandler(route.getFullPath(), this.zuul);
    }
  }
}

整体逻辑就是在路由加载的时候需要为每个路由指定处理器,因为Zuul不负责逻辑处理,所以它也没有对应的Controller可以使用,那怎么办呢,注册处理器的时候,使用的是ZuulController,是Controller的子类,对应的适配器是SimpleControllerHandlerAdapter,也就说每一个路由规则公共处理器都是ZuulController,这个处理器最终会调用ZuulServlet经过zuul定义的和自定义的拦截器。

上面还有一句:

Collection<Route> routes = this.routeLocator.getRoutes();

RouteLocator的作用是路由定位器,先看它有哪些实现类:

  • SimpleRouteLocator:主要加载配置文件的路由规则;
  • DiscoveryClientRouteLocator:服务发现的路由定位器,去注册中心如Eureka,Consul等拿到服务名称,以这样的方式/服务名称/**映射成路由规则;
  • CompositeRouteLocator:复合路由定位器,主要集成所有的路由定位器(如配置文件路由定位器,服务发现定位器,自定义路由定位器等)来路由定位;
  • RefreshableRouteLocator:路由刷新,只有实现了此接口的路由定位器才能被刷新。

从实现类的功能看路由定位器的作用就是区分当前从哪里加载路由进行注册。上面这几个实现类都实现了Ordered类,加载的顺序依照getOrder()数值大小来定。

至此我们已经把Zuul最核心的路由部分撸了一遍,从Spring MVC 加载Servlet 的过程入手,到自定义 ZuulServlet 进行处理,进而使用Zuul中定义的各种Filter来做逻辑过滤。原理其实很简单,重要的是思想。作为一个网关,它是很重要的服务,这种实现方式大家觉得是否优雅,是否还有别的实现方式呢?如果是你你会如何实现网关,这些问题大家可以慢慢思考。

以上是关于Spring Cloud Zuul 精进的主要内容,如果未能解决你的问题,请参考以下文章

Spring Cloud 微服务二:API网关spring cloud zuul

Spring Cloud Zuul 综合使用

7Spring -Cloud-路由网管Spring Cloud Zuul

spring cloud:Edgware.RELEASE版本中zuul回退方法的变化

spring cloud zuul

spring-cloud-Zuul学习--典型配置重新定义spring cloud实践