spring cloud 学习(6) - zuul 微服务网关

微服务架构体系中,通常一个业务系统会有很多的微服务,比如:OrderService、ProductService、UserService...,为了让调用更简单,一般会在这些服务前端再封装一层,类似下面这样:

 

前面这一层俗称为“网关层”,其存在意义在于,将"1对N"问题 转换成了"1对1”问题,同时在请求到达真正的微服务之前,可以做一些预处理,比如:来源合法性检测,权限校验,反爬虫之类...

传统方式下,最土的办法,网关层可以人肉封装,类似以下示例代码:

LoginResult login(...){<br>   //TODO 预处理...

   return userService.login();//调用用户服务的登录方法 

}

 

Product queryProduct(...){<br>   //TODO 预处理...

  return productService.queryProduct();//调用产品服务的查询方法 

}

 

Order submitOrder(...){<br>   //TODO 预处理...

   return orderService.submitOrder();//调用订单服务的查询方法

}

这样做,当然能跑起来,但是维护量大,以后各个微服务增加了新方法,都需要在网关层手动增加相应的方法封装,而spring cloud 中的zuul很好的解决了这一问题,示意图如下:

Zuul做为网关层,自身也是一个微服务,跟其它服务Service-1,Service-2, ... Service-N一样,都注册在eureka server上,可以相互发现,zuul能感知到哪些服务在线,同时通过配置路由规则(后面会给出示例),可以将请求自动转发到指定的后端微服务上,对于一些公用的预处理(比如:权限认证,token合法性校验,灰度验证时部分流量引导之类),可以放在所谓的过滤器(ZuulFilter)里处理,这样后端服务以后新增了服务,zuul层几乎不用修改。

使用步骤:

一、添加zuul依赖的jar包

1

compile 'org.springframework.cloud:spring-cloud-starter-zuul'

 

二、application.yml里配置路由

1

2

3

4

5

6

7

8

9

zuul:

  routes:

    api-a:

      path: /api-user/**

      service-id: service-provider

      sensitive-headers:

    api-b:

      path: /api-order/**

      service-id: service-consumer 

解释一下:上面这段配置表示,/api-user/开头的url请求,将转发到service-provider这个微服务上,/api-order/开头的url请求,将转发到service-consumer这个微服务上。

三、熔断处理

如果网关后面的微服务挂了,zuul还允许定义一个fallback类,用于熔断处理,参考下面的代码:

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;
 
import org.springframework.cloud.netflix.zuul.filters.route.ZuulFallbackProvider;
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;
 
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;
 
/**
 * Created by yangjunming on 2017/7/14.
 */
@Component
public class ServiceConsumerFallbackProvider implements ZuulFallbackProvider {
 
    @Override
    public String getRoute() {
        return "service-consumer";
    }
 
    @Override
    public ClientHttpResponse fallbackResponse() {
        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 {
                return new ByteArrayInputStream("Service-Consumer不可用".getBytes());
            }
 
            @Override
            public HttpHeaders getHeaders() {
                HttpHeaders headers = new HttpHeaders();
                MediaType mt = new MediaType("application", "json", Charset.forName("UTF-8"));
                headers.setContentType(mt);
                return headers;
            }
        };
    }
}

开发人员只要在getRoute这个方法里指定要处理的微服务实例,然后重写fallbackResponse即可。

此时,如果观察/health端点,也可以看到hystrix处于融断开启状态

 

四、ZuulFilter过滤器

过滤器是一个很有用的机制,下面分几种经典场景演示下:

4.1、token校验/安全认证

网关直接暴露在公网上时,终端要调用某个服务,通常会把登录后的token传过来,网关层对token进行有效性验证,如果token无效(或没传token),提示重新登录或直接拒绝。另外,网关后面的微服务,如果设置了spring security中的basic Auth(即:不允许匿名访问,必须提供用户名、密码),也可以在Filter中处理。参考下面的代码:

