Zuul网关

1.介绍及使用

Zuul 是 Netflix OSS 中的一员,是一个基于 JVM 路由和服务端的负载均衡器。提供路由、监控、弹性、安全等方面的服务框架。Zuul 能够与 Eureka、Ribbon、Hystrix 等组件配合使用。
Zuul 的核心是过滤器,通过这些过滤器我们可以扩展出很多功能,比如:

  1. 动态路由
    动态地将客户端的请求路由到后端不同的服务,做一些逻辑处理,比如聚合多个服务的数据返回。
  2. 请求监控
    可以对整个系统的请求进行监控,记录详细的请求响应日志,可以实时统计出当前系统的访问量以及监控状态。
  3. 认证鉴权
    对每一个访问的请求做认证,拒绝非法请求,保护好后端的服务。
  4. 压力测试
    压力测试是一项很重要的工作,像一些电商公司需要模拟更多真实的用户并发量来保证重大活动时系统的稳定。通过 Zuul 可以动态地将请求转发到后端服务的集群中,还可以识别测试流量和真实流量,从而做一些特殊处理。
  5. 灰度发布
    灰度发布可以保证整体系统的稳定,在初始灰度的时候就可以发现、调整问题,以保证其影响度。

2. 使用Zuul构建微服务网关

,在 pom.xml 中增加 Spring Cloud 项目的依赖,然后加入 Zuul 的依赖,代码如下所示。

 <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
    </dependencies>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

属性文件中增加配置信息:

server:
  port: 8088
spring:
  application:
    name: zuul-demo

