6.4 Zuul的真正威力:过滤器

虽然通过Zuul网关代理所有请求确实可以简化服务调用,但是在想要编写应用于所有流经网关的服务调用的自定义逻辑时, Zuul的真正威力才发挥出来。在大多数情况下,这种自定义逻辑用于强制执行一组一致的应用程序策略,如安全性、日志记录和对所有服务的跟踪。
    这些应用程序策略被认为是横切关注点,因为开发人员希望将它们应用于应用程序中的所有服务,而无需修改每个服务来实现它们。通过这种方式,Zuul过滤器可以按照与J2EE servlet过滤器或Spring Aspect类似的方式来使用。这种方式可以拦截大量行为,并且在原始编码人员意识不到变化的情况下,对调用的行为进行装饰或更改。servlet过滤器或Spring Aspect被本地化为特定的服务,而使用Zuul和Zuul过滤器允许开发人员为通过Zuul路由的所有服务实现横切关注点。
    Zuul允许开发人员使用Zuul网关内的过滤器构建自定义逻辑。过滤器可用于实现每个服务请求在执行时都会经过的业务逻辑链。
    Zuul支持以下3种类型的过滤器。
    前置过滤器——前置过滤器在Zuul将实际请求发送到目的地之前被调用。前置过滤器通常执行确保服务具有一致的消息格式(例如,关键的HTTP首部是否设置妥当)的任务,或者充当看门人,确保调用 证(他们的身份与他们声称的一致)和授权(他们可以做他们请求做的)。
    后置过滤器——后置过滤器在目标服务被调用并将响应发送回客户端后被调用。通常后置过滤器会用来记录从目标服务返回的响应、处理错误或审核对敏感信息的响应。
    路由过滤器——路由过滤器用于在调用目标服务之前拦截调用。通常使用路由过滤器来确定是否需要进行某些级别的动态路由。例如,本章的后面将使用路由级别的过滤器,该过滤器将在同一服务的两个不同版本之间进行路由,以便将一小部分的服务调用路由到服务的新版本,而不是路由到现有的服务。这样就能够在不让每个人都使用新服务的情况下,让少量的用户体验新功能。
    图6-11展示了在处理服务客户端请求时,前置过滤器、后置过滤器和路由过滤器如何组合在一起。
 
 
图6-11 前置过滤器、路由过滤器和后置过滤器组成了客户端请求流经的管道。随着请求进入Zuul,这些过滤器可以处理传入的请求
    如果遵循图6-11中所列出的流程,将会看到所有的事情都是从服务客户端调用服务网关公开的服务开始的。从这里开始,发生了以下活动。
    (1)在请求进入Zuul网关时,Zuul调用所有在Zuul网关中定义的前置过滤器。前置过滤器可以在HTTP请求到达实际服务之前对HTTP请求进行检查和修改。前置过滤器不能将用户重定向到不同的端点或服务。
    (2)在针对Zuul的传入请求执行前置过滤器之后,Zuul将执行已定义的路由过滤器。路由过滤器可以更改服务所指向的目的地。
    (3)路由过滤器可以将服务调用重定向到Zuul服务器被配置的发送路由以外的位置。但Zuul路由过滤器不会执行HTTP重定向,而是会终止传入的HTTP请求,然后代表原始调用者调用路由。这意味着路由过滤器必须完全负责动态路由的调用,并且不能执行HTTP重定向。
    (4)如果路由过滤器没有动态地将调用者重定向到新路由,Zuul服务器将发送到最初的目标服务的路由。
    (5)目标服务被调用后,Zuul后置过滤器将被调用。后置过滤器可以检查和修改来自被调用服务的响应。
 
了解如何实现Zuul过滤器的最佳方法就是使用它们。为此,在接下来的几节中,我们将构建前置过滤器、路由过滤器和后置过滤器,然后通过它们运行服务客户端请求。
    图6-12展示了如何将这些过滤器组合在一起以处理对EagleEye服务的请求。
 
 
