服务网关Spring Cloud Zuul(Finchley版本)

一、Zuul简介

Zuul作为微服务系统的网关组件,用于构建边界服务(Edge Service),致力于动态路由、过滤、监控、弹性伸缩和安全。其在微服务架构中有着重要的作用,主要体现在以下六个方面:

  • Zull、Ribbon以及Eureka相结合可以实现智能路由和负载均衡的功能,Zull可以按照某种策略将请求分发到不同的实例上;
  • 网关作为边界服务,将内部服务的API接口进行聚合并统一对外暴露接口。保护内部服务的API接口,防止内部服务被外界调用泄露敏感信息;
  • 网关可以对用户的身份权限进行认证,防止非法请求API接口;
  • 网关可以实现监控功能,实时日志输出,对请求进行记录;
  • 网关可以用来实现流量监控,在高流量的情况下,对服务进行降级;
  • API接口从内部服务分离出来,便于测试

二、请求路由

使用Spring Cloud Zuul实现路由的规则是十分简单的。路由方式包括两种:传统路由方式,面向服务的路由方式。

2.1 传统路由

下面我们看以下配置:

zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.url=http://localhosst:8080/

该规则配置表示所有符合/holiday/** 规则的访问都会被路由转发到http://localhosst:8080/地址上,例如:当我们访问http://localhost:5555/holiday/getAllDays,API网关就会将请求转发到http://localhost:8080/holiday/getAllDays提供的微服务接口上。其中holiday为微服务的名称,可以任意定义,但是一组path和url映射关系的路由名称必须相同,下面面向服务的路由方式也是如此。

2.2 面向服务的路由

Spring Cloud Zuul 与 Spring Cloud Eureka 可以实现无缝对接实现面向服务的路由。我们让路由的path映射到具体的服务上,而具体的url交由Eureka的服务发现机制去自动维护。具体配置如下(其他配置参考下面的实战):

zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.service-id=holiday

通过上面的配置,我们不需要维护具体实例的位置,是得维护工作十分简单。另外,面向服务打的路由默认实现了负载均衡,而传统路由还需要手动添加所有实例的位置。

三、路由规则

Spring Cloud Zuul提供了默认的路由规则,当然我们也可以修改这个路由规则。

3.1 默认路由规则

Zull与Eureka的配合使用后,Zull会默认配置一个路由规则,这些默认规则的path会使用service-id配置的服务名作为请求的前缀。例如:有holiday服务,他的默认规则如下

zuul.routes.holiday.path=/holiday/**
zuul.routes.holiday.service-id=holiday

由于默认情况下所有Eureka上的服务都会被Zuul自动创建映射关系进行路由,这会使得一些我们不希望对外开放的服务也被外部访问到。这个时候我们可以配置zuul.ignored-services参数来设置一个服务名匹配表达式进行判断,如果服务名匹配表达式,那么Zull将跳过这个服务,不为其创建路由规则。例如:zuul.ignored-services=*表示对所有的服务不自动创建路由规则,这样我们就需要为每个服配置路由规则。

3.2 自定义路由规则

有这样一个场景,由于业务的扩展,版本的升级,服务存在不同的版本。比如我们有这样的命名:holiday-v1、holiday-v2,默认情况下,Zuul自动为服务创建的路由表达式会采用服务名做前缀,针对holiday-v1就会产生/holiday-v1,/holiday-v2两个路径来做映射,但这样生成的表达式规则较为单一,不利于路径规则的管理。通常,对于上面这种情况,我们希望是生成的路径为/v1/holiday,/v2/holiday。我们可以通过自定义路由规则来实现,具体代码如下:

@Bean
public PatternServiceRouteMapper serviceRouteMapper(){
  return new PatternServiceRouteMapper(
    "(?<name>^.+)-(?<version>v.+$)",
    "${version}/${name}");
}

PatternServiceRouteMapper对象可以让开发者通过正则表达式来自定义服务于路由映射的生成关系。

四、Zuul的过滤器

Zull有请求过滤的功能,其过滤器可以在Http请求的发起和响应返回期间执行一系列的过滤器。Zuul包扩以下四种过滤器:

  • PRE: 该类型的filters在Request routing到源web-service之前执行。可以进行一些权限认证,日志记录,或者额外给Request增加一些属性供后续的filter使用;
  • ROUTING:该类型的filters用于把Request routing到源web-service,源web-service是实现业务逻辑的服务。这里使用HttpClient请求web-service;
  • POST:该类型的filters在ROUTING返回Response后执行。用来实现对Response结果进行修改,收集统计数据以及把Response传输会客户端;
  • ERROR:上面三个过程中任何一个出现错误都交由ERROR类型的filters进行处理。

Zuul过滤器具有以下关键特性:

  • Type(类型):Zuul过滤器的类型,这个类型决定过滤器在哪个阶段执行,例如:pre,post等阶段;
  • Execution Order(执行顺序):规定了过滤器的执行顺序,Order的值越小,越先执行;
  • Criteria(标准):Filters执行所需条件
  • Action(行动):如果符合执行条件,则执行Action(具体逻辑代码)

示例如下:

public class MyFilter extends ZuulFilter {
    @Override
    public Object run() {
            RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();

        System.out.println(String.format("%s AccessUserNameFilter request to %s", request.getMethod(), request.getRequestURL().toString()));
        // 获取请求的参数
        String username = request.getParameter("username");
        // 如果请求的参数不为空,且值为chhliu时,则通过
        if(null != username && username.equals("chhliu")) {
          // 对该请求进行路由
            ctx.setSendZuulResponse(true);
            ctx.setResponseStatusCode(200);
            // 设值,让下一个Filter看到上一个Filter的状态
            ctx.set("isSuccess", true);
            return null;
        }else{
          // 过滤该请求,不对其进行路由
            ctx.setSendZuulResponse(false);
            // 返回错误码
            ctx.setResponseStatusCode(401);
            // 返回错误内容
            ctx.setResponseBody("{\"result\":\"username is not correct!\"}");
            ctx.set("isSuccess", false);
            return null;
        }
    }

    @Override
    public boolean shouldFilter() {
    // 是否执行该过滤器,此处为true,说明需要过滤
        return true;
    }

    @Override
    public int filterOrder() {
    // 优先级为0,数字越大,优先级越低
        return 0;
    }

    @Override
    public String filterType() {
    // 前置过滤器
        return "pre";
    }
}

Zuul请求的生命周期如图所示:


GitHub

五、设置熔断

通常在服务无法提供服务的时候,需要执行熔断。zuul上实现熔断需要实现FallbackProvider的接口。实现接口中的两个方法:getRoute()用于指定应用在哪个服务上;fallbackResponse()进入熔断功能的执行逻辑。示例如下:

@Component
public class CustomZuulFallbackHandler implements FallbackProvider {
  private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
  /**
   * 指定处理的service
   *
   * @return
   */
  @Override
  public String getRoute() {
    return "*";
  }

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

      @Override
      public int getRawStatusCode() throws IOException {
        return 200;
      }

      @Override
      public String getStatusText() throws IOException {
        return "OK";
      }

      @Override
      public void close() {

      }

      @Override
      public InputStream getBody() throws IOException {
        return new ByteArrayInputStream((route+" is unavailable.").getBytes());
      }

      @Override
      public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
      }
    };
  }

  @Override
  public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    if (cause != null) {
      String reason = cause.getMessage();
      logger.info("Excption {}",reason);
    }
    return fallbackResponse(route);
  }
}