package com.cnblogs.yjmyzz.spring.cloud.study.gateway;
 
import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.apache.commons.codec.binary.Base64;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cloud.netflix.zuul.filters.support.FilterConstants;
import org.springframework.stereotype.Component;
 
import javax.servlet.http.HttpServletRequest;
 
/**
 * Created by yangjunming on 2017/7/13.
 */
@Component
public class AccessFilter extends ZuulFilter {
 
    private static Logger logger = LoggerFactory.getLogger(AccessFilter.class);
 
    @Override
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }
 
    @Override
    public int filterOrder() {
        return 0;
    }
 
    @Override
    public boolean shouldFilter() {
        return true;
    }
 
    @Override
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
 
        Object token = request.getParameter("token");
 
        //校验token
        if (token == null) {
            logger.info("token为空,禁止访问!");
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            return null;
        } else {
            //TODO 根据token获取相应的登录信息,进行校验(略)
        }
 
        //添加Basic Auth认证信息
        ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));
 
        return null;
    }
 
    private String getBase64Credentials(String username, String password) {
        String plainCreds = username + ":" + password;
        byte[] plainCredsBytes = plainCreds.getBytes();
        byte[] base64CredsBytes = Base64.encodeBase64(plainCredsBytes);
        return new String(base64CredsBytes);
    }
}

Filter一共有4种类型,其常量值在org.springframework.cloud.netflix.zuul.filters.support.FilterConstants 中定义

// Zuul Filter TYPE constants -----------------------------------
 
/**
 * {@link ZuulFilter#filterType()} error type.
 */
String ERROR_TYPE = "error";
 
/**
 * {@link ZuulFilter#filterType()} post type.
 */
String POST_TYPE = "post";
 
/**
 * {@link ZuulFilter#filterType()} pre type.
 */
String PRE_TYPE = "pre";
 
/**
 * {@link ZuulFilter#filterType()} route type.
 */
String ROUTE_TYPE = "route";  

安全校验,一般放在请求真正处理之前,所以上面的示例filterType指定为pre,剩下的只要在shouldFilter()、run()方法中重写自己的逻辑即可。

4.2 动态修改请求参数

zuulFilter可以拦截所有请求参数,并对其进行修改,比如:终端发过来的数据,出于安全要求,可能是经过加密处理的,需要在网关层进行参数解密,再传递到后面的服务;再比如:用户传过来的token值,需要转换成userId/userName这些信息,再传递到背后的微服务。参考下面的run方法:

public Object run() {
    try {
        RequestContext context = getCurrentContext();
        InputStream in = (InputStream) context.get("requestEntity");
        if (in == null) {
            in = context.getRequest().getInputStream();

        }

        String body = StreamUtils.copyToString(in, Charset.forName("UTF-8"));
        body = "动态增加一段内容到body中: " + body;
        byte[] bytes = body.getBytes("UTF-8");
        context.setRequest(new HttpServletRequestWrapper(getCurrentContext().getRequest()) {
            @Override
            public ServletInputStream getInputStream() throws IOException {
                return new ServletInputStreamWrapper(bytes);
            }

            @Override
            public int getContentLength() {
                return bytes.length;
            }

            @Override
            public long getContentLengthLong() {
                return bytes.length;
            }
        });

    } catch (IOException e) {
        rethrowRuntimeException(e);

    }

    return null;

}

更多filter的示例,可以参考官网:https://github.com/spring-cloud-samples/sample-zuul-filters

4.3 灰度发布(Gated Launch/Gray Release) 

大型分布式系统中,灰度发布是保证线上系统安全生产的重要手段,一般的做法为:从集群中指定一台(或某几台)机器,每次做新版本发布前,先只发布这些机器上,先观察一下是否正常,如果稳定运行后,再发布到其它机器。这种策略(相当于按部分节点来灰度),大多数情况下可以满足要求,但是有一些特定场景,可能不太适用。