图6-12 Zuul过滤器提供对服务调用、日志记录和动态路由的集中跟踪。Zuul过滤器允许开发人员针对微服务调用执行自定义规则和策略
    按照图6-12所示的流程,读者会看到以下过滤器被使用。
    (1)TrackingFilter——TrackingFilter是一个前置过滤器,它确保从Zuul流出的每个请求都具有相关的关联ID。关联ID是在执行客户请求时执行的所有微服务中都会携带的唯一ID。关联ID用于跟踪一个调用经过一系列微服务调用发生的事件链。
    (2)SpecialRoutesFilter——SpecialRoutesFilter是一个Zuul路由过滤器,它将检查传入的路由,并确定是否要在该路由上进行A/B测试。A/B测试是一种技术,在这种技术中,用户(在这种情况下是服务)随机使用同一个服务提供的两种不同的服务版本。A/B测试背后的理念是,新功能可以在推出到整个用户群之前进行测试。在我们的例子中,同一个组织服务将具有两个不同的版本。少数用户将被路由到较新版本的服务,与此同时,大多数用户将被路由到较旧版本的服务。
    (3)ResponseFilter——ResponseFilter是一个后置过滤器,它将把与服务调用相关的关联ID注入发送回客户端的HTTP响应首部中。这样,客户端就可以访问与其发出的请求相关联的关联ID。
 
6.5 构建第一个生成关联ID的Zuul前置过滤器
    在Zuul中构建过滤器是非常简单的。我们首先将构建一个名为TrackingFilter的Zuul前置过滤器,该过滤器将检查所有到网关的传入请求,并确定请求中是否存在名为tmx-correlation-id的HTTP首部。tmx-correlation-id首部将包含一个唯一的全局通用ID(Globally Universal ID,GUID),它可用于跨多个微服务来跟踪用户请求。
    
注意
 
我们在第5章中讨论了关联ID的概念。在这里我们将更详细地介绍如何使用Zuul来生成一个关联ID。如果读者跳过了此内容,我强烈建议读者查看第5章并阅读5.9节的内容。关联ID的实现将使用ThreadLocal变量实现,而要让ThreadLocal变量与Hystrix一起使用需要做额外的工作。
    
如果在HTTP首部中不存在tmx-correlation-id,那么Zuul TrackingFilter将生成并设置该关联ID。如果已经存在关联ID,那么Zuul将不会对该关联ID进行任何操作。关联ID的存在意味着该特定服务调用是执行用户请求的服务调用链的一部分。在这种情况下,TrackingFilter类将不执行任何操作。
    我们来看看代码清单6-6中的TrackingFilter的实现。这段代码也可以在本书示例的zuulsvr/src/main/java/com/thoughtmechanix/zuulsvr/filters/TrackingFilter.java中找到。
    
代码清单6-6 用于生成关联ID的Zuul前置过滤器
 
package com.thoughtmechanix.zuulsvr.filters;
import org.springframework.beans.factory.annotation.Autowired;
// 为了简洁,省略了其他import语句
←所有Zuul过滤器必须扩展ZuulFilter类和重写的四种方法:filterType(), filterOrder(), shouldFilter()和run()。
@Component
public class TrackingFilter extends ZuulFilter{
 
private static final int FILTER_ORDER = 1;
private static final boolean SHOULD_FILTER=true;
private static final Logger logger = LoggerFactory.getLogger(TrackingFilter.class);
 
⇽--- 在所有过滤器中使用的常用方法都封装在FilterUtils类中
@Autowired
FilterUtils filterUtils;
⇽--- filterType()方法用于告诉Zuul,该过滤器是前置过滤器、路由过滤器还是后置过滤器
@Override
public String filterType() {
return FilterUtils.PRE_FILTER_TYPE;
}
⇽--- filterOrder()方法返回一个整数值,指示不同类型的过滤器的执行顺序
@Override
public int filterOrder() {
return FILTER_ORDER;
}
⇽--- shouldFilter()方法返回一个布尔值来指示该过滤器是否要执行    
public boolean shouldFilter() {
return SHOULD_FILTER;
}
private boolean isCorrelationIdPresent(){
if (filterUtils.getCorrelationId() !=null){
return true;
}
return false;
}
⇽--- 该辅助方法实际上检查tmx-correlation-id是否存在,并且可以生成关联ID的GUID值  
private String generateCorrelationId(){
return java.util.UUID.randomUUID().toString();
}
⇽--- run()方法是每次服务通过过滤器时执行的代码。run()方法检查tmx-correlation-id是否存在,如果不存在,则生成一个关联值,并设置HTTP首部tmx-correlation-id        
public Object run() {
if (isCorrelationIdPresent()) {
logger.debug("tmx-correlation-id found in tracking filter: {}.",filterUtils.getCorrelationId());
}else{
filterUtils.setCorrelationId(generateCorrelationId());
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId());
}
RequestContext ctx =RequestContext.getCurrentContext();
logger.debug("Processing incoming request for {}.",ctx.getRequest().getRequestURI());
return null;
}
}
    
要在Zuul中实现过滤器,必须扩展ZuulFilter类,然后覆盖4个方法,即filterType()、filterOrder()、shouldFilter()和run()方法。代码清单6-6中的前三个方法描述了Zuul正在构建什么类型的过滤器,与这个类型的其他过滤器相比它应该以什么顺序运行,以及它是否应该处于活跃状态。最后一个方法run()包含过滤器要实现的业务逻辑。
    