六、实战

6.1 pom文件

<?xml version="1.0" encoding="UTF-8"?>
<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">
    <parent>
        <groupId>com.southgis.ibase.parent</groupId>
        <artifactId>parentWebService</artifactId>
        <version>2.0.1-SNAPSHOT</version>
        <relativePath>../../parent/parentWebService/pom.xml</relativePath>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>api-gateway</artifactId>
    <groupId>com.southgis.ibase.systemassistance</groupId>
    <version>2.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <description>网关服务</description>

    <dependencies>
      <!--服务注册与发现-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
        <!--配置中心-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-config-client</artifactId>
        </dependency>
        <!--路由网关-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.retry</groupId>
            <artifactId>spring-retry</artifactId>
        </dependency>
        <!--cas 客户端-->
        <dependency>
            <groupId>org.jasig.cas.client</groupId>
            <artifactId>cas-client-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
    <build>
        <finalName>apiGateway</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.southgis.ibase.systemassistance.ApiGatewayCustomApplication</mainClass>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

6.2 配置文件

  • bootstrap.properties

    #服务名 对应配置文件中的{application}部分
    spring.application.name=apiGateway
    #对应前配置文件中的{profile}部分
    spring.cloud.config.profile=dev2
    #配置访问路径
    server.servlet.context-path=/eureka-server
    
    #注册中心
    eureka.client.serviceUrl.defaultZone=http://localhost:8080/eureka-server/eureka
    
    #为监控端点 /info和/health端点也加上类似的前缀
    management.server.servlet.context-path=/apiGateway
    eureka.instance.statusPageUrlPath=${management.server.servlet.context-path}/actuator/info
    eureka.instance.healthCheckUrlPath=${management.server.servlet.context-path}/actuator/health
    
    #通过服务连接配置中心
    #spring.cloud.config.discovery.enabled=true
    #spring.cloud.config.discovery.serviceId=config-server
    spring.cloud.config.uri = http://localhost:8080/config-server
    #配置文件获取失败快速返回
    spring.cloud.config.failFast=true
    
    #日志配置
    #logging.config=classpath:logback-spring.xml
    #logging.path=D:/ibase/logs/holiday
    #logging.pattern.console=[%d{yyyy-MM-dd HH:mm:ss}] -- [%-5p]: [%c] -- %m%n
    #logging.pattern.file=[%d{yyyy-MM-dd HH:mm:ss}] -- [%-5p]: [%c] -- %m%n
    
  • apiGateway-dev2.properties

    #访问端口
    server.port=8080
    #设置session超时时间为540分钟
    server.servlet.session.timeout=PT540M
    
    #zuul默认为所有服务开启默认的路由,为了服务安全,此处关闭
    zuul.ignored-services=*
    #代码字典服务路由
    zuul.routes.codedict.path=/codedict/**
    zuul.routes.codedict.service-id=codedict
    #是否转发后还带转发特征的字符
    zuul.routes.codedict.strip-prefix=false
    #行政区划服务路由
    zuul.routes.adminzone.path=/adminzone/**
    zuul.routes.adminzone.service-id=adminzone
    zuul.routes.adminzone.strip-prefix=false
    
    #是否开启路由重试
    zuul.retryable=true
    #对当前服务的重试次数
    ribbon.MaxAutoRetries=2
    #切换实例的重试次数
    ribbon.MaxAutoRetriesNextServer=0
    #请求处理的超时时间
    ribbon.ReadTimeout=6000
    #请求连接的超时时间
    ribbon.ConnectTimeout=6000
    #对所有操作请求都进行重试
    ribbon.OkToRetryOnAllOperations=true
    
    #将 hystrix 的超时时间设置成 5000 毫秒(hystrix超时时间小于ribbon连接超时时间,先走hystrix)
    hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds=5000
    

6.3 过滤器配置

@Configuration
public class ApiGatewayFilter extends ZuulFilter {
  @Override
  public String filterType() {
    return "pre";
  }

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

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

  @Override
  public Object run() throws ZuulException {
    RequestContext context = RequestContext.getCurrentContext();
    HttpServletRequest request = context.getRequest();
    Principal principal = request.getUserPrincipal();
    //获取用户的登录id
    String userId = principal.getName();
    context.addZuulRequestHeader("X-AUTH-ID",userId);
    return null;
  }
}

在这里我们将获取的登录用户id设置到了请求头中传递给内部服务,内部服务可以通过下面的代码进行获取:

String user = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest().getHeader("X-AUTH-ID");

6.4 熔断配置

@Component
public class CustomZuulFallbackHandler implements FallbackProvider {
  private final Logger logger = LoggerFactory.getLogger(FallbackProvider.class);
  /**
   * 指定处理的service
   *
   * @return
   */
  @Override
  public String getRoute() {
    return "*";
  }

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

      @Override
      public int getRawStatusCode() throws IOException {
        return 200;
      }

      @Override
      public String getStatusText() throws IOException {
        return "OK";
      }

      @Override
      public void close() {

      }

      @Override
      public InputStream getBody() throws IOException {
        return new ByteArrayInputStream((route+" is unavailable.").getBytes());
      }

      @Override
      public HttpHeaders getHeaders() {
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.APPLICATION_JSON);
        return headers;
      }
    };
  }

  @Override
  public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
    if (cause != null) {
      String reason = cause.getMessage();
      logger.info("Excption {}",reason);
    }
    return fallbackResponse(route);
  }
}

6.5 启动类

**
 * 路由网关服务部署启动类
 *
 * @author simon
 **/
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
@EnableZuulProxy
@EnableEurekaClient
@SpringCloudApplication
public class ApiGatewayMicroApplication {
  public static void main(String[] args) {
    SpringApplication.run(ApiGatewayMicroApplication.class, args);
  }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,198评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,663评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,985评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,673评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,994评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,399评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,717评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,407评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,112评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,371评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,891评论 1 256
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,255评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,881评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,010评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,764评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,412评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,299评论 2 260

推荐阅读更多精彩内容