比如:笔者所在的“美味不用等”公司,主要B端用户为各餐饮品牌的商家,多数情况下,如果新上了一个功能,希望找一些规模较小的餐厅做试点,先看看上线后的运行情况,如果运行良好,再推广到其它商家。

再比如:后端服务有N多个版本在同时运行,比如V1、V2,现在新加了一个V3版本(这在手机app应用中很常见),希望只有部分升级了app的用户访问最新的V3版本服务,其它用户仍然访问旧版本,待系统稳定后,再大规模提示用户升级。

对于这些看上去需求各异的灰度需求,其实本质是一样的:将请求(根据参数内容+业务规则),将其转向到特定的灰度机器上。Spring Cloud MicroService中有一个metadata-map(元数据)设置,可以很好的满足这类需求。

首先要引入一个jar包:(这是github上开源的一个项目ribbon-discovery-filter-spring-cloud-starter)

1

compile 'io.jmnarloch:ribbon-discovery-filter-spring-cloud-starter:2.1.0'

示例如下:

在各个服务的application.yml中设置以下metadata-map

1

2

3

4

eureka:

  instance:

    metadata-map:

      gated-launch: false

即:所有节点发布后,默认灰度模式为false。然后把特定的灰度机器上的配置,该参数改成true(表明这台机器是用于灰度验证的)。

然后在ZuulFilter中参考下面的代码:

@Override
public Object run() {
    RequestContext ctx = RequestContext.getCurrentContext();
    HttpServletRequest request = ctx.getRequest();
 
    Object token = request.getParameter("token");
 
    //校验token
    if (token == null) {
        logger.info("token为空,禁止访问!");
        ctx.setSendZuulResponse(false);
        ctx.setResponseStatusCode(401);
        return null;
    } else {
        //TODO 根据token获取相应的登录信息,进行校验(略)
 
        //灰度示例
        RibbonFilterContextHolder.clearCurrentContext();
        if (token.equals("1234567890")) {
            RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "true");
        } else {
            RibbonFilterContextHolder.getCurrentContext().add("gated-launch", "false");
        }
    }
 
    //添加Basic Auth认证信息
    ctx.addZuulRequestHeader("Authorization", "Basic " + getBase64Credentials("app01", "*****"));
 
    return null;
}

注意18-23行,这里演示了通过特定的token参数值,将请求引导到gated-lanuch=true的机器上。(注:参考这个原理,大家可以把参数值,换成自己的version-版本号,shopId-商家Id之类)。只要请求参数中的token=1234567890,这次请求就会转发到灰度节点上。

如果有朋友好奇这是怎么做到的,可以看下io.jmnarloch.spring.cloud.ribbon.predicate.MetadataAwarePredicate 这个类:

@Override
protected boolean apply(DiscoveryEnabledServer server) {
 
    final RibbonFilterContext context = RibbonFilterContextHolder.getCurrentContext();
    final Set<Map.Entry<String, String>> attributes = Collections.unmodifiableSet(context.getAttributes().entrySet());
    final Map<String, String> metadata = server.getInstanceInfo().getMetadata();
    return metadata.entrySet().containsAll(attributes);
} 

大致原理就是拿上下文中,开发人员设置的属性 与 服务节点里的metadata-map 进行比较,如果metadata-map中包括开发人员设置的属性,就返回成功(即:选择这台服务器)

示例源码:https://github.com/yjmyzz/spring-cloud-demo 

 

热门文章

暂无图片
编程学习 ·

CnPlugin是PL/SQL Developer工具插件使用

CnPlugin是PL/SQL Developer工具插件,支持PL/SQL Developer 7.0以上版本。1、安装2、拷贝文件安装结束后,得到一个 CnPlugin.dll 和 “CnPlugin”的文件夹,把这两个拷贝到 PL/SQL Developer的安装路径下的 “PlugIns”目录下,重启PL/SQL Develop可以使用了,打开的时候 会在…
暂无图片
编程学习 ·

