SpringCloud系列8:一文搞定Zuul路由网关及源码解析

文章目录

      • 1、概述
      • 2、路由基本配置
      • 3、测试
      • 4、路由访问映射规则
        • 代理名称替换真实的名称
      • 5、Filter工作原理
        • Zuul中的Filter
        • Filter Types
        • Zuul请求生命周期
        • 自定义一个Filter
        • Filter的启用与禁用
      • 6、Zuul部分源码分析
      • 7、Zuul的容错与回退
      • 8、高可用

1、概述

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

路由在微服务体系结构的一个组成部分。例如一个电影售票系统,/可以映射到您的Web应用程序,/api/users映射到用户微服务,将/api/classic映射到分类微服务,将/api/pay映射到支付微服务。Zuul是Netflix的基于JVM的路由器和服务器端负载均衡器。
SpringCloud系列8:一文搞定Zuul路由网关及源码解析_第1张图片
Zuul包含了对请求的路由转发和过滤的主要功能:

  • 路由功能

    主要负责将外部请求转发到具体的微服务上,是实现外部访问入口的基础

  • 过滤功能

    负责对请求的处理过程进行干预,完成一系列的横切功能,例如权限校验、限流以及监控等

Zuul与Eureka进行整合,将Zuul自身注册为Eureka服务治理下的应用,同时从Eureka中获得其他微服务的信息,也即以后的微服务访问都是通过Zuul跳转后获得。

三大功能:代理、路由、过滤

github地址:https://github.com/Netflix/zuul/

2、路由基本配置

  • 新建lingluocloud-zuul-gateway-9527模块

  • pom文件


