Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

Stella981
• 阅读 511

技术背景

前面我们通过Ribbon或Feign实现了微服务之间的调用和负载均衡,那我们的各种微服务又要如何提供给外部应用调用呢。

当然,因为是REST API接口,外部客户端直接调用各个微服务是没有问题的,但出于种种原因,这并不是一个好的选择。

让客户端直接与各个微服务通讯,会有以下几个问题:

  • 客户端会多次请求不同的微服务,增加了客户端的复杂性。
  • 存在跨域请求,在一定场景下处理会变得相对比较复杂。
  • 实现认证复杂,每个微服务都需要独立认证。
  • 难以重构,项目迭代可能导致微服务重新划分。如果客户端直接与微服务通讯,那么重构将会很难实施。
  • 如果某些微服务使用了防火墙/浏览器不友好的协议,直接访问会有一定困难。

面对类似上面的问题,我们要如何解决呢?答案就是:服务网关!

使用服务网关具有以下几个优点:

  • 易于监控。可在微服务网关收集监控数据并将其推送到外部系统进行分析。
  • 易于认证。可在服务网关上进行认证,然后再转发请求到微服务,无须在每个微服务中进行认证。
  • 客户端只跟服务网关打交道,减少了客户端与各个微服务之间的交互次数。
  • 多渠道支持,可以根据不同客户端(WEB端、移动端、桌面端...)提供不同的API服务网关。

Spring Cloud Zuul

服务网关是微服务架构中一个不可或缺的部分。通过服务网关统一向外系统提供REST API的过程中,除了具备服务路由、均衡负载功能之外,它还具备了权限控制等功能。

Spring Cloud Netflix中的Zuul就担任了这样的一个角色,为微服务架构提供了前门保护的作用,同时将权限控制这些较重的非业务逻辑内容迁移到服务路由层面,使得服务集群主体能够具备更高的可复用性和可测试性。

在Spring Cloud体系中, Spring Cloud Zuul 封装了Zuul组件,作为一个API网关,负责提供负载均衡、反向代理和权限认证。

Zuul工作机制

过滤器机制

Zuul的核心是一系列的filters, 其作用类似Servlet框架的Filter,Zuul把客户端请求路由到业务处理逻辑的过程中,这些filter在路由的特定时期参与了一些过滤处理,比如实现鉴权、流量转发、请求统计等功能。Zuul的整个运行机制,可以用下图来描述。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

过滤器的生命周期

Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

基于Zuul的这些过滤器,可以实现各种丰富的功能,而这些过滤器类型则对应于请求的典型生命周期。

PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。

ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用Apache HttpClient或Netfilx Ribbon请求微服务。

POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。

ERROR:在其他阶段发生错误时执行该过滤器。

除了默认的过滤器类型,Zuul还允许我们创建自定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而不将请求转发到后端的微服务。

Zuul中默认实现的Filter

Zuul默认实现了很多Filter,这些Filter如下面表格所示。

类型

顺序

过滤器

功能

pre

-3

ServletDetectionFilter

标记处理Servlet的类型

pre

-2

Servlet30WrapperFilter

包装HttpServletRequest请求

pre

-1

FormBodyWrapperFilter

包装请求体

route

1

DebugFilter

标记调试标志

route

5

PreDecorationFilter

处理请求上下文供后续使用

route

10

RibbonRoutingFilter

serviceId请求转发

route

100

SimpleHostRoutingFilter

url请求转发

route

500

SendForwardFilter

forward请求转发

post

0

SendErrorFilter

处理有错误的请求响应

post

1000

SendResponseFilter

处理正常的请求响应

禁用指定的Filter

可以在application.yml中配置需要禁用的filter,格式为zuul.<SimpleClassName>.<filterType>.disable=true

比如要禁用org.springframework.cloud.netflix.zuul.filters.post.SendResponseFilter就设置如下。

zuul:
  SendResponseFilter:
    post:
      disable: true

自定义Filter