深度学习之计算机视觉

在第3章中,使用了名为ResNet的流行的卷积神经网络(Convolutional Neural Network,CNN)架构构建了一个图像分类器,我们将此模型作为黑盒使用。本章将讨论卷积网络的重要组成部分。本章将涵盖如下重要主题:神经网络简介;从零开始构建CNN模型;创建和探索VGG16模型;计算预…
暂无图片
编程学习 ·

Arduino项目实战——基于Arduino【智能垃圾桶】设计

Arduino项目实战——基于Arduino【智能垃圾桶】设计第一次接触Arduino是在大一的时候,距离现在已经五年,当时一个简单的“电子琴”项目就让我抓耳挠腮,根本不具备“面向百度编程”能力的我,用老师提供的文档跟我的组员用了好几天,才让Arduino跟蜂鸣器想起一首简单版的《小…
暂无图片
编程学习 ·

Web前端页面制作流程以及注意事项,满满的干货!

每天我们打开电脑,看到各种各样的web前端页面。你知道他们是如何制作的吗?为了让页面更具有规范性,让使用者更加方便,在制作页面过程中必须遵循一定的设计流程。在这里就为大家详细介绍一下制作一个Web前端页面的设计流程及注意事项。一:确定网站主题 每个网站都有自身以及…
暂无图片
编程学习 ·

Python爬虫入门教程 72-100 分布式爬虫初步解析-配好环境肝完一半

写在前面 最近该系列的爬虫更新比较慢了,原因是实在写不动了,70多篇博客,每一篇都是一个案例,在写好多都不是篇博客可以容纳的了,而且爬虫的技术在70多篇中都有所涉及了,但是flag既然历下了,那么就必须要把它完成,接下来进入一些稍微稍微麻烦一点的内容,第一个咱就写分…
暂无图片
编程学习 ·

ZHS16GBK字符集插入中文时报错ORA-01756

插入数据时遇到一个奇怪的问题,插入中文报错ORA-01756: quoted string not properly terminated简单的复现测试如下:查看客户端及数据库字符集可以看到字符集是一样的,如果不同,设置客户端字符集与数据库端相同再测试export NLS_LANG=AMERICAN_AMERICA.ZHS16GBK找了网上好多…
暂无图片
编程学习 ·

C++--找出三条能构成三角形且周长最大的边的一个普通方法

题目:给定由一些正数(代表长度)组成的数组 A,返回由其中三个长度组成的、面积不为零的三角形的最大周长。如果不能形成任何面积不为零的三角形,返回 0。 #输出示例 输入:[2,1,2] 输出:5 输入:[1,5,1] 输出:0 输入:[3,2,3,4] 输出:10 输入:[3,6,2,3] 输出:8 #inclu…
暂无图片
编程学习 ·

2. judgeSquareSum