我们已经实现了一个名为FilterUtils的类。这个类用于封装所有过滤器使用的常用功能。FilterUtils类位于zuulsvr/src/main/java/com/thoughtmechanix/zuulsvr/ FilterUtils.java中。本书不会详细解释整个FilterUtils类,在这里讨论的关键方法是getCorrelationId()和setCorrelationId()。
代码清单6-7展示了FilterUtils类的getCorrelationId()方法的代码。
    
代码清单6-7 从HTTP首部检索tmx-correlation-id
 
public String getCorrelationId(){
RequestContext ctx = RequestContext.getCurrentContext();
if (ctx.getRequest().getHeader(CORRELATION_ID) !=null) {
return ctx.getRequest().getHeader(CORRELATION_ID);
}
else{
return ctx.getZuulRequestHeaders().get(CORRELATION_ID);
}
}
    
在代码清单6-7中要注意的关键点是,首先要检查是否已经在传入请求的HTTP首部设置了tmx-correlation-ID。这里使用ctx.getRequest().getHeader(CORRELATION_ID)调用来做到这一点。
    
注意
    
    在一般的Spring MVC或Spring Boot服务中,RequestContext是org.springframework.web.servletsupport.RequestContext类型的。然而,Zuul提供了一个专门的RequestContext,它具有几个额外的方法来访问Zuul特定的值。该请求上下文是com.netflix.zuul.context包的一部分。
 
如果tmx-correlation-ID不存在,接下来就检查ZuulRequestHeaders。Zuul不允许直接添加或修改传入请求中的HTTP请求首部。如果想要添加tmx-correlation-id,并且以后在过滤器中能够再次访问到它,实际上在ctx.getRequestHeader()调用的结果中并不会包含它。为了解决这个问题,可以使用FilterUtils的getCorrelationId()方法。读者可能还记得,在TrackingFilter类的run()方法中,我们使用了以下代码片段:
else{     
filterUtils.setCorrelationId(generateCorrelationId());     
logger.debug("tmx-correlation-id generated in tracking filter: {}.", filterUtils.getCorrelationId()); 
}
    
tmx-correlation-id的设置发生在FilterUtils的setCorrelationId()方法中: 
public void setCorrelationId(String correlationId){     
RequestContext ctx = RequestContext.getCurrentContext();     
ctx.addZuulRequestHeader(CORRELATION_ID, correlationId); }
 
在FilterUtils的setCorrelationId()方法中,要向HTTP请求首部添加值时,应使用RequestContext的addZuulRequestHeader()方法。该方法将维护一个单独的HTTP首部映射,这个映射是在请求通过Zuul服务器流经这些过滤器时添加的。当Zuul服务器调用目标服务时,包含在ZuulRequestHeader映射中的数据将被合并。
    
在服务调用中使用关联ID
    既然已经确保每个流经Zuul的微服务调用都添加了关联ID,那么如何确保:
    正在被调用的微服务可以很容易访问关联ID;
    下游服务调用微服务时可能也会将关联ID传播到下游调用中。
 
要实现这一点,需要为每个微服务构建一组3个类。这些类将协同工作,从传入的HTTP请求中读取关联ID(以及稍后添加的其他信息),并将它映射到可以由应用程序中的业务逻辑轻松访问和使用的类,然后确保关联ID被传播到任何下游服务调用。
    图6-13展示了如何使用许可证服务来构建这些不同的部分。
 
图6-13 使用一组公共类,以便将关联ID传播到下游服务调用
    
我们来看一下图6-13中发生了什么。
    (1)当通过Zuul网关对许可证服务进行调用时,TrackingFilter会为所有进入Zuul的调用在传入的HTTP首部中注入一个关联ID。
    (2)UserContextFilter类是一个自定义的HTTP servlet过滤器。它将关联ID映射到UserContext类。UserContext存储在本地线程存储中,以便稍后在调用中使用。
    (3)许可证服务业务逻辑需要执行对组织服务的调用。
    (4)RestTemplate用于调用组织服务。RestTemplate将使用自定义的Spring拦截器类(UserContextInterceptor)将关联ID作为HTTP首部注入出站调用。
 
重复代码与共享库对比
    
是否应该在微服务中使用公共库的话题是微服务设计中的一个灰色地带。微服务纯粹主义者会告诉你,不应该在服务中使用自定义框架,因为它会在服务中引入人为的依赖。业务逻辑的更改或bug修正可能会对所有服务造成大规模的重构。但是,其他微服务实践者会指出,纯粹主义者的方法是不切实际的,因为会存在这样一些情况(如前面的UserContextFilter例子),在这些情况下构建公共库并在服务之间共享它是有意义的。
 