实现自定义滤器需要继承ZuulFilter,并实现ZuulFilter中的抽象方法。

public class MyFilter extends ZuulFilter {
    @Override
    String filterType() {
        return "pre"; // 定义filter的类型,有pre、route、post、error四种
    }

    @Override
    int filterOrder() {
        return 5; // 定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    boolean shouldFilter() {
        return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    Object run() {
        return null; // filter需要执行的具体操作
    }
}

实现案例

新建工程

新建一个项目 spring-cloud-consul-zuul,如下图。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

添加依赖

添加 consul、zuul 依赖。

pom.xml

<?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">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.4.RELEASE</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>

    <groupId>com.louis</groupId>
    <artifactId>spring-cloud-consul-zuul</artifactId>
    <version>${project.version}</version>
    <packaging>jar</packaging>

    <name>spring-cloud-consul-zuul</name>
    <description>spring-cloud-consul-zuul</description>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <project.version>1.0.0</project.version>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-consul-discovery</artifactId>
        </dependency>
    </dependencies>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

启动类

启动类添加 @EnableZuulProxy 注解,开启服务网关支持。

package com.louis.spring.cloud.consul.zuul;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.zuul.EnableZuulProxy;

@EnableZuulProxy
@SpringBootApplication
public class ZuulApplication {

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

添加配置

配置启动端口为 8541, 注册服务到注册中心,配置zuul转发规则。

这里配置在访问 locathost:8541/feigin/call 和 ribbon/call时,调用消费者(8521,8522)的对应接口。

server:
  port: 8541
spring:
  application:
    name: spring-cloud-consul-zuul
  cloud:
    consul:
      host: localhost
      port: 8500
      discovery:
        serviceName: ${spring.application.name}    # 注册到consul的服务名称
zuul:
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /ribbon/
    feign:
      path: /feign/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /feign/

测试效果

依次启动注册中心、服务提供者、服务消费者、服务网关项目。

访问 http://localhost:8541/ribbon/call, 效果如下图。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

访问 http://localhost:8541/feign/call, 效果如下图。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

说明Zuul已经成功转发请求,并成功调用后端微服务。

配置接口前缀

如果想给每个服务的API接口加上一个前缀,可使用zuul.prefix进行配置。

例如http://localhost:8541/v1/feign/call,即在所有的API接口上加一个v1作为版本号。

zuul:  prefix: /v1
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /ribbon/
    feign:
      path: /feign/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /feign/

默认路由规则

上面我们是通过添加路由配置进行请求转发的。

zuul:
  routes:
    ribbon:
      path: /ribbon/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /ribbon/
    feign:
      path: /feign/**
      serviceId: spring-cloud-consul-consumer  # 转发到消费者 /feign/

但是如果后端微服务服务非常多的时候,每一个都这样配置还是挺麻烦的,所以Spring Cloud Zuul已经帮我们做了默认配置。默认情况下,Zuul会代理所有注册到注册中心的微服务,并且Zuul的默认路由规则如下:http://ZUUL_HOST:ZUUL_PORT/微服务在注册中心的serviceId/**会被转发到serviceId对应的微服务,所以说如果遵循默认路由规则,基本上就没什么配置了。

我们移除上面的配置,直接通过 serviceId/feign/call 的方式访问。

访问 http://localhost:8541/spring-cloud-consul-consumer/feign/call,效果如下。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

结果也是可用访问的,说明ZUUL默认路由规则正在产生作用。

路由熔断

Zuul作为Netflix组件,可以与Ribbon、Eureka和Hystrix等组件相结合,实现负载均衡、熔断器的功能。默认情况下Zuul和Ribbon相结合,实现了负载均衡。实现熔断器功能需要实现FallbackProvider接口,实现该接口的两个方法,一个是getRoute(),用于指定熔断器功能应用于哪些路由的服务;另一个方法fallbackResponse()为进入熔断器功能时执行的逻辑。

创建 MyFallbackProvider 类,getRoute()方法返回"spring-cloud-consul-consumer",只针对consumer服务进行熔断。如果需要所有的路由服务都加熔断功能,需要在getRoute()方法上返回”*“的匹配符。getBody()方法返回发送熔断时的反馈信息,这里在发送熔断时返回信息:"Sorry, the service is unavailable now." 。

package com.louis.spring.cloud.consul.zuul;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

import org.springframework.cloud.netflix.zuul.filters.route.FallbackProvider;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.stereotype.Component;

@Component
public class MyFallbackProvider implements FallbackProvider {
    @Override
    public String getRoute() {
        return "spring-cloud-consul-consumer";
    }

    @Override
    public ClientHttpResponse fallbackResponse(String route, Throwable cause) {
        System.out.println("route:"+route);
        System.out.println("exception:"+cause.getMessage());
        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("Sorry, the service is unavailable now.".getBytes());
            }

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

重新启动,访问 http://localhost:8541/spring-cloud-consul-consumer/ribbon/call, 可以正常访问。

 Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

停掉spring-cloud-consumer相关的服务, 再次访问 http://localhost:8541/spring-cloud-consul-consumer/ribbon/call, 效果如下。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

说明我们自定义的熔断器已经起作用了。

自定义Filter

创建一个MyFilter, 继承ZuulFilter类,覆写run()方法逻辑,在转发请求前进行token认证,如果请求没有携带token,返回"there is no request token"提示。

package com.louis.spring.cloud.consul.zuul;

import java.io.IOException;

import javax.servlet.http.HttpServletRequest;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;

@Component
public class MyFilter extends ZuulFilter {

    private static Logger log=LoggerFactory.getLogger(MyFilter.class);

    @Override
    public String filterType() {
        return "pre"; // 定义filter的类型,有pre、route、post、error四种
    }

    @Override
    public int filterOrder() {
        return 0; // 定义filter的顺序,数字越小表示顺序越高,越先执行
    }

    @Override
    public boolean shouldFilter() {
        return true; // 表示是否需要执行该filter,true表示执行,false表示不执行
    }

    @Override
    public Object run() throws ZuulException {
        // filter需要执行的具体操作
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        System.out.println(token);
        if(token==null){
            log.warn("there is no request token");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            try {
                ctx.getResponse().getWriter().write("there is no request token");
            } catch (IOException e) {
                e.printStackTrace();
            }
            return null;
        }
        log.info("ok");
        return null;
    }
}

OK,这样就行了,Zuul会自动加载Filter执行过滤的。

重新启动Zuul项目,访问 http://localhost:8541/spring-cloud-consul-consumer/ribbon/call,效果如下。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

请求时带上token,访问 http://localhost:8541/spring-cloud-consul-consumer/ribbon/call?token=111,效果如下。

Spring Boot + Spring Cloud 构建微服务系统(七):API服务网关(Zuul)

Zuul的高可用性

Zuul作为API服务网关,不同的客户端使用不同的负载将请求统一分发到后端的Zuul,再有Zuul转发到后端服务。因此,为了保证Zuul的高可用性,前端可以同时开启多个Zuul实例进行负载均衡,另外,在Zuul的前端还可以使用Nginx或者F5再次进行负载转发,从而保证Zuul的高可用性。

源码下载

码云:https://gitee.com/liuge1988/spring-cloud-demo.git


作者:朝雨忆轻尘
出处:https://www.cnblogs.com/xifengxiaoma/
版权所有,欢迎转载,转载请注明原文作者及出处。

本文同步分享在 博客"朝雨忆轻尘"(CNBlog)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Stella981 Stella981
3年前
Nepxion Discovery 5.5.0 发布
!(https://oscimg.oschina.net/oscnet/f81c043194ef4732880459d00c1a720e.png)发布日志功能更新:增加基于Opentracing调用链的支持,目前支持UberJaeger,实现在SpringCloudGateway、Zuul和服务上的灰度
Wesley13 Wesley13
3年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
11个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这