消息总线组件 Spring Cloud Bus

单个工程更新

有了配置中心,我们就可以吧配置文件放到git上来统一管理了,但是如果配置文件发生了变化,客户端 又如何更新呢?

1.在配置文件中增加自定义属性

person:  
  id: 1  
  name: 李四

2.在customer-provider工程中的 Controller 中增加@Value注解引用属性

@RestController
public class MyController {

    @Value("${person.name}")
    private String name;

    @RequestMapping("getName")
    public String getName(){
        return name;
    }
}

3.访问url测试

微服务额外组件_微服务


更新配置文件配置文件

person:  
  id: 1  
  name: 张三

重新访问:

微服务额外组件_微服务额外组件_02


发现是没有变化的,也就是说配置文件更新后,如果想读取到最新的内容还是需要重启工程的。这 样就造成了很多不便。那么怎么才能不重启更新呢?

spring boot actuator执行器刷新操作

简单介绍一下spring boot actuator,是spring boot项目运行的一个监视器服务,启动项目的 endpoints就是由spring boot actuator输出的,包含了对spring boot的bean的监视,健康状况的管 理,可以通过/actuator 查看各种项目运行的信息。

1.在customer-provider 工程的pom文件加入spring boot actuator的引用

<!--  spring boot actuator的引用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

2.修改 bootstrap.yml 配置文件,增加如下内容:

#执行器刷新操作
management:
  endpoints:
    web:
      exposure:
        include: "*"

3.在Controller上增加 @RefreshScope 注解

@RestController
@RefreshScope
public class MyController {

    @Value("${person.name}")
    private String name;

    @RequestMapping("getName")
    public String getName(){
        return name;
    }
}

4.重新启动 customer-provider 工程

5.修改配置文件后访问下面url 执行刷新操作,使用post方法执行。

//查看actuator可以执行的方法(get形式访问)
http://localhost:9001/actuator
//执行刷新操作
http://localhost:9001/actuator/refresh

返回变更信息,就刷新成功了:

微服务额外组件_微服务_03


重新访问即可获取新的信息。

这样就实现了动态刷新,但是存在一个问题。每次更新文件都需要子服务手动刷新来获取最新值,服务量一旦过大对于维护以及体验都很糟糕。所以接下来介绍与spring cloud bus搭配实现无需子服务手动 刷新即可动态获取服务端最新值。

Spring cloud bus介绍

Spring cloud bus通过轻量消息代理连接各个分布的节点。这会用在广播状态的变化(例如配置变化) 或者其他的消息指令。Spring bus的一个核心思想是通过分布式的启动器对spring boot应用进行扩展, 也可以用来建立一个多个应用之间的通信频道。目前唯一实现的方式是用AMQP消息代理作为通道,同样特性的设置(有些取决于通道的设置)在更多通道的文档中。

Spring cloud bus被国内很多都翻译为消息总线,也挺形象的。大家可以将它理解为管理和传播所有分布式项目中的消息既可,其实本质是利用了MQ的广播机制在分布式的系统中传播消息,目前常用的有 Kafka和RabbitMQ。利用bus的机制可以做很多的事情,其中配置中心客户端刷新就是典型的应用场景之一,我们用一张图来描述bus在配置中心使用的机制:

微服务额外组件_spring_04


根据此图我们可以看出利用Spring Cloud Bus做配置更新的步骤:

  • 提交代码触发post给客户端A发送bus/refresh
  • 客户端A接收到请求从Server端更新配置并且发送给Spring Cloud Bus
  • Spring Cloud bus接到消息并通知给其它客户端
  • 其它客户端接收到通知,请求Server端获取最新配置
  • 全部客户端均获取到最新的配置

RabbitMQ