我认为这里存在一个中间地带。在处理基础设施风格的任务时,是很适合使用公共库的。但是,如果开始共享面向业务的类,就是在自找麻烦,因为这样是在打破服务之间的界限。
    在本章的代码示例中,我似乎违背了自己的建议,因为如果查看本章中的所有服务,读者就会发现它们都有自己的UserContextFilter、UserContext和UserContextInterceptor类的副本。在这里我之所以采用无共享的方法,是因为我不希望通过创建一个必须发布到第三方Maven存储库的共享库来将代码示例复杂化。因此,该服务的utils包中的所有类都在所有服务之间共享。
    
1.UserContextFilter:拦截传入的HTTP请求
    要构建的第一个类是UserContextFilter类。这个类是一个HTTP servlet过滤器,它将拦截进入服务的所有传入HTTP请求,并将关联ID(和其他一些值)从HTTP请求映射到UserContext类。
代码清单6-8展示了UserContext类的代码。这个类的源代码可以在licensing-service/src/main/java/com/thoughtmechanix/licenses/utils/UserContextFilter.java中找到。
    
代码清单6-8 将关联ID映射到UserContext类
package com.thoughtmechanix.licenses.utils;
// 为了简洁,省略了import语句
⇽--- 这个过滤器是通过使用Spring的@Component注解和实现一个 javax.servler.Filter 接口来被Spring注册与获取的
@Component
public class UserContextFilter implements Filter {
private static final Logger logger =LoggerFactory.getLogger(UserContextFilter.class);
@Override
public void doFilter(ServletRequest servletRequest,ServletResponse servletResponse,
FilterChain filterChain)throws IOException, ServletException {
HttpServletRequest httpServletRequest = (HttpServletRequest)servletRequest;
 ⇽--- 过滤器从首部中检索关联ID,并将值设置在UserContext类       
UserContextHolder.getContext().setCorrelationId(
httpServletRequest.getHeader(UserContext.CORRELATION_ID));
UserContextHolder.getContext().setUserId(
httpServletRequest.getHeader(UserContext.USER_ID));
⇽--- 如果使用在代码的README文件中定义的验证服务示例,那么从HTTP首部中获得的其他值将发挥作用  
UserContextHolder.getContext()
.setAuthToken(httpServletRequest.getHeader(UserContext.AUTH_TOKEN) );
UserContextHolder.getContext()
.setOrgId(httpServletRequest.getHeader(UserContext.ORG_ID) );
filterChain.doFilter(httpServletRequest, servletResponse);
}
// 没有显示空的初始化方法和销毁方法 
}
    
最终,UserContextFilter用于将我们感兴趣的HTTP首部的值映射到Java类UserContext中。
    
2.UserContext:使服务易于访问HTTP首部
    UserContext类用于保存由微服务处理的单个服务客户端请求的HTTP首部值。它由getter和setter方法组成,用于从java.lang.ThreadLocal中检索和存储值。代码清单6-9展示了UserContext类中的代码。这个类的源代码可以在licensing-service/src/main/java/com/thoughtmechanix/-licenses/utils/UserContext.java中找到。
    
代码清单6-9 将HTTP首部值存储在UserContext类中
...
现在UserContext类只是一个POJO,它保存从传入的HTTP请求中获取的值。使用一个名为UserContextHolder的类(在zuulsvr/src/main/java/com/thoughtmechanix/zuulsvr/filters/ UserContextHolder.java中)将UserContext存储在ThreadLocal变量中,该变量可以在处理用户请求的线程调用的任何方法中访问。UserContextHolder的代码如代码清单6-10所示。
   
 代码清单6-10 UserContextHolder类将UserContext存储在ThreadLocal中
 
package com.thoughtmechanix.licenses.utils;
import org.springframework.util.Assert;
 
public class UserContextHolder {
    private static final ThreadLocal<UserContext> userContext = new ThreadLocal<UserContext>();
 
    public static final UserContext getContext(){
        UserContext context = userContext.get();
 
        if (context == null) {
            context = createEmptyContext();
            userContext.set(context);
 
        }
        return userContext.get();
    }
 
    public static final void setContext(UserContext context) {
        Assert.notNull(context, "Only non-null UserContext instances are permitted");
        userContext.set(context);
    }
 
    public static final UserContext createEmptyContext(){
        return new UserContext();
    }
}
 
 
    
