SpringCloud学习笔记(七、SpringCloud Netflix Zuul)
目录:
- 什么是zuul
- 为什么要有zuul、zuul能做什么
- zuul的基础知识
- springcloud整合eureka、config、zuul
- zuul源码分析
什么是zuul
Zuul是Netflix开源的一款网关项目,说到这里我们就需要了解一下什么是网关了。
按照个人理解,网关就是网络层面的关卡,是流量的一扇门,流量是否能通过这扇门,以及通向什么地方由网关来控制;这里你可以把网关类比成海关,这样你可以能更加快速的理解网关的含义。
综上服务网关最基本的功能便是路由转发 + 过滤器。
- 路由转发:接收一切外界请求,转发到后端的微服务上去;这里我拿我们较为熟悉的海关来类比,当一个商品要流入我国时我国的海关是一定要把控商品的流向的,比如可以流入到什么地方,不能流入到什么地方。
- 过滤器:在服务网关中可以完成一系列的横切功能,例如权限校验、限流以及监控等,这些都可以通过过滤器完成(路由转发也是通过过滤器实现的);这里我还是拿海关来类比,商品入流我国时我国的海关肯定要读商品做一系列的检查(是否合法,数量的控制以及后续的流向等等),比如毒品、违禁品、外来物种都是不能流入我国的。
为什么要有zuul、zuul能做什么
这里还是可以用海关还说明。
首先我们假设没有海关会发生什么,第一商品的安全我们肯定是没有办法全面保证的,因为每个商家的认知水平肯定是参差不齐的,并不是每个商家都能很好的区分商品的安全性;就比如白粉是毒品,但它看起来和面粉没啥区别,假设有恐怖分子向通过这种方式来残害某地人员,而商家又没有识别到,这就会有严重的问题发生;当然这只是一个假设,实际上恐怖分子也不会这么蠢。
正如上面所说,商家的认知水平是参差不齐的,所以商家要保证商品的安全性就一定要学习如何辨识商品的危险度,这对商家来说无疑提高了学习成本,故此个人认为这也是海关应运而生的一个条件。
同理,在微服务体系中每个服务也肯定是要保证自身服务的安全,防止外部的恶意攻击;因此由服务自身来实现安全校验无疑增加了开发成本,而且也并不能很好的保证服务的安全,所以网关的诞生也是如此。
综上:zuul网关可以帮助微服务体系下的业务应用专注与自身的业务逻辑,而不必为了API日志、服务安全、监控、负载等等业务外的功能增加开发成本。
——————————————————————————————————————————————————————————————————————
上面说到微服务体系中可以通过网关来完成个服务的安全校验,这无疑使正确的。当然避免个服务都要自我实现安全校验的办法肯定不止一种,你可以将类似的操作都写到一个公共服务中去,然后其它的服务来依赖这个公共的服务。
是的,这样也可以达到预期的效果,但是微服务体系的服务实现肯定是不止java这一种语言的,如果你才用这种方法实现还是有些弊端的。就算这些服务都是java的,但也会因为依赖了公共服务,导致服务自身jar包的大小增加,不利于docker镜像进行部署的场景以及后续的维护。
——————————————————————————————————————————————————————————————————————
Zuul能做的很多,常用的应用场景如下:
- 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
- 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
- 动态路由:动态地将请求路由到不同的后端集群。
- 压力测试:逐渐增加指向集群的流量,以了解性能。
- 负载分配:为每一种负载类型分配对应容量,并启用超出限定值的请求。
- 静态响应处理:在边缘位置直接建立部分相应,从而避免其转发到内部集群。
zuul的基础知识
zuul路由配置:
1、传统路由配置:
传统路由的配置不依赖与服务发现机制,通过定制每个路由表达式实例的映射关系来实现api网关对外部请求的路由。
1 ### 单例配置 ### 2 # 配置请求路由路径:zuul.routes.<route>.path=xxx 3 zuul.routes.service-a.path=/service-a/** 4 # 配置路由后转发的url:zuul.routes.<route>.url=xxx 5 zuul.routes.service-a.url=http://localhost:8080/ 6 7 ### 多例配置 ### 8 # 配置请求路由路径:zuul.routes.<route>.path=xxx 9 zuul.routes.service-a.path=/service-a/** 10 # 配置服务id:zuul.routes.<route>.serviceId=xxx 11 zuul.routes.service-a.serviceId=service-a 12 # ribbon负载 13 ribbon.eureka.enabled=false 14 # 配置路由后转发的url:zuul.routes.<route>.url=xxx 15 service-a.ribbon.listOfServers=http://localhost:8080/,http://localhost:9090/
说到这里就有必要提一下路由路径配置的表达式了(也就是请求路径后面的**):
- ?:单个字符匹配,如/service-a/a、/service-a/b这些。
- *:匹配任意数量的字符:如/service-a/a、/service-a/aaa这些,像/service-a/a/b就匹配不到。
- **:匹配多级目前的任意数量字符:如/service-a/a、/service-a/aaa、/service-a/a/b都可以匹配到。
2、服务路由配置(基于服务发现):
集成服务发现时不需要像传统路由那样为serviceId指定具体服务实例的地址,通过SpringCloud Zuul与SpringCloud Eureka的整合就可以实现对服务实例的自动化维护。
1 zuul.routes.service-a.path=/service-a/** 2 zuul.routes.service-a.serviceId=service-a
3、路由路径配置:
- zuul.ignoredPatterns:忽略表达式,参数用来设置不希望被api网关进行路由的url表达式。
- zuul.routes.prefix=/xxx、zuul.routes.strip-prefix=false:路由前缀,为路由规则增加前缀;如你希望网关上的路由规则增加/api前缀就可以在prefix里配置;但需要之意的是你要吧zuul.routes.strip-prefix配置为true。
- serviceId:forward:/path:路由跳转,zuul支持forward形式的服务端跳转配置。
- zuul.sensitiveHeaders:Cookie与头信息,zuul支持过去调http请求头的一些敏感信息,防止传递到下游服务。
——————————————————————————————————————————————————————————————————————
zuul线程模型:
zuul有两种线程模型,一种同步阻塞、一种异步非阻塞。
1、同步阻塞:
2、异步非阻塞:
——————————————————————————————————————————————————————————————————————
zuul请求生命周期:
zuul分为4大过滤器,pre-前置过滤器、route-路由过滤器、post-后置过滤器、error-错误过滤器。
- pre:在请求转发前处理请求,如日志、请求校验等。
- route:将请求转发到具体的服务提供方。
- post:在接收到服务提供方的返回结果后做的一些处理,如数据加工、内容转换等等(数据脱敏)。
- error:当请求发生异常时启用error过滤器。
zuul过滤器:
1、zuul自带过滤器
2、自定义过滤器:
见:https://github.com/mrjdrs/springcloud-integration.git的com.jdr.maven.sc.integration.zuul.filter.LogFilter
springcloud整合eureka、config、zuul:
1、架构图
2、源码:https://github.com/mrjdrs/springcloud-integration.git
zuul源码分析:
阅读目的:
- 为什么在启动类上加@EnableZuulProxy就可以让此服务变成微服务网关,这个注解做了什么,它是如何实现的。
- zuul是动态路由 + 过滤器,这两个分别是如何实现的。
- 过滤器有四种,前置、请求转发、后置、错误,分别是如何实现的。
源码分析:
在阅读zuul源码前首先我们要大致了解下如下几个类的作用:
- ZuulProperties:zuul的相关配置。
- ZuulController:zuul的控制器,当有请求转发时会经过由ZuulController来处理请求。
- ZuulServlet:是zuul用于处理具体请求的一个servlet。
- ZuulHandlerMapping:HandlerMapping会根据请求的url找到对应处理器(Handler),并返回一个执行链;ZuulHandlerMapping就是根据配置的zuul请求找到对应的处理器。
好,开始分析!
——————————————————————————————————————————————————————————————————————
在SpringCloud Netflix Zuul中,我们只需要在启动类加上@EnableZuulProxy注解便可以让当前服务成为一个微服务网关。那么它是怎样做到的呢,我们来看看这个注解。
1 @EnableCircuitBreaker 2 @Target(ElementType.TYPE) 3 @Retention(RetentionPolicy.RUNTIME) 4 @Import(ZuulProxyMarkerConfiguration.class) 5 public @interface EnableZuulProxy { 6 }
@Target、@Retention不必多说,是元注解。@EnableCircuitBreaker是启用断路器,不在本次分析的范围内,所以我们主要看下@Import(ZuulProxyMarkerConfiguration.class)。
@Import(ZuulProxyMarkerConfiguration.class):将ZuulProxyMarkerConfiguration注册到Spring容器中;既然是注册的bean,那么肯定是要使用的,我们来看看它是在哪使用的。
通过IDEA查看后得知类在org.springframework.cloud.netflix.zuul.ZuulProxyAutoConfiguration调用了:
1 @Configuration 2 @Import({ RibbonCommandFactoryConfiguration.RestClientRibbonConfiguration.class, 3 RibbonCommandFactoryConfiguration.OkHttpRibbonConfiguration.class, 4 RibbonCommandFactoryConfiguration.HttpClientRibbonConfiguration.class, 5 HttpClientConfiguration.class }) 6 @ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class) 7 public class ZuulProxyAutoConfiguration extends ZuulServerAutoConfiguration { 8 }
结合ZuulProxyMarkerConfiguration的调用情况(@ConditionalOnBean(ZuulProxyMarkerConfiguration.Marker.class))和类名可以推测其只是一个标记类,无特殊逻辑。
通过对ZuulProxyAutoConfiguration的大致浏览你可以发下,这是对ZuulProxy的自动装配类,主要是自动的初始化了一些ZuulProxy相关的bean。
当然这些都不是本次的重点,这一小段都只是通过各种零碎的信息叫你如何看源码。
——————————————————————————————————————————————————————————————————————
重点来了。
我们可以看到ZuulProxyAutoConfiguration是一个zuul的代理类,所以肯定是增加的额外功能,其核心逻辑还是在父类ZuulServerAutoConfiguration中。
这一结论你也可以通过org.springframework.cloud.netflix.zuul.EnableZuulProxy上的注释来得到。
所以我们来看看ZuulServerAutoConfiguration到底定义且实现了一些什么东西,源码不是很多,但我任只保留了一些核心的东西。
1 @Configuration 2 @EnableConfigurationProperties({ ZuulProperties.class }) 3 @ConditionalOnClass(ZuulServlet.class) 4 @ConditionalOnBean(ZuulServerMarkerConfiguration.Marker.class) 5 // Make sure to get the ServerProperties from the same place as a normal web app would 6 @Import(ServerPropertiesAutoConfiguration.class) 7 public class ZuulServerAutoConfiguration { 8 9 @Autowired 10 protected ZuulProperties zuulProperties; 11 12 @Bean 13 public ZuulController zuulController() { 14 return new ZuulController(); 15 } 16 17 @Bean 18 public ZuulHandlerMapping zuulHandlerMapping(RouteLocator routes) { 19 ZuulHandlerMapping mapping = new ZuulHandlerMapping(routes, zuulController()); 20 mapping.setErrorController(this.errorController); 21 return mapping; 22 } 23 24 }
这里涉及到的几个类也是开篇所提到的那几个,ZuulProperties、ZuulController、ZuulServlet、ZuulHandlerMapping。
我们一个一个来分析。
1、ZuulProperties:zuul的相关配置都在这里。
稍微浏览一下,你可以很容易的发现我们较为熟悉的几个配置,如:
- routes:private Map<String, ZuulRoute> routes = new LinkedHashMap<>();
- ignoredPatterns:private Set<String> ignoredPatterns = new LinkedHashSet<>();
routes配置了路由转发相关的信息,如上面zuul基础知识提到的path、url、serviceId等等。
同样的我们把的核心代码贴在下面:
1 public static class ZuulRoute { 2 private String id; 3 private String path; 4 private String serviceId; 5 private String url; 6 private boolean stripPrefix = true; 7 private Boolean retryable; 8 private Set<String> sensitiveHeaders = new LinkedHashSet<>(); 9 private boolean customSensitiveHeaders = false; 10 11 // 省略构造函数、getter、setter等等 12 }
2、ZuulController:zuul的控制器,当有请求转发时会经过由ZuulController来处理请求。
从ZuulController的继承关系来看,它是通过Spring提供了Wrapping功能来实现自身逻辑的,其核心类图如下。
最上层的接口Controller的定义是为了处理请求,定义了handleRequest方法。
1 public interface Controller { 2 3 ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception; 4 5 }
AbstractController是Controller的抽象实现,定义了基本的可复用实现(检查请求参数、准备相应等等),且定义了处理内部请求的抽象方法handleRequestInternal。
1 public abstract class AbstractController extends WebContentGenerator implements Controller { 2 3 @Override 4 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) 5 throws Exception { 6 7 if (HttpMethod.OPTIONS.matches(request.getMethod())) { 8 response.setHeader("Allow", getAllowHeader()); 9 return null; 10 } 11 12 // Delegate to WebContentGenerator for checking and preparing. 13 checkRequest(request); 14 prepareResponse(response); 15 16 // Execute handleRequestInternal in synchronized block if required. 17 if (this.synchronizeOnSession) { 18 HttpSession session = request.getSession(false); 19 if (session != null) { 20 Object mutex = WebUtils.getSessionMutex(session); 21 synchronized (mutex) { 22 return handleRequestInternal(request, response); 23 } 24 } 25 } 26 27 return handleRequestInternal(request, response); 28 } 29 30 protected abstract ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 31 throws Exception; 32 33 }
而ServletWrappingController则是对AbstractController功能的一个包装,模板化的提供了提现方式,但需要指定实现的servlet。
1 public class ServletWrappingController extends AbstractController 2 implements BeanNameAware, InitializingBean, DisposableBean { 3 4 /** 具体实现的servlet类对象 */ 5 private Class<? extends Servlet> servletClass; 6 7 private String servletName; 8 9 private Properties initParameters = new Properties(); 10 11 private String beanName; 12 13 /** servlet示例 */ 14 private Servlet servletInstance; 15 16 public void setServletClass(Class<? extends Servlet> servletClass) { 17 this.servletClass = servletClass; 18 } 19 20 public void setServletName(String servletName) { 21 this.servletName = servletName; 22 } 23 24 @Override 25 public void afterPropertiesSet() throws Exception { 26 if (this.servletClass == null) { 27 throw new IllegalArgumentException("'servletClass' is required"); 28 } 29 if (this.servletName == null) { 30 this.servletName = this.beanName; 31 } 32 // 拿到servletClass实例初始化servletInstance 33 this.servletInstance = this.servletClass.newInstance(); 34 this.servletInstance.init(new DelegatingServletConfig()); 35 } 36 37 @Override 38 protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 39 throws Exception { 40 41 this.servletInstance.service(request, response); 42 return null; 43 } 44 45 }
最后我们来看看ZuulController,这样就会变得简单很多了。
1 public class ZuulController extends ServletWrappingController { 2 3 public ZuulController() { 4 // 定义servlet实现实例为ZuulServlet 5 setServletClass(ZuulServlet.class); 6 setServletName("zuul"); 7 setSupportedMethods((String[]) null); // Allow all 8 } 9 10 @Override 11 public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response) throws Exception { 12 try { 13 // We don't care about the other features of the base class, just want to 14 // handle the request 15 return super.handleRequestInternal(request, response); 16 } 17 finally { 18 // @see com.netflix.zuul.context.ContextLifecycleFilter.doFilter 19 RequestContext.getCurrentContext().unset(); 20 } 21 } 22 23 }
15行的super.handleRequestInternal也正是上面说到ServletWrappingController包装好的,由具体servlet实现。
1 @Override 2 protected ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) 3 throws Exception { 4 5 this.servletInstance.service(request, response); 6 return null; 7 }
所以这里的this.servletInstance.service(request, response);也会有ZuulServlet来完成,com.netflix.zuul.http.ZuulServlet#service。
3、ZuulHandlerMapping:ZuulHandlerMapping就是根据配置的zuul请求找到对应的处理器。
在细说ZuulHandlerMapping源码前是有必要了解下SpringMVC的,因为ZuulHandlerMapping的父级接口为HandlerMapping,它是SpringMVC的重要组件,会根据请求的url找到对应处理器(Handler),并返回一个执行链;同样的我们还是来瞅瞅ZuulHandlerMapping的类图。
HandlerMapping:定义了doHandler方法,用于返回此请求的处理程序和所有拦截器。
1 public interface HandlerMapping { 2 3 HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception; 4 5 }
AbstractHandlerMapping:对doHandler方法提供了默认实现,以及其他基础功能。
1 @Override 2 public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { 3 // 获取内部执行处理器(有具体子类实现) 4 Object handler = getHandlerInternal(request); 5 if (handler == null) { 6 // 当内部处理器为null时,取默认的处理器 7 // 默认处理器初始值为null,可以通过AbstractHandlerMapping#setDefaultHandler来指定 8 handler = getDefaultHandler(); 9 } 10 if (handler == null) { 11 return null; 12 } 13 // Bean name or resolved handler? 14 // 如果处理器是String类型(返回的是beanName),则直接去getBean 15 if (handler instanceof String) { 16 String handlerName = (String) handler; 17 handler = getApplicationContext().getBean(handlerName); 18 } 19 20 // 拿到执行链并返回(不是本次重点,不细说,有兴趣可细读) 21 HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request); 22 if (CorsUtils.isCorsRequest(request)) { 23 CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request); 24 CorsConfiguration handlerConfig = getCorsConfiguration(handler, request); 25 CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig); 26 executionChain = getCorsHandlerExecutionChain(request, executionChain, config); 27 } 28 return executionChain; 29 }
可见AbstractHandlerMapping#doHandler是先拿到由子类具体实现的内部执行器,再去获取执行链,这些操作都是包装好的,实现的子类只需要提供获取内部执行器的方法即可(当然也可以指定默认的处理器)。
AbstractUrlHandlerMapping:对url的处理映射。
1 @Override 2 protected Object getHandlerInternal(HttpServletRequest request) throws Exception { 3 // 获取除host外的访问路径 4 // 如request http://localhost:9999/user-consumer/userConsumer/get/ZD 5 // 得到/user-consumer/userConsumer/get/ZD 6 String lookupPath = getUrlPathHelper().getLookupPathForRequest(request); 7 // 查找handler 8 Object handler = lookupHandler(lookupPath, request); 9 if (handler == null) { 10 // We need to care for the default handler directly, since we need to 11 // expose the PATH_WITHIN_HANDLER_MAPPING_ATTRIBUTE for it as well. 12 Object rawHandler = null; 13 // 根目录处理器 14 if ("/".equals(lookupPath)) { 15 rawHandler = getRootHandler(); 16 } 17 // 默认处理器 18 if (rawHandler == null) { 19 rawHandler = getDefaultHandler(); 20 } 21 // rawHandler是beanName时getBean后再处理 22 if (rawHandler != null) { 23 // Bean name or resolved handler? 24 if (rawHandler instanceof String) { 25 String handlerName = (String) rawHandler; 26 rawHandler = getApplicationContext().getBean(handlerName); 27 } 28 validateHandler(rawHandler, request); 29 handler = buildPathExposingHandler(rawHandler, lookupPath, lookupPath, null); 30 } 31 } 32 if (handler != null && logger.isDebugEnabled()) { 33 logger.debug("Mapping [" + lookupPath + "] to " + handler); 34 } 35 else if (handler == null && logger.isTraceEnabled()) { 36 logger.trace("No handler mapping found for [" + lookupPath + "]"); 37 } 38 return handler; 39 }
1 protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { 2 // Direct match? 3 Object handler = this.handlerMap.get(urlPath); 4 if (handler != null) { 5 // Bean name or resolved handler? 6 if (handler instanceof String) { 7 String handlerName = (String) handler; 8 handler = getApplicationContext().getBean(handlerName); 9 } 10 validateHandler(handler, request); 11 return buildPathExposingHandler(handler, urlPath, urlPath, null); 12 } 13 14 // Pattern match? 15 List<String> matchingPatterns = new ArrayList<String>(); 16 for (String registeredPattern : this.handlerMap.keySet()) { 17 if (getPathMatcher().match(registeredPattern, urlPath)) { 18 matchingPatterns.add(registeredPattern); 19 } 20 else if (useTrailingSlashMatch()) { 21 if (!registeredPattern.endsWith("/") && getPathMatcher().match(registeredPattern + "/", urlPath)) { 22 matchingPatterns.add(registeredPattern + "/"); 23 } 24 } 25 } 26 27 String bestMatch = null; 28 Comparator<String> patternComparator = getPathMatcher().getPatternComparator(urlPath); 29 if (!matchingPatterns.isEmpty()) { 30 Collections.sort(matchingPatterns, patternComparator); 31 if (logger.isDebugEnabled()) { 32 logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns); 33 } 34 bestMatch = matchingPatterns.get(0); 35 } 36 if (bestMatch != null) { 37 handler = this.handlerMap.get(bestMatch); 38 if (handler == null) { 39 if (bestMatch.endsWith("/")) { 40 handler = this.handlerMap.get(bestMatch.substring(0, bestMatch.length() - 1)); 41 } 42 if (handler == null) { 43 throw new IllegalStateException( 44 "Could not find handler for best pattern match [" + bestMatch + "]"); 45 } 46 } 47 // Bean name or resolved handler? 48 if (handler instanceof String) { 49 String handlerName = (String) handler; 50 handler = getApplicationContext().getBean(handlerName); 51 } 52 validateHandler(handler, request); 53 String pathWithinMapping = getPathMatcher().extractPathWithinPattern(bestMatch, urlPath); 54 55 // There might be multiple 'best patterns', let's make sure we have the correct URI template variables 56 // for all of them 57 Map<String, String> uriTemplateVariables = new LinkedHashMap<String, String>(); 58 for (String matchingPattern : matchingPatterns) { 59 if (patternComparator.compare(bestMatch, matchingPattern) == 0) { 60 Map<String, String> vars = getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath); 61 Map<String, String> decodedVars = getUrlPathHelper().decodePathVariables(request, vars); 62 uriTemplateVariables.putAll(decodedVars); 63 } 64 } 65 if (logger.isDebugEnabled()) { 66 logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables); 67 } 68 return buildPathExposingHandler(handler, bestMatch, pathWithinMapping, uriTemplateVariables); 69 } 70 71 // No handler found... 72 return null; 73 }
lookupHandler分为两种处理,一种是精确匹配,一种是正则匹配;如/get/user这种则为精确匹配,正则则为这种/get/user/{id:\\d+}。
两种方式源码不难,不做赘述。但其中handler都是从handlerMap取值,所以handlerMap中的值是从哪来的我们还是要了解清楚。
1 private final Map<String, Object> handlerMap = new LinkedHashMap<String, Object>();
首先handlerMap是private,所以put应该是在本类发生的,当然你可能会说通过getHandlerMap函数再在外部进行修改;这是不允许的,因为getHandlerMap定义成了unmodifiableMap。
1 public final Map<String, Object> getHandlerMap() { 2 return Collections.unmodifiableMap(this.handlerMap); 3 }
所以put是肯定只能在类的内部发生,根据这个思路,便可以找到AbstractUrlHandlerMapping#registerHandler(java.lang.String, java.lang.Object)。
1 protected void registerHandler(String urlPath, Object handler) throws BeansException, IllegalStateException { 2 Assert.notNull(urlPath, "URL path must not be null"); 3 Assert.notNull(handler, "Handler object must not be null"); 4 Object resolvedHandler = handler; 5 6 // Eagerly resolve handler if referencing singleton via name. 7 if (!this.lazyInitHandlers && handler instanceof String) { 8 String handlerName = (String) handler; 9 if (getApplicationContext().isSingleton(handlerName)) { 10 resolvedHandler = getApplicationContext().getBean(handlerName); 11 } 12 } 13 14 Object mappedHandler = this.handlerMap.get(urlPath); 15 if (mappedHandler != null) { 16 if (mappedHandler != resolvedHandler) { 17 throw new IllegalStateException( 18 "Cannot map " + getHandlerDescription(handler) + " to URL path [" + urlPath + 19 "]: There is already " + getHandlerDescription(mappedHandler) + " mapped."); 20 } 21 } 22 else { 23 // 根目录匹配 24 if (urlPath.equals("/")) { 25 if (logger.isInfoEnabled()) { 26 logger.info("Root mapping to " + getHandlerDescription(handler)); 27 } 28 setRootHandler(resolvedHandler); 29 } 30 // 一级目录匹配 31 else if (urlPath.equals("/*")) { 32 if (logger.isInfoEnabled()) { 33 logger.info("Default mapping to " + getHandlerDescription(handler)); 34 } 35 setDefaultHandler(resolvedHandler); 36 } 37 // 其它情况(多级目录匹配) 38 else { 39 this.handlerMap.put(urlPath, resolvedHandler); 40 if (logger.isInfoEnabled()) { 41 logger.info("Mapped URL path [" + urlPath + "] onto " + getHandlerDescription(handler)); 42 } 43 } 44 } 45 }
之后我们根据registerHandler函数的调用,便可以得出ZuulHandlerMapping在ZuulHandlerMapping#lookupHandler初始化了用户配置的route。
1 zuul.routes.user-provider=/user-provider/** 2 zuul.routes.user-consumer=/user-consumer/**
1 private void registerHandlers() { 2 Collection<Route> routes = this.routeLocator.getRoutes(); 3 if (routes.isEmpty()) { 4 this.logger.warn("No routes found from RouteLocator"); 5 } 6 else { 7 for (Route route : routes) { 8 registerHandler(route.getFullPath(), this.zuul); 9 } 10 } 11 }
ZuulMappingHandler:针对zuul的映射器。
分析完上述三个父类后,ZuulMappingHandler就显得格外简单了,它对lookupHandler进行了重写,可忽略一些指定的路径,以及热加载routes列表(低版本的有bug,不建议使用)。
1 @Override 2 protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception { 3 if (this.errorController != null && urlPath.equals(this.errorController.getErrorPath())) { 4 return null; 5 } 6 // 忽略指定的路径(不走Zuul网关) 7 if (isIgnoredPath(urlPath, this.routeLocator.getIgnoredPaths())) return null; 8 RequestContext ctx = RequestContext.getCurrentContext(); 9 if (ctx.containsKey("forward.to")) { 10 return null; 11 } 12 // 文件改变时会通过Spring事件机制将dirty重新置为true 13 // 以完成热加载routes配置 14 if (this.dirty) { 15 synchronized (this) { 16 if (this.dirty) { 17 registerHandlers(); 18 this.dirty = false; 19 } 20 } 21 } 22 return super.lookupHandler(urlPath, request); 23 }
在总结完ZuulHandlerMapping后,你肯定有一个疑问,为什么在开篇要提到SpringMVC呢;因为HandlerMapping是SpringMVC的重要组件,定义的doHandler都是在DispatcherServlet初始化调用的。
其逻辑在org.springframework.web.servlet.DispatcherServlet#getHandler:
1 protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { 2 for (HandlerMapping hm : this.handlerMappings) { 3 if (logger.isTraceEnabled()) { 4 logger.trace( 5 "Testing handler map [" + hm + "] in DispatcherServlet with name '" + getServletName() + "'"); 6 } 7 HandlerExecutionChain handler = hm.getHandler(request); 8 if (handler != null) { 9 return handler; 10 } 11 } 12 return null; 13 }
这块需要你自己debug理解。
最后我们来总结下ZuulHandlerMapping的调用链路:
1 org.springframework.web.servlet.DispatcherServlet#getHandler 2 org.springframework.web.servlet.handler.AbstractHandlerMapping#getHandler 3 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#getHandlerInternal 4 org.springframework.cloud.netflix.zuul.web.ZuulHandlerMapping#lookupHandler 5 org.springframework.web.servlet.handler.AbstractUrlHandlerMapping#lookupHandler
由DispatcherServlet开始获取所有Handler,并调用getHandler方法;在确认Handler为ZuulHandlerMapping后,依次调用AbstractHandlerMapping、AbstractUrlHandlerMapping、ZuulHandlerMapping。
4、ZuulServlet:是zuul用于处理具体请求的一个servlet。
在源码的最后,我们把zuul的其它几个核心组件,如ZuulProperties、ZuulController、ZuulHandlerMapping都简单的过了一遍,就只剩下具体的Servlet处理了。
你现在还记得他是如何调用的嘛,没错正式由ZuulController#handleRequest委托ServletWrappingController#handleRequestInternal调用的。
1 @Override 2 public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse) throws ServletException, IOException { 3 try { 4 init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse); 5 6 // Marks this request as having passed through the "Zuul engine", as opposed to servlets 7 // explicitly bound in web.xml, for which requests will not have the same data attached 8 RequestContext context = RequestContext.getCurrentContext(); 9 context.setZuulEngineRan(); 10 11 try { 12 // 路由前的处理 13 preRoute(); 14 } catch (ZuulException e) { 15 error(e); 16 postRoute(); 17 return; 18 } 19 try { 20 // 路由处理 21 route(); 22 } catch (ZuulException e) { 23 error(e); 24 postRoute(); 25 return; 26 } 27 try { 28 // 路由后的处理 29 postRoute(); 30 } catch (ZuulException e) { 31 error(e); 32 return; 33 } 34 35 } catch (Throwable e) { 36 // 发送异常时的处理 37 error(new ZuulException(e, 500, "UNHANDLED_EXCEPTION_" + e.getClass().getName())); 38 } finally { 39 RequestContext.getCurrentContext().unset(); 40 } 41 }
每个处理器都由ZuulRunner封装,由FilterProcessor具体调用。
1 void postRoute() throws ZuulException { 2 zuulRunner.postRoute(); 3 } 4 5 void route() throws ZuulException { 6 zuulRunner.route(); 7 } 8 9 void preRoute() throws ZuulException { 10 zuulRunner.preRoute(); 11 }
1 public void postRoute() throws ZuulException { 2 FilterProcessor.getInstance().postRoute(); 3 } 4 5 public void route() throws ZuulException { 6 FilterProcessor.getInstance().route(); 7 } 8 9 public void preRoute() throws ZuulException { 10 FilterProcessor.getInstance().preRoute(); 11 }
每个过滤器都会走向runFilters方法,拿到相应类型的过滤器列表并以此调用processZuulFilter方法:
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 // 首先根据传入的过滤器类型来拿到过滤器列表 7 List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType); 8 if (list != null) { 9 for (int i = 0; i < list.size(); i++) { 10 ZuulFilter zuulFilter = list.get(i); 11 // 然后依次调用processZuulFilter方法 12 Object result = processZuulFilter(zuulFilter); 13 if (result != null && result instanceof Boolean) { 14 bResult |= ((Boolean) result); 15 } 16 } 17 } 18 return bResult; 19 }
processZuulFilter方法也不难,主要逻辑是调用runFilter,而runFilter则是调用com.netflix.zuul.IZuulFilter#run。
这几段逻辑不难读懂,我把它简单列出来。
1 public Object processZuulFilter(ZuulFilter filter) throws ZuulException { 2 3 RequestContext ctx = RequestContext.getCurrentContext(); 4 boolean bDebug = ctx.debugRouting(); 5 final String metricPrefix = "zuul.filter-"; 6 long execTime = 0; 7 String filterName = ""; 8 try { 9 long ltime = System.currentTimeMillis(); 10 filterName = filter.getClass().getSimpleName(); 11 12 RequestContext copy = null; 13 Object o = null; 14 Throwable t = null; 15 16 if (bDebug) { 17 Debug.addRoutingDebug("Filter " + filter.filterType() + " " + filter.filterOrder() + " " + filterName); 18 copy = ctx.copy(); 19 } 20 21 ZuulFilterResult result = filter.runFilter(); 22 ExecutionStatus s = result.getStatus(); 23 execTime = System.currentTimeMillis() - ltime; 24 25 switch (s) { 26 case FAILED: 27 t = result.getException(); 28 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); 29 break; 30 case SUCCESS: 31 o = result.getResult(); 32 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.SUCCESS.name(), execTime); 33 if (bDebug) { 34 Debug.addRoutingDebug("Filter {" + filterName + " TYPE:" + filter.filterType() + " ORDER:" + filter.filterOrder() + "} Execution time = " + execTime + "ms"); 35 Debug.compareContextState(filterName, copy); 36 } 37 break; 38 default: 39 break; 40 } 41 42 if (t != null) throw t; 43 44 usageNotifier.notify(filter, s); 45 return o; 46 47 } catch (Throwable e) { 48 if (bDebug) { 49 Debug.addRoutingDebug("Running Filter failed " + filterName + " type:" + filter.filterType() + " order:" + filter.filterOrder() + " " + e.getMessage()); 50 } 51 usageNotifier.notify(filter, ExecutionStatus.FAILED); 52 if (e instanceof ZuulException) { 53 throw (ZuulException) e; 54 } else { 55 ZuulException ex = new ZuulException(e, "Filter threw Exception", 500, filter.filterType() + ":" + filterName); 56 ctx.addFilterExecutionSummary(filterName, ExecutionStatus.FAILED.name(), execTime); 57 throw ex; 58 } 59 } 60 }
1 public ZuulFilterResult runFilter() { 2 ZuulFilterResult zr = new ZuulFilterResult(); 3 if (!isFilterDisabled()) { 4 if (shouldFilter()) { 5 Tracer t = TracerFactory.instance().startMicroTracer("ZUUL::" + this.getClass().getSimpleName()); 6 try { 7 Object res = run(); 8 zr = new ZuulFilterResult(res, ExecutionStatus.SUCCESS); 9 } catch (Throwable e) { 10 t.setName("ZUUL::" + this.getClass().getSimpleName() + " failed"); 11 zr = new ZuulFilterResult(ExecutionStatus.FAILED); 12 zr.setException(e); 13 } finally { 14 t.stopAndLog(); 15 } 16 } else { 17 zr = new ZuulFilterResult(ExecutionStatus.SKIPPED); 18 } 19 } 20 return zr; 21 }
综上:只要在zuul的上下文中有定义前置、路由、后置、错误这几种过滤器时,zuul就会在请求进来的时候顺序调用,那么是不是说我们想要自定义过滤器时只要继承ZuulFilter方法呢,是的就是这样;这一点我们之前也在zuul过滤器时说过,你可以再回去看看。
——————————————————————————————————————————————————————————————————————
至此zuul的基本运行原理已经说完了,我们再会到开头提到的几个问题,一一解答。
1、为什么在启动类上加@EnableZuulProxy就可以让此服务变成微服务网关,这个注解做了什么,它是如何实现的。
@EnableZuulProxy通过@Import注解导入了ZuulProxyMarkerConfiguration,此中完成了一些自动化的配置,这样在后续的请求中就可以顺利使用了。
而这些加载的ZuulProperties、ZuulController、ZuulHandlerMapping、ZuulServlet都是由SpringMVC在doDispatcher的getHandler调用。
2、zuul是动态路由 + 过滤器,这两个分别是如何实现的。
动态路由:基于AbstractUrlHandlerMapping中对精确匹配、正则表达匹配来判断是否需要zuul网关动态路由,拿到routes.path转发请求到routes.url。
过滤器:定义根接口IZuulFilter,在请求到达前依次经过前置、请求转发、后置过滤器,发送异常是走到错误过滤器。
3、过滤器有四种,前置、请求转发、后置、错误,分别是如何实现的。
同上。