由于Spring cloud bus服务需要MQ中间件,所以我们需要先安装RabbitMQ。rabbitMQ是一个在 AMQP协议标准基础上完整的,可服用的企业消息系统。它遵循Mozilla Public License开源协议,采用 Erlang 实现的工业级的消息队列(MQ)服务器,Rabbit MQ 是建立在Erlang OTP平台上。

  1. 安装Erlang
    下载地址:网盘链接 提取码:dj1l
    本文选择erlang 22.0 Windows 64-bit,安装时可以改安装路径,其他直接默认安装就可以
    注意
  • erlang不支持中文,所以主机名一定要改成英文,以及C:/Users/用户名 也要改
  • 注意erlang与rabbitmq的版本问题:详情

设置环境变量,新建 ERLANG_HOME

此电脑–>鼠标右键“属性”–>高级系统设置–>环境变量–>“新建”系统环境变量

微服务额外组件_spring_05


修改环境变量path,增加Erlang变量 %ERLANG_HOME%\bin 至path ;

微服务额外组件_SpringCloud_06


在控制台输入erl ,出现版本信息,就成功了。

  1. 安装rabbitmq

链接:网盘链接 提取码:fwvv

安装时设置以下安装目录即可,一路下一步安装完毕。然后到rabbitmq安装目录下的sbin目录 下执行如下命令来安装管理插件:

.\rabbitmq-plugins.bat enable rabbitmq_management

如果出现下面情况:

微服务额外组件_客户端_07


解决方法

C:\Users\Administrator.erlang.cookie 同步至C:\Windows\System32\config\systemprofile.erlang.cookie

同时删除:C:\Users\Administrator\AppData\Roaming\RabbitMQ目录

成功后时展现下面信息:

微服务额外组件_客户端_08


3.为了方便以后使用,可以添加RabbitMQ到系统环境变量:

//添加环境变量
名字:RABBIT_HOME  值:你的安装位置,我的是 D:\RabbitMQ\rabbitmq_server-3.8.3
在path添加 %RABBIT_HOME%\sbin;

4.重启服务

rabbitmq-server

4.访问rabbitmq的管理界面

http://127.0.0.1:15672/

用户名:guest
密码:guest

至此RabbitMQ安装完毕。

登录rabbitmq后创建新的用户并设置权限,以便客户端访问。

客户端改造

除去配置中心以外的所有服务包括 customer-consumercustomer-provider都是配置中心的客 户端,我们可以在每个工程中都增加如下内容:

pom文件

<!--  消息总线起步依赖      -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <!--  spring boot actuator的引用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

bootstrap.yml中增加如下内容:

#消息组件
spring:
  rabbitmq:
    #指定host,运行在本地可以不写
    host: localhost
    #默认服务端口
    port: 5672
    username: lbb
    password: 123456
#执行器刷新操作
management:
  endpoints:
    web:
      exposure:
        include: "*"

重启工程

修改git服务器上的配置文件,访问接口:

//bus刷新接口 post方式提交
http://localhost:9001/actuator/bus-refresh

更新配置文件后在任意客户端访问 /actuator/bus-refresh 可以刷新所有客户端配置。

改进版本

在上面的流程中,我们已经到达了利用消息总线触发一个客户端 bus/refresh ,而刷新所有客户端的配 置的目的。但这种方式并不优雅。原因如下:

  • 打破了微服务的职责单一性。
  • 微服务本身是业务模块,它本不应该承担配置刷新的职责。
  • 破坏了微服务各节点的对等性。
  • 有一定的局限性。例如,微服务在迁移时,它的网络地址常常会发生变化,此时如果想要做到自动 刷新,那就不得不修改WebHook的配置。

因此我们将上面的架构模式稍微改变一下。

微服务额外组件_微服务额外组件_09


这时Spring Cloud Bus做配置更新步骤如下:

1、提交代码触发post请求给bus/refresh

2、server端接收到请求并发送给Spring Cloud Bus

3、Spring Cloud bus接到消息并通知给其它客户端

4、其它客户端接收到通知,请求Server端获取最新配置

5、全部客户端均获取到最新的配置

升级改造:

配置中心工程的pom文件中增加如下内容:

<!--  消息总线起步依赖      -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-bus-amqp</artifactId>
        </dependency>
        <!--  spring boot actuator的引用-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>

配置中心的application.yml中增加如下内容:

#消息组件
spring:
  rabbitmq:
    #指定host,运行在本地可以不写
    host: localhost
    #默认服务端口
    port: 5672
    username: lbb
    password: 123456
#执行器刷新操作
management:
  endpoints:
    web:
      exposure:
        include: "*"

客户端工程中可以去掉对spring-boot-starter-actuator的依赖,及相关刷新配置。

重启config-centercustomer-providercustomer-consumer工程,此时若过配置发生改变,只需在配置中心刷新,而其他的客户端没有此功能,这样就把职责划分开了。

//注册中心对应端口下访问 ,执行刷新
http://localhost:9911/actuator/bus-refresh

微服务网关Zuul

微服务网关介绍

前面我们介绍了,Eureka用于服务的注册于发现,Feign支持服务的调用以及均衡负载,Hystrix处理服 务的熔断防止故障扩散,Spring Cloud Config服务集群配置中心,似乎一个微服务框架已经完成了。

我们还是少考虑了一个问题,外部的应用如何来访问内部各种各样的微服务呢?在微服务架构中,后端 服务往往不直接开放给调用端,而是通过一个API网关根据请求的url,路由到相应的服务。当添加API网 关后,在第三方调用端和服务提供方之间就创建了一面墙,这面墙直接与调用方通信进行权限控制,后 将请求均衡分发给后台服务端。

在微服务架构模式下后端服务的实例数一般是动态的,对于客户端而言很难发现动态改变的服务实例的 访问地址信息。因此在基于微服务的项目中为了简化前端的调用逻辑,通常会引入API Gateway作为轻 量级网关,同时API Gateway中也会实现相关的认证逻辑从而简化内部服务之间相互调用的复杂度。

微服务额外组件_微服务_10

Spring Cloud Zuul

在Spring Cloud体系中, Spring Cloud Zuul就是提供负载均衡、反向代理、权限认证的一个API gateway。pring Cloud Zuul路由是微服务架构的不可或缺的一部分,提供动态路由,监控,弹性,安 全等的边缘服务。Zuul是Netflix出品的一个基于JVM路由和服务端的负载均衡器。

简单使用

  1. 创建一个新的maven模块 gate-way ,pom文件中添加依赖:
<!--    网关依赖    -->		
		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
        </dependency>
  1. 创建application.yml配置文件
spring:
  application:
    name: gate-way
server:
  port: 8888