3.自定义RestTemplate和UserContextInteceptor:确保关联ID被传播
    我们要看的最后一段代码是UserContextInterceptor类。这个类用于将关联ID注入基于HTTP的传出服务请求中,这些服务请求由RestTemplate实例执行。这样做是为了确保可以建立服务调用之间的联系。
    
要做到这一点,需要使用一个Spring拦截器,它将被注入RestTemplate类中。让我们看看代码清单6-11中的UserContextInterceptor。
 
代码清单6-11 所有传出的微服务调用都会注入关联ID
package com.thoughtmechanix.licenses.utils;
// 为了简洁,省略了import语句
⇽--- UserContextIntercept实现了Spring框架的ClientHttpRequestInterceptor
public class UserContextInterceptor implements ClientHttpRequestInterceptor {
    private static final Logger logger = LoggerFactory.getLogger(UserContextInterceptor.class);
 
     ⇽--- intercept()方法在RestTemplate发生实际的HTTP服务调用之前被调用
    @Override
    public ClientHttpResponse intercept(
            HttpRequest request, byte[] body, ClientHttpRequestExecution execution)
            throws IOException {
 
        HttpHeaders headers = request.getHeaders();
        headers.add(UserContext.CORRELATION_ID, UserContextHolder.getContext().getCorrelationId());
        ⇽--- 为传出服务调用准备HTTP请求首部,并添加存储在UserContext中的关联ID
        headers.add(UserContext.AUTH_TOKEN, UserContextHolder.getContext().getAuthToken());
 
        return execution.execute(request, body);
    }
}   
    
为了使用UserContextInterceptor,我们需要定义一个RestTemplate bean,然后将UserContextInterceptor添加进去。为此,我们需要将自己的RestTemplate bean定义添加到licensing-service/src/main/java/com/thoughtmechanix/licenses/Application.java中的Application类中。代码清单6-12展示了添加到这个类中的方法。
 
代码清单6-12 将UserContextInterceptor添加到RestTemplate类
    ⇽--- @LoadBalanced注解表明这个RestTemplate将要使用Ribbon
    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate(){
        RestTemplate template = new RestTemplate();
        List interceptors = template.getInterceptors();
        ⇽--- 将UserContextInterceptor添加到已创建的RestTemplate实例中
        if (interceptors==null){
            template.setInterceptors(Collections.singletonList(new UserContextInterceptor()));
        }
        else{
            interceptors.add(new UserContextInterceptor());
            template.setInterceptors(interceptors);
        }
 
        return template;
    }
 
有了这个bean定义,每当使用@Autowired注解将RestTemplate注入一个类,就会使用代码清单6-12中创建的RestTemplate,它附带了UserContextInterceptor。
    
日志聚合和验证等
    既然已经将关联ID传递给每个服务,那么就可以跟踪事务了,因为关联ID流经所有涉及调用的服务。要做到这一点,需要确保每个服务都记录到一个中央日志聚合点,该聚合点将从所有服务中捕获日志条目到一个点。在日志聚合服务中捕获的每个日志条目将具有与每个条目关联的关联ID。实施日志聚合解决方案超出了本章的讨论范围,在第9章中,我们将了解如何使用Spring Cloud Sleuth。Spring Cloud Sleuth不会使用本章构建的TrackingFilter,但它将使用相同的概念——跟踪关联ID,并确保在每次调用中注入它。
 
6.6 构建接收关联ID的后置过滤器
    记住,Zuul代表服务客户端执行实际的HTTP调用。Zuul有机会从目标服务调用中检查响应,然后修改响应或以额外的信息装饰它。当与以前置过滤器捕获数据相结合时,Zuul后置过滤器是收集指标并完成与用户事务相关联的日志记录的理想场所。我们将利用这一点,通过将已经传递给微服务的关联ID注入回用户。
    我们将使用Zuul后置过滤器将关联ID注入HTTP响应首部中,该HTTP响应首部传回给服务调用者。这样,就可以将关联ID传回给调用者,而无需接触消息体。代码清单6-13展示了构建后置过滤器的代码。这段代码可以在zuulsvr/src/main/java/com/thoughtmechanix/zuulsvr/filters/ResponseFilter.java中找到。
   
 代码清单6-13 将关联ID注入HTTP响应中
// 为了简洁,省略了import语句
public class ResponseFilter extends ZuulFilter{
    private static final int  FILTER_ORDER=1;
    private static final boolean  SHOULD_FILTER=true;
    private static final Logger logger = LoggerFactory.getLogger(ResponseFilter.class);
    