<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0modelVersion>

    <groupId>com.lingluo.springcloudgroupId>
    <artifactId>lingluocloudartifactId>
    <version>1.0-SNAPSHOTversion>
    <modules>
        <module>lingluocloud-apimodule>
        <module>lingluocloud-provider-dept-8001module>
        <module>lingluocloud-consumer-dept-80module>
        <module>lingluocloud-eureka-7001module>
        <module>lingluocloud-eureka-7002module>
        <module>lingluocloud-eureka-7003module>
        <module>lingluocloud-provider-dept-8002module>
        <module>lingluocloud-provider-dept-8003module>
        <module>lingluocloud-consumer-dept-feignmodule>
        <module>lingluocloud-provider-dept-hystrix-8001module>
        <module>lingluocloud-consumer-hystrix-dashboardmodule>
        <module>lingluocloud-zuul-gateway-9527module>
    modules>
    <packaging>pompackaging>
    <properties>
        <project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
        <maven.compiler.source>1.8maven.compiler.source>
        <maven.compiler.target>1.8maven.compiler.target>
        <junit.version>4.12junit.version>
        <log4j.version>1.2.17log4j.version>
        <druid.version>1.1.10druid.version>
        <spring-boot.version>1.5.19.RELEASEspring-boot.version>
        <spring-cloud.version>Dalston.SR1spring-cloud.version>
        <mysql-connector.version>5.1.47mysql-connector.version>
        <mybatis-starter.version>1.3.3mybatis-starter.version>
        <logback.version>1.2.3logback.version>
        <lombok.version>1.18.6lombok.version>
    properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloudgroupId>
                <artifactId>spring-cloud-dependenciesartifactId>
                <version>${spring-cloud.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            <dependency>
                <groupId>org.springframework.bootgroupId>
                <artifactId>spring-boot-dependenciesartifactId>
                <version>${spring-boot.version}version>
                <type>pomtype>
                <scope>importscope>
            dependency>
            <dependency>
                <groupId>mysqlgroupId>
                <artifactId>mysql-connector-javaartifactId>
                <version>${mysql-connector.version}version>
            dependency>
            <dependency>
                <groupId>com.alibabagroupId>
                <artifactId>druidartifactId>
                <version>${druid.version}version>
            dependency>
            <dependency>
                <groupId>org.mybatis.spring.bootgroupId>
                <artifactId>mybatis-spring-boot-starterartifactId>
                <version>${mybatis-starter.version}version>
            dependency>
            <dependency>
                <groupId>log4jgroupId>
                <artifactId>log4jartifactId>
                <version>${log4j.version}version>
            dependency>
            <dependency>
                <groupId>ch.qos.logbackgroupId>
                <artifactId>logback-coreartifactId>
                <version>${logback.version}version>
            dependency>
            <dependency>
                <groupId>junitgroupId>
                <artifactId>junitartifactId>
                <version>${junit.version}version>
                <scope>testscope>
            dependency>
        dependencies>
    dependencyManagement>

    <build>
        <finalName>lingluocloudfinalName>
        <resources>
            <resource>
                <directory>src/main/resourcesdirectory>
                <filtering>truefiltering>
            resource>
        resources>

        <plugins>
            <plugin>
                <groupId>org.apache.maven.pluginsgroupId>
                <artifactId>maven-resources-pluginartifactId>
                
                <configuration>
                    <delimiters>
                        <delimit>$delimit>
                    delimiters>
                configuration>
            plugin>
        plugins>
    build>
project>
  • yml文件
server:
  port: 9527

spring:
  application:
    name: lingluocloud-zuul-gateway

eureka:
  instance:
    instance-id: gateway-9527
    prefer-ip-address: true
  client:
    service-url:
      # 单机版配置defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka #设置与eureka server 交互的地址查询服务和注册服务都需要依赖的地址
      #集群配置
      defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/

info:
  app.name: lingluocloud-zuul-gateway
  company.name: www.lingluo.com
  build.artifactId: ${project.artifactId}
  build.version: ${project.version}
  • hosts文件修改,添加
  127.0.0.1  lingluozuul.com
  • 新建主启动类Zuul_Gateway_9527_App
@SpringBootApplication
@EnableZuulProxy
public class Zuul_Gateway_9527_App {
    public static void main(String[] args) {
        SpringApplication.run(Zuul_Gateway_9527_App.class,args);
    }
}

3、测试

启动
三个eureka集群7001 7002 7003
一个服务提供类lingluocloud-provider-dept-8001
一个路由Zuul_Gateway_9527_App

看一下Eureka注册的实例 http://localhost:7001/
路由网关实例注册进来了

  • 不启用路由:http://localhost:8001/dept/findAll
  • 启用路由:http://lingluozuul.com:9527/lingluocloud-dept/dept/findAll

结果是都可以访问到
SpringCloud系列8:一文搞定Zuul路由网关及源码解析_第2张图片

4、路由访问映射规则

代理名称替换真实的名称

有时候我不想直接暴露微服务名叫灵洛,于是想换成虚拟的名称mydept

  • 修改yml,增加如下配置

    配置后原路径http://lingluozuul.com:9527/lingluocloud-dept/dept/findAll

    可变为代理路径http://lingluozuul.com:9527/mydept/dept/findAll

zuul:
  routes:
    mydept.serviceId: LINGLUOCLOUD-DEPT
    mydept.path: /mydept/**
  • 忽略原真实服务名
zuul:
  #多个用"*" ignored-services: "*"
  ignored-services: LINGLUOCLOUD-DEPT
  routes:
    mydept.serviceId: LINGLUOCLOUD-DEPT
    mydept.path: /mydept/**
  • 设置统一公共前缀
zuul:
  #公共前缀
  prefix: /springcloud
  #多个用"*" ignored-services: "*"
  ignored-services: LINGLUOCLOUD-DEPT
  routes:
    mydept.serviceId: LINGLUOCLOUD-DEPT
    mydept.path: /mydept/**

重启微服务,访问
http://lingluozuul.com:9527/springcloud/mydept/dept/findAll

SpringCloud系列8:一文搞定Zuul路由网关及源码解析_第3张图片
成功!

更多的配置项和配置方法可以参考
spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/ZuulProperties.java并结合上述例子扩展。在ZuulProperties.java中每个字段都会有注释解释其作用。

5、Filter工作原理

Zuul中的Filter

Zuul是围绕一系列Filter展开的,这些Filter在整个HTTP请求过程中执行一连串的操作。

  • Zuul Filter有以下几个特征

Type:用以表示路由过程中的阶段(内置包含PRE、ROUTING、POST和ERROR)
Execution Order:表示相同Type的Filter的执行顺序
Criteria:执行条件
Action:执行体
Zuul提供了动态读取、编译和执行Filter的框架。各个Filter间没有直接联系,但是都通过RequestContext共享一些状态数据。

尽管Zuul支持任何基于JVM的语言,但是过滤器目前是用Groovy编写的。 每个过滤器的源代码被写入到Zuul服务器上的一组指定的目录中,这些目录将被定期轮询检查是否更新。Zuul会读取已更新的过滤器,动态编译到正在运行的服务器中,并后续请求中调用。

Filter Types

以下提供四种标准的Filter类型及其在请求生命周期中所处的位置:

PRE Filter:在请求路由到目标之前执行。一般用于请求认证、负载均衡和日志记录。
ROUTING Filter:处理目标请求。这里使用Apache HttpClient或Netflix Ribbon构造对目标的HTTP请求。
POST Filter:在目标请求返回后执行。一般会在此步骤添加响应头、收集统计和性能数据等。
ERROR Filter:整个流程某块出错时执行。
除了上述默认的四种Filter类型外,Zuul还允许自定义Filter类型并显示执行。例如,我们定义一个STATIC类型的Filter,它直接在Zuul中生成一个响应,而非将请求在转发到目标。

Zuul请求生命周期

一图胜千言,下面通过官方的一张图来了解Zuul请求的生命周期。
SpringCloud系列8:一文搞定Zuul路由网关及源码解析_第4张图片

自定义一个Filter

1、添加一个PreRequestLogFilter:

public class PreRequestLogFilter extends ZuulFilter{
    @Override
    public String filterType() {
        return PRE_TYPE;
    }
 
    @Override
    public int filterOrder() {
        return 0;
    }
 
    @Override
    public boolean shouldFilter() {
        return true;
    }
 
    @Override
    public Object run() {
        RequestContext currentContext = RequestContext.getCurrentContext();
        HttpServletRequest request = currentContext.getRequest();
        System.out.print(String.format("send %s request to %s",request.getMethod(),request.getRequestURL()));
        return null;
    }
}

2、修改启动类Zuul_Gateway_9527_App,添加Filter注入:

@Bean
public PreRequestLogFilter preRequestLogFilter(){
    return new PreRequestLogFilter();
}

3、启动服务并请求服务,观察控制台输出日志信息。

控制台输出信息

Filter的启用与禁用

我们自己写的Filter可以通过修改shouldFilter()启用或禁用。如果第三方的Filter怎样控制其启用及禁用呢?

很简单,通过配置文件就可以做到:

zuul:
  PreRequestLogFilter: #自定义Filter类名
    pre: #Type
      disable: true

6、Zuul部分源码分析

在前面提到在ROUTING过滤器中会选择使用Apache HttpClient或Netflix Ribbon请求目标服务,那么什么时候会使用Ribbon是么时候用Apache HttpClient呢?

这里涉及到两个关键Filter:RibbonRoutingFilter和SimpleHostRoutingFilter。从名字就可以看出来RibbonRoutingFilter是使用Ribbon请求目标服务,而SimpleHostRoutingFilter则是另一个。

通过filterOrder()可以看到RibbonRoutingFilter会在SimpleHostRoutingFilter之前执行。

//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java

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

//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/SimpleHostRoutingFilter.java

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

对比两个Filter的生效条件可以看出当配置serviceId时RibbonRoutingFilter生效。

下面看看RibbonRoutingFilter是如何将Ribbon和Hystrix集成进来的:

//spring-cloud-netflix-zuul/src/main/java/org/springframework/cloud/netflix/zuul/filters/route/RibbonRoutingFilter.java

public RibbonRoutingFilter(ProxyRequestHelper helper,
                           RibbonCommandFactory<?> ribbonCommandFactory,
                           List<RibbonRequestCustomizer> requestCustomizers) {
    this.helper = helper;
    this.ribbonCommandFactory = ribbonCommandFactory;
    this.requestCustomizers = requestCustomizers;
    // To support Servlet API 3.1 we need to check if getContentLengthLong exists
    // Spring 5 minimum support is 3.0, so this stays
    try {
        HttpServletRequest.class.getMethod("getContentLengthLong");
    } catch(NoSuchMethodException e) {
        useServlet31 = false;
    }
}
 
@Override
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);
    }
}
 
protected RibbonCommandContext buildCommandContext(RequestContext context) {
    HttpServletRequest request = context.getRequest();
 
    MultiValueMap<String, String> headers = this.helper
            .buildZuulRequestHeaders(request);
    MultiValueMap<String, String> params = this.helper
            .buildZuulRequestQueryParams(request);
    String verb = getVerb(request);
    InputStream requestEntity = getRequestBody(request);
    if (request.getContentLength() < 0 && !verb.equalsIgnoreCase("GET")) {
        context.setChunkedRequestBody();
    }
 
    String serviceId = (String) context.get(SERVICE_ID_KEY);
    Boolean retryable = (Boolean) context.get(RETRYABLE_KEY);
    Object loadBalancerKey = context.get(LOAD_BALANCER_KEY);
 
    String uri = this.helper.buildZuulRequestURI(request);
 
    // remove double slashes
    uri = uri.replace("//", "/");
 
    long contentLength = useServlet31 ? request.getContentLengthLong(): request.getContentLength();
 
    return new RibbonCommandContext(serviceId, verb, uri, retryable, headers, params,
            requestEntity, this.requestCustomizers, contentLength, loadBalancerKey);
}
 
protected ClientHttpResponse forward(RibbonCommandContext context) throws Exception {
    Map<String, Object> info = this.helper.debug(context.getMethod(),
            context.getUri(), context.getHeaders(), context.getParams(),
            context.getRequestEntity());
 
    RibbonCommand command = this.ribbonCommandFactory.create(context);
    try {
        ClientHttpResponse response = command.execute();
        this.helper.appendDebug(info, response.getRawStatusCode(), response.getHeaders());
        return response;
    }
    catch (HystrixRuntimeException ex) {
        return handleException(info, ex);
    }
 
}

上述代码在构造函数中注入了ribbonCommandFactory(实际类型为RestClientRibbonCommandFactory)。接着在run()方法中构造RibbonCommandContext,并通过RestClientRibbonCommandFactory创建一个RibbonCommand(实际类型为RestClientRibbonCommand)。RestClientRibbonCommand继承AbstractRibbonCommand时所带的泛型参数RestClient具备负载均衡能力。又由于在RestClientRibbonCommand的继承链上出现了HystrixCommand,所以通过该Filter发出的请求实际上就同时集成了Ribbon和Hystrix。

7、Zuul的容错与回退

通过实现接口可以实现Zuul的容错与回退功能,下面这个例子来自Zuul的源码DefaultFallbackProvider,这里我稍微修改了下:

@Component
public class DefaultFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        //匹配微服务名字,*表示匹配所有
        return "*";
    }
 
    @Override
    public ClientHttpResponse fallbackResponse() {
        return null;
    }
 
    @Override
    public ClientHttpResponse fallbackResponse(Throwable cause) {
        return new ClientHttpResponse() {
            @Override
            public HttpStatus getStatusCode() throws IOException {
                //当fallback时返回给调用者的状态码
                return HttpStatus.OK;
            }
 
            @Override
            public int getRawStatusCode() throws IOException {
                return this.getStatusCode().value();
            }
 
            @Override
            public String getStatusText() throws IOException {
                //状态码的文本形式
                return null;
            }
 
            @Override
            public void close() {
 
            }
 
            @Override
            public InputStream getBody() throws IOException {
                //响应体
                return new ByteArrayInputStream("default fallback".getBytes());
            }
 
            @Override
            public HttpHeaders getHeaders() {
                //设定headers
                HttpHeaders headers = new HttpHeaders();
                headers.setContentType(MediaType.TEXT_HTML);
                return headers;
            }
        };
    }
}

停掉LINGLUOCLOUD-DEPT(8001)后再请求,会发现返回了DefaultFallbackProvider中定义fallback的内容。
SpringCloud系列8:一文搞定Zuul路由网关及源码解析_第5张图片

8、高可用

作为网关这么重要的角色,高可用是非常有必要的。但是通常来说网关所面对的请求应该的是来于外部,所以虽然说网关可以注册到Eureka Server上,但是外部的客户端数量众多,是不可能向Eureka Server注册的。那么要实现高可用的,要么在网关前面再架一个前置代理(如Nginx),要么让客户端从Eureka Server处获取Zuul网关地址实现客户端负载均衡。

感兴趣的可以关注下gzh [灵洛的人间乐园],里面记录了从零开始搭建SpringCloud工程。

你可能感兴趣的:(Java)