#配置路由信息
zuul:
  routes:
    baidu:
      path: /bing/**
      url: https://cn.bing.com/
  1. 编写启动类
@SpringBootApplication
@EnableZuulProxy //开启网关代理,启动类添加 @EnableZuulProxy ,支持网关路由。 
public class GateWayRunner {
    public static void main(String[] args) {
        SpringApplication.run(GateWayRunner.class,args);
    }
}
  1. 启动工程并测试,访问:http://localhost:8888/bing/search?q=121
  2. 配置路由信息增加本地微服务映射:
spring:
  application:
    name: gate-way
server:
  port: 8888
#配置路由信息
zuul:
  routes:
    baidu:
      path: /bing/**
      url: https://cn.bing.com/
    customer:
      path: /provider/**
      url: http://localhost:9001/

服务化

通过url映射的方式来实现zull的转发有局限性,比如每增加一个服务就需要配置一条内容,另外后端的 服务如果是动态来提供,就不能采用这种方案来配置了。实际上在实现微服务架构时,服务名与服务实 例地址的关系在eureka server中已经存在了,所以只需要将Zuul注册到eureka server上去发现其他服 务,就可以实现对serviceId的映射。我们还是对 gate-way 进行改造。

  1. 添加依赖,对eureka的支持。
<!--   eureka起步依赖     -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
  1. 修改配置文件

application.yml中增加Eureka的配置信息。将路由的url改为 serviceId

spring:
  application:
    name: gate-way
server:
  port: 8888
#配置路由信息
zuul:
  routes:
    baidu:
      path: /bing/**
      url: https://cn.bing.com/
    customer:
      path: /provider/**
      #url: http://localhost:9001/
      serviceId: CUSTOMER-SERVICE

#eureka
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8000/eureka
  1. 测试 访问:http://localhost:8888/provider/getName ,如果成功输出,说明通过zuul成功调用了customer-client服务,如果要是多个服务集群的话还可以实现负载均衡 。

默认路由规则

但是如果后端服务多达十几个的时候,每一个都这样配置也挺麻烦的,spring cloud zuul已经帮我们做 了默认配置。默认情况下,Zuul会代理所有注册到Eureka Server的微服务,并且Zuul的路由规则如下: http://ZUUL_HOST:ZUUL_PORT/微服务在Eureka上的serviceId/** 会被转发到serviceId对应的微服务。

访问url:

//注意url中的server-id应该是小写字母。 
http://localhost:8888/customer-service/getName

过滤器

其实Zuul还有更多的应用场景,比如:鉴权、流量转发、请求统计等等,这些功能都可以使用Zuul来实 现。Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是 “PRE”、 “ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示。

微服务额外组件_spring_11


Zuul大部分功能都是通过过滤器来实现的,这些过滤器类型对应于请求的典型生命周期。

  • PRE: 这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择 请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient或Netfilx Ribbon请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul还允许我们创建自 定义的过滤器类型。例如,我们可以定制一种STATIC类型的过滤器,直接在Zuul中生成响应,而 不将请求转发到后端的微服务。

6.1 自定义Filter

实现自定义Filter,需要继承ZuulFilter的类,并覆盖其中的4个方法。

public class TokenFilter extends ZuulFilter {
    //定义filter的类型,有pre、route、post、error四种
    @Override
    public String filterType() {
        return "pre"; 
    }
	//定义filter的顺序,数字越小表示顺序越高,越先执行
    @Override
    public int filterOrder() {
        return 10; 
    }
	//表示是否需要执行该filter,true表示执行,false表示不执行 
    @Override
    public boolean shouldFilter() {
        return true; 
    }
	//filter需要执行的具体操作 
    @Override
    public Object run() throws ZuulException {
        return null; 
    }
}

6.2 自定义Filter示例

我们假设有这样一个场景,因为服务网关应对的是外部的所有请求,为了避免产生安全隐患,我们需要 对请求做一定的限制,比如请求中含有Token便让请求继续往下走,如果请求不带Token就直接返回并 给出提示。

首先自定义一个Filter,在run()方法中验证参数是否含有Token。

public class TokenFilter extends ZuulFilter {
    @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 {
        //获取request看是否含token
        //请求上下文
        RequestContext ctx = RequestContext.getCurrentContext();
        //获取请求对象
        HttpServletRequest request = ctx.getRequest();
        // 获取请求的参数
        String token = request.getParameter("token");
        //token无效
        if (token == null || token.length() < 1){
            //不对其进行路由转发
            ctx.setSendZuulResponse(false);
            //状态码
            ctx.setResponseStatusCode(400);
            //返回错误信息
            ctx.setResponseBody("token is empty");
            ctx.set("isSuccess", false);
            return null;
        }else {//token有效
            //不对其进行路由转发
            ctx.setSendZuulResponse(true);
            //状态码
            ctx.setResponseStatusCode(200);
            ctx.set("isSuccess", true);
            return null;
        }
    }
}

TokenFilter加入到请求拦截队列,修改启动类:

@SpringBootApplication
@EnableZuulProxy //开启网关代理
public class GateWayRunner {
    public static void main(String[] args) {
        SpringApplication.run(GateWayRunner.class,args);
    }
    @Bean
    public TokenFilter getTokenFilter(){
        return new TokenFilter();
    }
}

这样就将我们自定义好的Filter加入到了请求拦截中。

测试

访问地址: http://localhost:8888/customer-service/getName ,返回:token is empty ,请求被拦截返回。

访问地址: http://localhost:8888/customer-service/getName?token=xx ,返回:json数据,说明请求正常响应。

通过上面这例子我们可以看出,我们可以使用“PRE”类型的Filter做很多的验证工作,在实际使用中我们 可以结合shiro、oauth2.0等技术去做鉴权、验证。