接着上一篇继续讲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 精进的更多相关文章

  1. Spring Cloud Zuul 添加 ZuulFilter

    紧接着上篇随笔Spring Cloud Zuul写,添加过滤器,进行权限验证 1.添加过滤器 package com.dzpykj.filter; import java.io.IOException ...

  2. Spring Cloud Zuul网关 Filter、熔断、重试、高可用的使用方式。

    时间过的很快,写springcloud(十):服务网关zuul初级篇还在半年前,现在已经是2018年了,我们继续探讨Zuul更高级的使用方式. 上篇文章主要介绍了Zuul网关使用模式,以及自动转发机制 ...

  3. 笔记:Spring Cloud Zuul 快速入门

    Spring Cloud Zuul 实现了路由规则与实例的维护问题,通过 Spring Cloud Eureka 进行整合,将自身注册为 Eureka 服务治理下的应用,同时从 Eureka 中获取了 ...

  4. Spring Cloud Zuul 限流详解(附源码)(转)

    在高并发的应用中,限流往往是一个绕不开的话题.本文详细探讨在Spring Cloud中如何实现限流. 在 Zuul 上实现限流是个不错的选择,只需要编写一个过滤器就可以了,关键在于如何实现限流的算法. ...

  5. Spring Cloud Zuul 中文文件上传乱码

    原文地址:https://segmentfault.com/a/1190000011650034 1 描述 使用Spring Cloud Zuul进行路由转发时候吗,文件上传会造成中文乱码“?”.1. ...

  6. spring cloud zuul参数调优

    zuul 内置参数 zuul.host.maxTotalConnections 适用于ApacheHttpClient,如果是okhttp无效.每个服务的http客户端连接池最大连接,默认是200. ...

  7. Spring Cloud Zuul 网关使用与 OAuth2.0 认证授权服务

    API 网关的出现的原因是微服务架构的出现,不同的微服务一般会有不同的服务地址,而外部客户端可能需要调用多个服务的接口才能完成一个业务需求,如果让客户端直接与各个微服务通信,会有以下的问题: 客户端会 ...

  8. spring cloud: zuul(四): 正则表达式匹配其他微服务(给其他微服务加版本号)

    spring cloud: zuul(四): 正则表达式匹配其他微服务(给其他微服务加版本号) 比如我原来有,spring-boot-user微服务,后台进行迭代更新,另外其了一个微服务: sprin ...

  9. spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关)

    spring cloud: zuul(二): zuul的serviceId/service-id配置(微网关) zuul: routes: #路由配置表示 myroute1: #路由名一 path: ...

随机推荐

  1. StringBuilder删除最后的字符

    stringbuilder碰到拼接XXx:XXX:这样的字符的时候,往往需要删除最后一个字符,通过remove(起始索引,向右移除的个数)可以实现. StringBuilder sb = new St ...

  2. Redis 实战搭建高可用架构

    前言:最近在看关于redis缓存方面的知识,今天就来个 Redis sentinel 高可用架构,实战开始之前,先看看sentinel的概念 什么是redis-sentinel Redis-Senti ...

  3. Java Base64Utils ----Utils

    Java Base64Utils   目录   Java Base64Utils 7 /**  * <html>  * <body>  *  <P> Copyrig ...

  4. Mybatis映射文件中的标签的使用

    <foreach> <!-- foreach --> <delete id="delMulti" parameterType="java.u ...

  5. MySQL连接超时处理

    1.由于MySQL默认是8小时的wait_timeout,当超过8小时的连接时间后,在JAVA中调用将出现如下报错 SEVERE EXCEPTION com.mysql.jdbc.exceptions ...

  6. Ueditor文字和echarts图片 生成 word 前端解决方案

    编程就像搭积木,少了任何一个就拼接不起来,所有积木都找到就只剩下调试. 一.echarts  获取图片方法getDataURL  详细配置:https://www.echartsjs.com/zh/a ...

  7. 运用软链接,解决NBU默认安装目录空间不足

    建立软链接实例: 例如 /usr 剩余空间5G,/opt剩余空间100G 提前在通过如下命令在/opt 目录下创建openv文件夹 mkdir -p /opt/openv 然后进入 /opt/open ...

  8. js处理滚动条操作

    在做UI自动化项目的时候,我们不免会遇到一些页面的内容比较多,导致会有滚动条,但是我们又主要操作当前页面看不到的元素,这怎么办呢? 在我们实际操作过程中,我们肯定是直接滑动鼠标操作就可以完成,但是在做 ...

  9. Leetcode——3. 无重复字符的最长子串

    难度: 中等 题目 Given a string, find the length of the longest substring without repeating characters. 给定一 ...

  10. Codeforces H. Kilani and the Game(多源BFS)

    题目描述: Kilani and the Game time limit per test 2 seconds memory limit per test 256 megabytes input st ...