    @Autowired
    FilterUtils filterUtils;
    ⇽--- 要构建一个后置过滤器,需要设置过滤器的类型为POST_FILTER_TYPE
    @Override
    public String filterType() {
        return FilterUtils.POST_FILTER_TYPE;
    }
 
    @Override
    public int filterOrder() {
        return FILTER_ORDER;
    }
 
    @Override
    public boolean shouldFilter() {
        return SHOULD_FILTER;
    }
 
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
 
        logger.debug("Adding the correlation id to the outbound headers. {}", filterUtils.getCorrelationId());
           ⇽--- 获取原始HTTP请求中传入的关联ID,并将它注入响应中  
        ctx.getResponse().addHeader(FilterUtils.CORRELATION_ID, filterUtils.getCorrelationId());
         ⇽--- 记录传出的请求URI,这样就有了“书挡”,它将显示进入Zuul的用户请求的传入和传出条目    
        logger.debug("Completing outgoing request for {}.", ctx.getRequest().getRequestURI());
 
        return null;
    }
}
 
实现完ResponseFilter之后,就可以启动Zuul服务,并通过它调用EagleEye许可证服务。服务完成后,就可以在调用的HTTP响应首部上看到一个tmx-correlation-id。图6-14展示了从调用中发回的tmx-correlation-id。
图6-14 tmx-correlation-id已被添加到发送回服务客户端的响应首部中
    到目前为止,我们所有的过滤器示例都是在路由到目的地之前或之后对服务客户端调用进行操作。对于最后一个过滤器示例,让我们看看如何动态地更改用户要到达的目标路径。
    6.7 构建动态路由过滤器
    本章要介绍的最后一个Zuul过滤器是Zuul路由过滤器。如果没有自定义的路由过滤器,Zuul将根据本章前面的映射定义来完成所有路由。通过构建Zuul路由过滤器,可以为服务客户端的调用添加智能路由。
    在本节中,我们将通过构建一个路由过滤器来学习Zuul的路由过滤器,从而允许对新版本的服务进行A/B测试。A/B测试是推出新功能的地方,在这里有一定比例的用户能够使用新功能,而其余的用户仍然使用旧服务。在本例中,我们将模拟出一个新的组织服务版本,并希望50%的用户使用旧服务,另外50%的用户使用新服务。
    为此,需要构建一个名为SpecialRoutesFilter的路由过滤器。该过滤器将接收由Zuul调用的服务的Eureka服务ID,并调用另一个名为SpecialRoutes的微服务。SpecialRoutes服务将检查内部数据库以查看服务名称是否存在。如果目标服务名称存在,它将返回服务的权重以及替代位置的目的地。SpecialRoutesFilter将接收返回的权重,并根据权重随机生成一个值,用于确定用户的调用是否将被路由到替代组织服务或Zuul路由映射中定义的组织服务。图6-15展示了使用SpecialRoutesFilter时所发生的流程。
 
 
图6-15 通过SpecialRoutesFilter调用组织服务的流程
    
在图6-15中,在服务客户端调用Zuul背后的服务时,SpecialRoutesFilter会执行以下操作。
    (1)SpecialRoutesFilter检索被调用服务的服务ID。
    (2)SpecialRoutesFilter调用SpecialRoutes服务。SpecialRoutes服务将查询是否有针对目标端点定义的替代端点。如果找到一条记录,那么这条记录将包含一个权重,它将告诉Zuul应该发送到旧服务和新服务的服务调用的百分比。
    (3)然后SpecialRoutesFilter生成一个随机数,并将它与SpecialRoutes服务返回的权重进行比较。如果随机生成的数字大于替代端点权重的值,那么SpecialRoutesFilter会将请求发送到服务的新版本。
    (4)如果SpecialRoutesFilter将请求发送到服务的新版本,Zuul会维持最初的预定义管道,并通过已定义的后置过滤器将响应从替代服务端点发送回来。
   
 6.7.1 构建路由过滤器的骨架
    本节将介绍用于构建SpecialRoutesFilter的代码。在迄今为止所看到的所有过滤器中,实现Zuul路由过滤器所需进行的编码工作最多,因为通过路由过滤器,开发人员将接管Zuul功能的核心部分——路由,并使用自己的功能替换掉它。本节不会详细介绍整个类,而会讨论相关的细节。
 
SpecialRoutesFilter遵循与其他Zuul过滤器相同的基本模式。它扩展ZuulFilter类,并设置了filterType()方法来返回“route”的值。本节不会再进一步解释filterOrder()和shouldFilter()方法,因为它们与本章前面讨论过的过滤器没有任何区别。代码清单6-14展示了路由过滤器的骨架。
    
代码清单6-14 路由过滤器的骨架
 