忘记是自然选择,重要的是抽取学习方法双指针 --判断某一非负整数是否是两数平方和(easy) leecode输入:5 输出:true 5=1^2 + 2^2 思路:等于从一个有序数组找两个数平方和为target,注意的是最大值肯定小于Math.sqrt(target)public boolean judgeSquareSum(int c) {if (c < 0)…
暂无图片
编程学习 ·

jdk源码解析二之HashMap

这里写自定义目录标题HashMapputremovereplaceget扩容resize迭代器总结什么时候采用红黑树?为什么每次扩容后,是2的幂次方?为什么扩容后,相同的在原位置保存,而不同的则当前索引+之前原位置索引保存?为啥用尾插法?为什么线程不安全? HashMap HashMap的loadFactor为什么是0…
暂无图片
编程学习 ·

1252 奇数值单元格的数目(模拟)

1. 问题描述:给你一个 n 行 m 列的矩阵,最开始的时候,每个单元格中的值都是 0。另有一个索引数组 indices,indices[i] = [ri, ci] 中的 ri 和 ci 分别表示指定的行和列(从 0 开始编号)。你需要将每对 [ri, ci] 指定的行和列上的所有单元格的值加 1。请你在执行完所有 ind…
暂无图片
编程学习 ·

奥运五环的绘制

#奥运五环的绘制 import turtle turtle.width(10) turtle.color(“blue”) turtle.circle(50) turtle.penup() turtle.goto(120,0) turtle.pendown() turtle.color(“black”) turtle.circle(50) turtle.penup() turtle.goto(240,0) turtle.pendown() turtle.color(“red”) tu…
暂无图片
编程学习 ·

07:从一个二维数组中查找一个数

题目:从一个二维数组中查找一个数 数组要求:从左到右,从上到小依次递增的矩形二维数组 思路:我们从矩阵的右上角开始查找,若该位置的数值小于目标值,列数减一;若数值大于目标值,行数加一,后续重复。 public class Offer07 {public static void main(String[] args) {i…
暂无图片
编程学习 ·

Netty-为什么选择 Netty

人工智能,零基础入门!http://www.captainbed.net/inner API 使用简单,开发门槛低;功能强大,预置了多种编解码功能,支持多种主流协议;定制能力强,可以通过 ChannelHandler 对通信框架进行灵活的扩展;性能高,通过与其它业界主流的 NIO 框架对比,Netty 的综合性能最优;…
暂无图片
编程学习 ·

PTA7-16 一元多项式求导 (20分) 设计函数求一元多项式的导数

设计函数求一元多项式的导数。 题目在此 输入格式: 以指数递降方式输入多项式非零项系数和指数(绝对值均为不超过1000的整数)。数字间以空格分隔。 输出格式: 以与输入相同的格式输出导数多项式非零项的系数和指数。数字间以空格分隔,但结尾不能有多余空格。 输入样例::3 4…
暂无图片
编程学习 ·

appium获取toast

环境准备:appium server 1.7版本 下载地址:https://github.com/appium/appium-desktop/releases/tag/v1.5.0下载appium-desktop-Setup-1.5.0-ia32.exe 并启动服务安装appium-uiautomator2-driver使用npm命令安装,npm如何安装自己百度。 npm install appium-uiautomator2-dri…
暂无图片
编程学习 ·

将word文档锁定其他用户不可编辑

将word文档锁定其他用户不可编辑 首先打开你需要锁定的word文档然后点击审阅,如上图 点击限制编辑 点击现在对选定的样式设置格式化,防止被格式化 点击仅允许在文档中进行此类型的编辑 在下拉框内选择不允许任何修改(只读) 点击"是,启动强制保护"
暂无图片
编程学习 ·

有哪样 BI 产品能支持数据填报补录功能?

大部分的 BI 产品是不支持数据填报补录功能的。因为 BI 产品主要侧重于数据分析,常用于做自助报表、多维分析,并不关注数据的填报补录,也就不带这个功能了,所以很多 BI 产品是搭配报表工具来使用的,这样就可以利用报表的填报功能来补足 BI 的不足了。以润乾报表为代表的国…
暂无图片
编程学习 ·

Android TV中RecyclerView循环切换

前言 最近,在做一个菜单功能,其实就是一个RecyclerView的列表,需要做循环。因为在TV上,涉及焦点问题,所以跟手机还是有些许不同,遇到了一些问题,网上只有一篇相关,所以,完成了功能之后,自己来总结分享一下。 解决方案第一种: 在adapter里面设置第一个和最后一个view的…
暂无图片
编程学习 ·

Zookeeper、Apache Dubbo

Zookeeper zookeeper概述 ZooKeeper从字面意思理解,【Zoo - 动物园,Keeper - 管理员】动物园中有很多种动物,这里的动物就可以比作分布式环境下多种多样的服务,而ZooKeeper做的就是管理这些服务。 Apache ZooKeeper的系统为分布式协调是构建分布式应用的高性能服务。 ZooKe…