zuul:
  routes:
    wen:
      path: /user/**
      url: https://www.baidu.com/

启动类的代码如下所示:

@SpringBootApplication
@EnableEurekaClient
@EnableZuulProxy
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class,args);
    }
}

通过 zuul.routes 来配置路由转发,user 是自定义的名称,当访问 user/** 开始的地址时,就会跳转到 https://www.baidu.com/ 上。比如输入地址为“http://localhost:8088/user/abc”

2.1 集成 Eureka

通过对上部分的学习,我们已经可以简单地使用 Zuul 进行路由的转发了,在实际使用中我们通常是用 Zuul 来代理请求转发到内部的服务上去,统一为外部提供服务。

内部服务的数量会很多,而且可以随时扩展,我们不可能每增加一个服务就改一次路由的配置,所以也得通过结合 Eureka 来实现动态的路由转发功能。

首先需要添加 Eureka 的依赖,代码如下所示。

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

启动类不需要修改,因为 @EnableZuulProxy 已经自带了 @EnableDiscoveryClient。只需要在配置文件中增加 Eureka 的地址即可:

eureka:
	client:
		service-url:
		  defaultZone:        http://admin:admin@localhost:8080/eureka/,http://admin:admin@localhost:9000/eureka/

重启服务,我们可以通过默认的转发规则来访问 Eureka 中的服务。访问规则是“API 网关地址+访问的服务名称+接口 URI”。

3 Zuul路由配置详解

在给服务指定名称的时候,应尽量短一点,这样的话我们就可以用默认的路由规则进行请求,不需要为每个服务都定一个路由规则,这样就算新增了服务,API 网关也不用修改和重启了。
默认规则举例:

  • API 网关地址:http://localhost:8088
  • 用户服务名称:user-service。
  • 用户登录接口:/user/login。
    那么通过 Zuul 访问登录接口的规则就是 http://localhost:8088/user-service/user/login。
3.1 指定具体服务路由

我们可以为每一个服务都配置一个路由转发规则:

zuul.routes.fsh-house.path=/api-house/**

上述代码将 fsh-house 服务的路由地址配置成了 api-house,也就是当需要访问 fsh-house 中的接口时,我们可以通过 api-house/house/hello 来进行。这其实就是将服务名称变成了我们自定义的名称。

有的时候服务名称太长了,放在 URL 中不太友好,我们希望它变得更友好一点,就可以这么去配置。这里的 api-house/** 后面一定要配置两个星号,两个星号表示可以转发任意层级的 URL,比如“/api-house/house/1”。如果只配置一个星号,那么就只能转发一级,比如“/api-house/house”。

3.2 路由前缀

有的时候我们会想在 API 前面配置一个统一的前缀,比如像 http://c.biancheng.net/user/login 这样登录接口,如果想将其变成 http://c.biancheng.net/rest/user/login,即在每个接口前面加一个 rest,此时我们就可以通过 Zuul 中的配置来实现:

zuul.prefix=/rest
3.3 本地跳转

Zuul 的 API 路由还提供了本地跳转功能,通过 forward 就可以实现。

zuul.routes.fsh-substitution.path=/api/**
zuul.routes.fsh-substitution.url=forward:/local

当我们想在访问 api/1 的时候会路由到本地的 local/1 上去,就可以参照上述代码实现。local 是本地接口需要我们自行添加,因此我们要建一个 Controller,代码如下所示。

@RestController
public class LocalController {
    @GetMapping("/local/{id}")
    public String local(@PathVariable String id) {
        return id;
    }
}

然后访问 http://localhost:2103/api/1 就可以看到我们想要的返回结果了

4 Zuul过滤器介绍及使用(传递数据、拦截请求和异常处理)

Zuul 可以实现很多高级的功能,比如限流、认证等。想要实现这些功能,必须要基于 Zuul 给我们提供的核心组件“过滤器”。下面我们一起来了解一下 Zuul 的过滤器。

4.1 过滤器类型

Zuul 中的过滤器跟我们之前使用的 javax.servlet.Filter 不一样,javax.servlet.Filter 只有一种类型,可以通过配置 urlPatterns 来拦截对应的请求。
而 Zuul 中的过滤器总共有 4 种类型,且每种类型都有对应的使用场景。

4.1.1 pre

可以在请求被路由之前调用。适用于身份认证的场景,认证通过后再继续执行下面的流程。

4.1.2 route

在路由请求时被调用。适用于灰度发布场景,在将要路由的时候可以做一些自定义的逻辑。

4.1.3 post

在 route 和 error 过滤器之后被调用。这种过滤器将请求路由到达具体的服务之后执行。适用于需要添加响应头,记录响应日志等应用场景。

4.1.4 error

处理请求时发生错误时被调用。在执行过程中发送错误时会进入 error 过滤器,可以用来统一记录错误信息。

4.2 请求生命周期

可以通过图 看出整个过滤器的执行生命周期,此图来自 Zuul GitHub wiki 主页。
image
通过上面的图可以清楚地知道整个执行的顺序,请求发过来首先到 pre 过滤器,再到 routing 过滤器,最后到 post 过滤器,任何一个过滤器有异常都会进入 error 过滤器。
通过 com.netflix.zuul.http.ZuulServlet 也可以看出完整执行顺序,ZuulServlet 类似 Spring-Mvc 的 DispatcherServlet,所有的 Request 都要经过 ZuulServlet 的处理。
ZuulServlet 源码如下所示:

@Override
public void service(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse)
        throws ServletException, IOException {
    try {
        init((HttpServletRequest) servletRequest, (HttpServletResponse) servletResponse);
        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();
    }
}
4.3 使用过滤器

我们创建一个 pre 过滤器,来实现 IP 黑名单的过滤操作,代码如下所示。

public class IpFilter extends ZuulFilter {
    // IP黑名单列表
    private List<String> blackIpList = Arrays.asList("127.0.0.1");
    public IpFilter() {
        super();
    }
    @Override
    public boolean shouldFilter() {
        return true
    }
    @Override
    public String filterType() {
        return "pre";
    }
    @Override
    public int filterOrder() {
        return 1;
    }
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        String ip = IpUtils.getIpAddr(ctx.getRequest());
        // 在黑名单中禁用
        if (StringUtils.isNotBlank(ip) && blackIpList.contains(ip)) {
            ctx.setSendZuulResponse(false);
            ResponseData data = ResponseData.fail("非法请求 ", ResponseCode.NO_AUTH_CODE.getCode());
            ctx.setResponseBody(JsonUtils.toJson(data));
            ctx.getResponse().setContentType("application/json; charset=utf-8");
            return null;
        }
        return null;
    }
}

由代码可知,自定义过滤器需要继承 ZuulFilter,并且需要实现下面几个方法:

  1. shouldFilter
    是否执行该过滤器,true 为执行,false 为不执行,这个也可以利用配置中心来实现,达到动态的开启和关闭过滤器。
  2. filterType
    过滤器类型,可选值有 pre、route、post、error。
  3. filterOrder
    过滤器的执行顺序,数值越小,优先级越高。
  4. run
    执行自己的业务逻辑,本段代码中是通过判断请求的 IP 是否在黑名单中,决定是否进行拦截。blackIpList 字段是 IP 的黑名单,判断条件成立之后,通过设置 ctx.setSendZuulResponse(false),告诉 Zuul 不需要将当前请求转发到后端的服务了。通过 setResponseBody 返回数据给客户端。
    过滤器定义完成之后我们需要配置过滤器才能生效,IP 过滤器配置代码如下所示。
@Configuration
public class FilterConfig {
    @Bean
    public IpFilter ipFilter() {
        return new IpFilter();
    }
}

4.4 过滤器禁用

有的场景下,我们需要禁用过滤器,此时可以采取下面的两种方式来实现:

  • 利用 shouldFilter 方法中的 return false 让过滤器不再执行
  • 通过配置方式来禁用过滤器,格式为“zuul. 过滤器的类名.过滤器类型 .disable=true”。如果我们需要禁用“使用过滤器”部分中的 IpFilter,可以用下面的配置:
 ipFilter:
    pre:
      disabled: true
4.5 过滤器中传递数据

项目中往往会存在很多的过滤器,执行的顺序是根据 filterOrder 决定的,那么肯定有一些过滤器是在后面执行的,如果你有这样的需求:第一个过滤器需要告诉第二个过滤器一些信息,这个时候就涉及在过滤器中怎么去传递数据给后面的过滤器。
实现这种传值的方式笔者第一时间就想到了用 ThreadLocal,既然我们用了 Zuul,那么 Zuul 肯定有解决方案,比如可以通过 RequestContext 的 set 方法进行传递,RequestContext 的原理就是 ThreadLocal。

RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("msg", "你好吗");

后面的过滤就可以通过 RequestContext 的 get 方法来获取数据:

RequestContext ctx = RequestContext.getCurrentContext();
ctx.get("msg");

上面我们说到 RequestContext 的原理就是 ThreadLocal,这不是笔者自己随便说的,而是笔者看过源码得出来的结论,下面请看源码,代码如下所示。

protected 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;
}

4.6 过滤器拦截请求

在过滤器中对请求进行拦截是一个很常见的需求,本节的“使用过滤器”部分中讲解的 IP 黑名单限制就是这样的一个需求。如果请求在黑名单中,就不能让该请求继续往下执行,需要对其进行拦截并返回结果给客户端。
拦截和返回结果只需要 5 行代码即可实现,代码如下所示。

RequestContext ctx = RequestContext.getCurrentContext();
ctx.setSendZuulResponse(false);
ctx.set("sendForwardFilter.ran", true);
ctx.setResponseBody("返回信息");
return null;

ctx.setSendZuulResponse(false) 告诉 Zuul 不需要将当前请求转发到后端的服务。原理体现在 shouldFilter() 方法上,源码在 org.springframework.cloud.netflix.zuul.filters.route.RibbonRoutingFilter 中的 shouldFilter() 方法里,代码如下所示。

@Override
public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    return (ctx.getRouteHost() == null && ctx.get(SERVICE_ID_KEY) != null && ctx.sendZuulResponse());
}

代码“ctx.set("sendForwardFilter.ran",true);”是用来拦截本地转发请求的,当我们配置了 forward:/local 的路由,ctx.setSendZuulResponse(false) 对 forward 是不起作用的,需要设置 ctx.set("sendForwardFilter.ran",true) 才行。
对应实现的源码体现在 org.springframework.cloud.netflix.zuul.filters.route.SendForwardFilter的shouldFilter() 方法中,代码如下所示。

protected static final String SEND_FORWARD_FILTER_RAN = "sendForwardFilter.ran";
@Override
public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    return ctx.containsKey(FORWARD_TO_KEY) && !ctx.getBoolean(SEND_FORWARD_FILTER_RAN, false);
}

到这一步之后,当前的过滤器中确实将请求进行拦截了,并且可以给客户端返回信息。但是当你的项目中有多个过滤器的时候,假如你需要过滤的那个过滤器是第一个执行的,发现非法请求,然后进行拦截,以笔者之前使用 javax.servlet.Filter 的经验,进行拦截之后,在 chain.doFilter 之前进行返回就可以让过滤器不往下执行了。
但是 Zuul 中的过滤器不一样,即使你刚刚通过 ctx.setSendZuulResponse(false) 设置了不路由到服务,并且返回 null,那只是当前的过滤器执行完成了,后面还有很多过滤器在等着执行。

通过源码可以看出,Zuul 中 Filter 的执行逻辑如下:在 ZuulServlet 中的 service 方法中执行对应的 Filter,比如 preRoute()。preRoute 中会通过 ZuulRunner 来执行(代码如下所示)。

void preRoute() throws ZuulException {
    zuulRunner.preRoute();
}

zuulRunner 中通过调用 FilterProcessor 来执行 Filter(代码如下所示)。

public void preRoute() throws ZuulException {
    FilterProcessor.getInstance().preRoute();
}

FilterProcessor 通过过滤器类型获取所有过滤器,并循环执行(代码如下所示)。

public Object runFilters(String sType) throws Throwable {
    if (RequestContext.getCurrentContext().debugRouting()) {
        Debug.addRoutingDebug("Invoking {" + sType + "} type filters");
    }
    boolean bResult = false;
    List<ZuulFilter> list = FilterLoader.getInstance().getFiltersByType(sType);
    if (list != null) {
        for (int i = 0; i < list.size(); i++) {
            ZuulFilter zuulFilter = list.get(i);
            Object result = processZuulFilter(zuulFilter);
            if (result != null && result instanceof Boolean) {
                bResult |= ((Boolean) result);
            }
        }
    }
    return bResult;
}

通过上面的讲解,我们大致知道了为什么所有的过滤器都会执行,解决这个问题的办法就是通过 shouldFilter 来处理,即在拦截之后通过数据传递的方式告诉下一个过滤器是否要执行。
改造上面的拦截代码,增加一行数据传递的代码:

ctx.set("isSuccess", false);

在 RequestContext 中设置一个值来标识是否成功,当为 true 的时候,后续的过滤器才执行,若为 false 则不执行。
利用这种方法,在后面的过滤器就需要用到这个值来决定自己此时是否需要执行,此时只需要在 shouldFilter 方法中加上如下所示的代码即可。

public boolean shouldFilter() {
    RequestContext ctx = RequestContext.getCurrentContext();
    Object success = ctx.get("isSuccess");
    return success == null ? true : Boolean.parseBoolean(success.toString());
}
4.7 过滤器中异常处理

对于异常来说,无论在哪个地方都需要处理。过滤器中的异常主要发生在 run 方法中,可以用 try catch 来处理。Zuul 中也为我们提供了一个异常处理的过滤器,当过滤器在执行过程中发生异常,若没有被捕获到,就会进入 error 过滤器中。
我们可以定义一个 error 过滤器来记录异常信息,代码如下所示。

public class ErrorFilter extends ZuulFilter {
    Logger logger = LoggerFactory.getLogger(ErrorFilter.class);
    @Override
    public String filterType() {
        return "error";
    }

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

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext context = RequestContext.getCurrentContext();
        Throwable throwable = context.getThrowable();
        logger.error("Filter errore:{}",throwable.getCause().getMessage());
        return null;
    }
}

然后我们在其他过滤器中模拟一个异常信息,改造本节“使用过滤器”部分中的 IpFilter 代码,在 run 方法中增加下面的代码来模拟 java.lang.ArithmeticException:/by zero。
System.out.println(2/0);
访问我们的服务接口可以看到500 错误信息表示控制台也有异常日志输出。

我们后端的接口服务都是 REST 风格的API,返回的数据都有固定的 Json 格式,现在变成这样一个页面了,让客户端那边怎么处理?我们通过实现 ErrorController 来解决这个问题。

@RestController
public class ErrorController implements org.springframework.boot.web.servlet.error.ErrorController {

    @Autowired
    private ErrorAttributes errorAttributes;


    @Override
    public String getErrorPath() {
        return "/error";
    }

    @RequestMapping("error")
    public ResponseData error(HttpServletRequest request){
        Map<String,Object> errorAttr = getErrorAttributes(request);
        String message = (String) errorAttr.get("message");
        String trace = (String) errorAttr.get("trace");
        if(StringUtils.isNotBlank(trace)){
            message+=String.format("and trace %s",trace);
        }
        return ResponseData.fail(message,500);


    }

    private Map<String, Object> getErrorAttributes(HttpServletRequest request) {
        return errorAttributes.getErrorAttributes(new ServletWebRequest(request),true);
    }

}

我们再次访问之前的接口,这次就不是一个错误页面了,而是我们固定好的 Json 格式的数据

之前我们讲解过 Spring Boot 中统一进行异常处理的办法,也就是把页面的错误转换成了统一的 Json 格式数据返回给调用方,为什么这里还要用另一种办法来实现呢?

因为 @ControllerAdvice 注解主要用来针对 Controller 中的方法做处理,作用于 @RequestMapping 标注的方法上,只对我们定义的接口异常有效,在 Zuul 中是无效的。

5 使用Zuul实现容错回退功能

Zuul 主要功能就是转发,在转发过程中我们无法保证被转发的服务是可用的,这个时候就需要容错机制及回退机制。

5.1 容错机制

容错,简单来说就是当某个服务不可用时,能够切换到其他可用的服务上去,也就是需要有重试机制。在 Zuul 中开启重试机制需要依赖 spring-retry。
首先在 pom.xml 中添加 spring-retry 的依赖,代码如下所示。

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

在属性文件中开启重试机制以及配置重试次数:

zuul.retryable=true
ribbon.connectTimeout=500
ribbon.readTimeout=5000
ribbon.maxAutoRetries=1
ribbon.maxAutoRetriesNextServer=3
ribbon.okToRetryOnAllOperations=true
ribbon.retryableStatusCodes=500,404,502

其中:

  • zuul.retryable:开启重试。
  • ribbon.connectTimeout:请求连接的超时时间(ms)。
  • ribbon.readTimeout:请求处理的超时时间(ms)。
  • ribbon.maxAutoRetries:对当前实例的重试次数。
  • ribbon.maxAutoRetriesNextServer:切换实例的最大重试次数。
  • ribbon.okToRetryOnAllOperations:对所有操作请求都进行重试。
  • ribbon.retryableStatusCodes:对指定的 Http 响应码进行重试。

可以启动两个 hystrix-feign-demo 服务,默认 Ribbon 的转发规则是轮询,然后我们停掉一个 hystrix-feign-demo 服务。没加重试机制之前,当你请求接口的时候肯定有一次是会被转发到停掉的服务上去的,返回的是异常信息。

当我们加入了重试机制后,你可以循环请求接口,这个时候不会返回异常信息,因为 Ribbon 会根据重试配置进行重试,当请求失败后会将请求重新转发到可用的服务上去。

5.2 回退机制

在 Spring Cloud 中,Zuul 默认整合了 Hystrix,当后端服务异常时可以为 Zuul 添加回退功能,返回默认的数据给客户端。
实现回退机制需要实现 FallbackProvider 接口,代码如下所示。

@Component
public class ServiceConsumerFallbackProvider implements FallbackProvider {

    Logger log = LoggerFactory.getLogger(ServiceConsumerFallbackProvider.class);

    @Override
    public String getRoute() {
        return "*";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                return HttpStatus.OK;

            }

            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }

            @Override
            public String getStatusText() throws IOException {
                return this.getStatusCode().getReasonPhrase();
            }

            @Override
            public void close() {

            }

            @Override
            public InputStream getBody() throws IOException {
                if(cause!=null){
                    log.error("",cause.getCause());
                }
                RequestContext context = RequestContext.getCurrentContext();
                ResponseData data = ResponseData.fail("服务器内部出错",500);
                return new ByteArrayInputStream(JSONObject.toJSONBytes(data));
            }

            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders httpHeaders = new HttpHeaders();
                MediaType mediaType = new MediaType("application","json", Charset.forName("UTF-8"));
                httpHeaders.setContentType(mediaType);
                return httpHeaders;
            }
        };

    }
}

getRoute 方法中返回*表示对所有服务进行回退操作,如果只想对某个服务进行回退,那么就返回需要回退的服务名称,这个名称一定要是注册到 Eureka 中的名称。

通过 ClientHttpResponse 构造回退的内容。通过 getStatusCode 返回响应的状态码。通过 getStatusText 返回响应状态码对应的文本。通过 getBody 返回回退的内容。通过 getHeaders 返回响应的请求头信息。

通过 API 网关来访问 hystrix-feign-demo 服务,将 hystrix-feign-demo 服务停掉,然后再次访问,就可以看到回退的内容了

5.3 Zuul 高可用

跟业务相关的服务我们都是注册到 Eureka 中,通过 Ribbon 来进行负载均衡,服务可以通过水平扩展来实现高可用。

现实使用中,API 网关这层往往是给 APP、Webapp、客户来调用接口的,如果我们将 Zuul 也注册到 Eureka 中是达不到高可用的,因为你不可能让你的客户也去操作你的注册中心。

这时最好的办法就是用额外的负载均衡器来实现 Zuul 的高可用,比如我们最常用的 Nginx,或者 HAProxy、F5 等。

这种方式也是单体项目最常用的负载方式,当用户请求一个地址的时候,通过 Nginx 去做转发,当一个服务挂掉的时候,Nginx 会把它排除掉。

如果想要 API 网关也能随时水平扩展,那么我们可以用脚本来动态修改 Nginx 的配置,通过脚本操作 Eureka,发现有新加入的网关服务或者下线的网关服务,直接修改 Nginx 的 upstream,然后通过重载(reload)配置来达到网关的动态扩容。

如果不用脚本结合注册中心去做的话,就只能提前规划好 N 个节点,然后手动配置上去。

6 查看Zuul的路由端点和过滤器信息

6.1 /routes 端点

当 @EnableZuulProxy 与 Spring Boot Actuator 配合使用时,Zuul 会暴露一个路由管理端点 /routes。借助这个端点,可以方便、直观地查看以及管理 Zuul 的路由。

将所有端点都暴露出来,在 application.properties 配置文件中增加以下配置:

management:
  endpoints:
    web:
      exposure:
        include:  '*'

访问 http://localhost:8088/actuator/routes 可以显示所有路由信息,如图 所示。
image

6.2 /filters 端点

/fliters 端点会返回 Zuul 中所有过滤器的信息。可以清楚地了解 Zuul 中目前有哪些过滤器,哪些过滤器被禁用了等详细信息。
访问 http://localhost:8088/actuator/filters 可以显示所有过滤器信息,如图 所示。
image

7 Zuul请求响应信息输出

系统在生产环境出现问题时,排查问题最好的方式就是查看日志了,日志的记录尽量详细,这样你才能快速定位问题。

下面带大家学习如何在 Zuul 中输出请求响应的信息来辅助我们解决一些问题。

熟悉 Zuul 的朋友都知道,Zuul 中有 4 种类型过滤器,每种都有特定的使用场景,要想记录响应数据,那么必须是在请求路由到了具体的服务之后,返回了才有数据,这种需求就适合用 post 过滤器来实现了。代码如下所示。

HttpServletRequest req = (HttpServletRequest) RequestContext.getCurrentContext().getRequest();
System.err.println("REQUEST:: " + req.getScheme() + " " + req.getRemoteAddr() + ":" + req.getRemotePort());
StringBuilder params = new StringBuilder("?");
// 获取URL参数
Enumeration<String> names = req.getParameterNames();
if (req.getMethod().equals("GET")) {
    while (names.hasMoreElements()) {
        String name = (String) names.nextElement();
        params.append(name);
        params.append("=");
        params.append(req.getParameter(name));
        params.append("&");
    }
}
if (params.length() > 0) {
    params.delete(params.length() - 1, params.length());
}
System.err.println(
        "REQUEST:: > " + req.getMethod() + " " + req.getRequestURI() + params + " " + req.getProtocol());
Enumeration<String> headers = req.getHeaderNames();
while (headers.hasMoreElements()) {
    String name = (String) headers.nextElement();
    String value = req.getHeader(name);
    System.err.println("REQUEST:: > " + name + ":" + value);
}
final RequestContext ctx = RequestContext.getCurrentContext();
// 获取请求体参数
if (!ctx.isChunkedRequestBody()) {
    ServletInputStream inp = null;
    try {
        inp = ctx.getRequest().getInputStream();
        String body = null;
        if (inp != null) {
            body = IOUtils.toString(inp);
            System.err.println("REQUEST:: > " + body);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

获取响应内容的第一种方式,代码如下所示。

try {
    Object zuulResponse = RequestContext.getCurrentContext().get("zuulResponse");
    if (zuulResponse != null) {
        RibbonHttpResponse resp = (RibbonHttpResponse) zuulResponse;
        String body = IOUtils.toString(resp.getBody());
        System.err.println("RESPONSE:: > " + body);
        resp.close();
        RequestContext.getCurrentContext().setResponseBody(body);
    }
} catch (IOException e) {
    e.printStackTrace();
}

获取响应内容的第二种方式,代码如下所示。

public static void main(String[] args) {
    InputStream stream = RequestContext.getCurrentContext().getResponseDataStream();
    try {
        if (stream != null) {
            String body = IOUtils.toString(stream);
            System.err.println("RESPONSE:: > " + body);
            RequestContext.getCurrentContext().setResponseBody(body);
        }
    } catch (IOException e) {
        e.printStackTrace();
    }
}

为什么上面两种方式可以取到响应内容?
在 RibbonRoutingFilter 或者 SimpleHostRoutingFilter 中可以看到下面一段代码,代码如下所示。

public Object run() {
    RequestContext context = RequestContext.getCurrentContext();
    this.helper.addIgnoredHeaders();
    try {
        RibbonCommandContext commandContext = buildCommandContext(context);
        ClientHttpResponse response = forward(commandContext);
        setResponse(response);
        return response;
    } catch (ZuulException ex) {
        throw new ZuulRuntimeException(ex);
    } catch (Exception ex) {
        throw new ZuulRuntimeException(ex);
    }
}

forward() 方法对服务调用,拿到响应结果,通过 setResponse() 方法进行响应的设置,代码如下所示。

protected void setResponse(ClientHttpResponse resp) throws ClientException, IOException {
    RequestContext.getCurrentContext().set("zuulResponse", resp);
    this.helper.setResponse(resp.getStatusCode().value(), resp.getBody() == null ? null : resp.getBody(),
            resp.getHeaders());
}

上面第一行代码就可以解释我们的第一种获取的方法,这里直接把响应内容加到了 RequestContext 中。
第二种方式的解释就在 helper.setResponse 的逻辑里面了,代码如下所示。

public void setResponse(int status, InputStream entity, MultiValueMap<String, String> headers) throws IOException {
    RequestContext context = RequestContext.getCurrentContext();
    context.setResponseStatusCode(status);
    if (entity != null) {
        context.setResponseDataStream(entity);
    }
    // .....
}

8 实现Zuul自带的Debug功能

Zuul 中自带了一个 DebugFilter,一开始笔者也没明白这个 DebugFilter 有什么用,看名称很容易理解,它是用来调试的,可是你看它的源码几乎没什么逻辑,就 set 了两个值而已,代码如下所示。

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    ctx.setDebugRouting(true);
    ctx.setDebugRequest(true);
    return null;
}

要想让这个过滤器执行就得研究一下它的 shouldFilter() 方法,代码如下所示。

@Override
public boolean shouldFilter() {
    HttpServletRequest request = RequestContext.getCurrentContext().getRequest();
    if ("true".equals(request.getParameter(DEBUG_PARAMETER.get()))) {
        return true;
    }
    return ROUTING_DEBUG.get();
}

只要满足两个条件中的任何一个就可以开启这个过滤器。
第一个条件是请求参数中带了“某个参数 =true”就可以开启,这个参数名是通过下面的代码获取的,代码如下所示。

private static final DynamicStringProperty DEBUG_PARAMETER = DynamicPropertyFactory.getInstance()
            .getStringProperty(ZuulConstants.ZUUL_DEBUG_PARAMETER, "debug");

posted @ 2021-06-19 10:00  风飘落叶  阅读(712)  评论(0编辑  收藏  举报