package com.thoughtmechanix.zuulsvr.filters;
@Component
public class SpecialRoutesFilter extends ZuulFilter {
@Override
public String filterType() {
return filterUtils.ROUTE_FILTER_TYPE;
}
@Override
public int filterOrder() {}
 
@Override
public boolean shouldFilter() {}
@Override
public Object run() {}
}
    
6.7.2 实现run()方法
SpecialRoutesFilter的实际工作从代码的run()方法开始。代码清单6-15展示了此方法的代码。
  
代码清单6-15 SpecialRoutesFilter的run()方法是工作开始的地方
   @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
         ⇽---  执行对SpecialRoutes服务的调用,以确定该服务ID是否有路由记录
        AbTestingRoute abTestRoute = getAbRoutingInfo( filterUtils.getServiceId() );
         ⇽---  useSpecialRoute()方法将会接受路径的权重,生成一个随机数,并确定是否将请求转发到替代服务  
        if (abTestRoute!=null && useSpecialRoute(abTestRoute)) {
            ⇽---  如果有路由记录,则将完整的URL(包含路径)构建到由specialroutes服务指定的服务位置     
            String route = buildRouteString(ctx.getRequest().getRequestURI(),                                                                                                                                                              abTestRoute.getEndpoint(), ctx.get("serviceId").toString());
            ⇽---  forwardToSpecialRoute()方法完成转发到其他服务的工作
            forwardToSpecialRoute(route);
        }
 
        return null;
    }
  
    代码清单6-15中代码的一般流程是,当路由请求触发SpecialRoutesFilter中的run()方法时,它将对SpecialRoutes服务执行REST调用。该服务将执行查找,并确定是否存在针对被调用的目标服务的Eureka服务ID的路由记录。对SpecialRoutes服务的调用是在getAbRoutingInfo()方法中完成的。getAbRoutingInfo()方法如代码清单6-16所示。
    
代码清单6-16 调用SpecialRouteservice以查看路由记录是否存在
    
private AbTestingRoute getAbRoutingInfo(String serviceName){
        ResponseEntity<AbTestingRoute> restExchange = null;
        try {
            ⇽---  调用SpecialRoutesService端点  
            restExchange = restTemplate.exchange(
                             "http://specialroutesservice/v1/route/abtesting/{serviceName}",
                             HttpMethod.GET,
                             null, AbTestingRoute.class, serviceName);
        }
        ⇽---  如果路由服务没有找到记录(它将返回HTTP状态码404),该方法将返回空值    
        catch(HttpClientErrorException ex){
            if (ex.getStatusCode()== HttpStatus.NOT_FOUND) return null;
            throw ex;
        }
        return restExchange.getBody();
    }
 
    
一旦确定目标服务的路由记录存在,就需要确定是否应该将目标服务请求路由到替代服务位置,或者路由到由 Zuul 路由映射静态管理的默认服务位置。为了做出这个决定,需要调用useSpecialRoute()方法。代码清单6-17展示了这个方法。
    
代码清单6-17 决定是否使用替代服务路由
 
public boolean useSpecialRoute(AbTestingRoute testRoute){
        Random random = new Random();
 
        if (testRoute.getActive().equals("N")) return false;
         ⇽---  确定是否应该使用替代服务路由
        int value = random.nextInt((10 - 1) + 1) + 1;
 
        if (testRoute.getWeight()<value) return true;
 
        return false;
    }    
这个方法做了两件事。首先,该方法检查从SpecialRoutes服务返回的AbTestingRoute记录中的active字段。如果该记录设置为"N",则useSpecialRoute()方法不应该执行任何操作,因为现在不希望进行任何路由。其次,该方法生成1到10之间的随机数。然后,该方法将检查返回路由的权重是否小于随机生成的数。如果条件为true,则useSpecialRoute()方法将返回true,表示确实希望使用该路由。
    
一旦确定要路由进入SpecialRoutesFilter的服务请求,就需要将请求转发到目标服务。
    
6.7.3 转发路由
    SpecialRoutesFilter中出现的大部分工作是到下游服务的路由的实际转发。虽然Zuul确实提供了辅助方法来使这项任务更容易,但开发人员仍然需要负责大部分工作。forwardToSpecialRoute()方法负责转发工作。该方法中的代码大量借鉴了Spring Cloud的SimpleHostRoutingFilter类的源代码。虽然本章不会介绍forwardToSpecialRoute()方法中调用的所有辅助方法,但是会介绍该方法中的代码,如代码清单6-18所示。
    
代码清单6-18 forwardToSpecialRoute调用替代服务
⇽--- helper变量是类ProxyRequestHelper类型的一个实例变量。这是Spring Cloud提供的类,带有用于代理服务请求的辅助方法
private ProxyRequestHelper helper= new ProxyRequestHelper ();    
 
private void forwardToSpecialRoute(String route) {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        ⇽--- 创建将发送到服务的所有HTTP请求首部的副本
        MultiValueMap<String, String> headers = this.helper.buildZuulRequestHeaders(request);
         ⇽--- 创建所有HTTP请求参数的副本
        MultiValueMap<String, String> params = this.helper.buildZuulRequestQueryParams(request);
        String verb = getVerb(request);
        ⇽--- 创建将被转发到替代服务的HTTP主体的副本
        InputStream requestEntity = getRequestBody(request);
        if (request.getContentLength() < 0) {
            context.setChunkedRequestBody();
        }
 
        this.helper.addIgnoredHeaders();
        CloseableHttpClient httpClient = null;
        HttpResponse response = null;
 
        try {
            httpClient  = HttpClients.createDefault();
             ⇽--- 使用forward()辅助方法(未显示)调用替代服务
            response = forward(httpClient, verb, route, request, headers, params, requestEntity);
            ⇽--- 通过setResponse()辅助方法将服务调用的结果保存回Zuul服务器
            setResponse(response);
        }
        catch (Exception ex ) {
            ex.printStackTrace();
 
        }
        finally{
            try {
                httpClient.close();
            }
            catch(IOException ex){} // 为了简洁,省略了其余的代码
        }
    }    
    代码清单6-18中的关键要点是,我们将传入的HTTP请求(首部参数、HTTP动词和主体)中的所有值复制到将在目标服务上调用的新请求。然后forwardToSpecialRoute()方法从目标服务返回响应,并将响应设置在Zuul使用的HTTP请求上下文中。上述过程通过setResponse()辅助方法(未显示)完成。Zuul使用HTTP请求上下文从调用服务客户端返回响应。
    6.7.4 整合
    既然已经实现了SpecialRoutesFilter,我们就可以通过调用许可证服务来查看它的动作。读者可能还记得,在前面的几章中,许可证服务调用组织服务来检索组织的联系人数据。
    在代码示例中,specialroutesservice具有用于组织服务的数据库记录,该数据库记录指示有50%的概率把对组织服务的请求路由到现有的组织服务(Zuul中映射的那个),50%的概率路由到替代组织服务。从SpecialRoutes服务返回的替代组织服务路径是http://orgservice-new,并且不能直接从Zuul访问。为了区分这两个服务,我修改了组织服务,将文本“OLD::”和“NEW::”添加到组织服务返回的联系人姓名的前面。
    如果现在通过Zuul访问许可证服务端点,应该看到从许可证服务调用返回的contactName在OLD::和NEW::值之间变化。
 
http://localhost:5555/api/licensing/v1/organizations/e254f8c-c442-4ebe-a82a- ➥  e2fc1d1ff78a/licenses/f3831f8c-c338-4ebe-a82a-e2fc1d1ff78a
    
图6-16展示了这一点。
 
图6-16 当访问替代组织服务时,将会看到NEW被添加到contactName前面
    实现Zuul路由过滤器确实比实现前置过滤器或后置过滤器需要更多的工作,但它也是Zuul最强大的部分之一,因为开发人员可以轻松地让服务路由方式变得智能。
    6.8 小结
    Spring Cloud使构建服务网关变得十分简单。
    Zuul服务网关与Netflix的Eureka服务器集成,可以自动将通过Eureka注册的服务映射到Zuul路由。
    Zuul可以对所有正在管理的路由添加前缀,因此可以轻松地给路由添加/api之类的前缀。
    可以使用Zuul手动定义路由映射。这些路由映射是在应用程序配置文件中手动定义的。
    通过使用Spring Cloud Config服务器,可以动态地重新加载路由映射,而无须重新启动Zuul服务器。
    可以在全局和个体服务水平上定制Zuul的Hystrix和Ribbon的超时。
 
Zuul允许通过Zuul过滤器实现自定义业务逻辑。Zuul有3种类型的过滤器,即前置过滤器、后置过滤器和路由过滤器。
    Zuul前置过滤器可用于生成一个关联ID,该关联ID可以注入流经Zuul的每个服务中。
    Zuul后置过滤器可以将关联ID注入服务客户端的每个HTTP服务响应中。
    自定义Zuul路由过滤器可以根据Eureka服务ID执行动态路由,以便在同一服务的不同版本之间进行A/B测试。
 
 
posted @ 2019-12-03 10:51  mongotea  阅读(428)  评论(0编辑  收藏  举报