5

spring cloud面向开发人员,对分布式系统从编程模型上提供了强大的支持。可以说是分布式系统解决方案的全家桶,极大地降低了开发与构建分布式系统的门槛。

包括了诸如下列功能:

  • Eureka服务注册发现
  • 统一配置中心
  • Spring Cloud Stream异步调用
  • Zuul服务网关
  • Hystrix服务降级容错
  • 服务调用跟踪

Netflix是一家互联网流媒体播放商,美国视频巨头,最近买下《流浪地球》,并在190多个国家播出的就是它了。随着Netflix转型为一家云计算公司,也开始积极参与开源项目。Netflix OSS(Open Source)就是由Netflix公司开发的框架,解决上了规模之后的分布式系统可能出现的一些问题。spring cloud基于spring boot,为spring boot提供Netflix OSS的集成。

这段时间对spring cloud进行了学习总结,留下点什么,好让网友上手spring cloud时少躺坑。

我用的版本如下:

  • spring-boot 2.0.2.RELEASE
  • spring-cloud Finchley.RELEASE

spring boot不同版本之间在配置和依赖上会有差异,强烈建议创建工程后先把spring boot和spring cloud的版本依赖调整成与本篇一致,快速上手少躺坑。

上手完后,你可以尝试升到最新版本,官方spring-cloud页(进去后拉到最下面)提供了spring-cloud与spring-boot版本匹配表。升级完后建议重新测试一下相关功能。

Eureka服务注册与发现

Eureka Server

IntelliJ IDEA(我是2018.2.5的版本),创建工程eureka-server:

File -> New->Product... -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Server(springboot版本选个2.0以上的)

注意创建工程后调整spring boot和spring cloud的版本:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.2.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
...
<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
</properties>

由于选择了Eureka Server,创建成功后pom.xml里已经帮你引入了以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

我们给启动类加上注解@EnableEurekaServer:

package com.hicoview.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

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

}

配置application.yml(习惯yml风格的同学,把application.properties直接改过来):

eureka:
  client:
    # 默认eureka服务注册中心会将自身作为客户端来尝试注册,所以我们需要禁用它的客户端注册行为
    register-with-eureka: false
    # 默认30秒会更新客户端注册上来的服务清单,启动时就不获取了,不然启动会有报错,虽然不影响
    fetch-registry: false
  server:
    # 关闭注册中心自我保护(默认是true,生产环境不建议关闭,去掉该配置项或改成true)
    enable-self-preservation: false
spring:
  application:
    name: eureka
server:
  port: 8761

服务注册中心搞定,启动成功后访问http://localhost:8761,可以看到注册中心页面:

clipboard.png

Eureka Server高可用

只要eureka-server以不同端口启动多个实例即可,多个实例两两注册到对方。比如,
以8761端口启动一个Eureka Server端,注册到8762:

eureka:
  client:
    service-url:
      # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去
      defaultZone: http://localhost:8762/eureka/
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false
spring:
  application:
    name: eureka
server:
  port: 8761

以8762端口启动一个Eureka Server端,注册到8761:

eureka:
  client:
    service-url:
      # 提供其他注册中心的地址,注册中心将自身以客户端注册的方式注册到其他注册中心去
      defaultZone: http://localhost:8761/eureka/
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false
spring:
  application:
    name: eureka
server:
  port: 8762

访问http://localhost:8761,可以看到有8762的副本:
clipboard.png

访问http://localhost:8762,也一样有8761的副本。

接下来我们用Eureka Client来验证下服务的注册。

Eureka Client

实际上充当Eureka Client角色应该是各种业务的微服务工程了,这里为了快速演示一下服务注册,临时先搞个无意义的eureka-client工程示范。

创建Eureka Client工程eureka-client:

File -> New->Product... -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next -> 选择Cloud Discovery -> 选择Eureka Discovery(springboot版本选2.0以上)

注意每次创建工程后的第一件事,改spring-boot和spring-cloud的版本,不再赘述

由于选择了Eureka Discovery,创建成功后pom.xml里已经帮你引入了以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

修改pom.xml,还要加入web依赖,不然无法启动成功:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

启动类加注解@EnableDiscoveryClient:

package com.hicoview.client;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ClientApplication {

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

}

配置application.yml:

eureka:
  client:
    service-url:
      # 注册中心地址,如果注册中心是高可用,那么这里后面可以添加多个地址,逗号分开
      defaultZone: http://localhost:8761/eureka/
      #defaultZone: http://localhost:8761/eureka/,http://localhost:8762/eureka/
spring:
  application:
    name: client

启动成后,访问注册中心http://localhost:8761
clipboard.png

完成了Eureka服务注册示例,接下来我们简单模拟一个业务场景,示范微服务之间的服务调用。

微应用之间的服务调用

服务调用示例

以商品下单为例,比如将业务拆分为商品服务和订单服务,订单服务会调用商品服务的库存扣减。

单个微服务工程,统一按以下目录编排:

-product
--product-common  商品服务公用对象
--product-client  商品服务客户端,以jar包方式被订单服务依赖
--product-server  商品服务,要注册到Eureka Server,外部通过product-client来调用

我们的微服务工程之间,依赖关系如下:
clipboard.png

我们从商品微服务工程开始

外层product初始pom.xml:

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.0.2.RELEASE</version>
    <relativePath/>
</parent>
<groupId>com.hicoview</groupId>
<artifactId>product</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>product</name>
<description>Demo project for Spring Boot</description>
<modules>
    <module>product-common</module>
    <module>product-client</module>
    <module>product-server</module>
</modules>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Finchley.RELEASE</spring-cloud.version>
    <product-common.version>0.0.1-SNAPSHOT</product-common.version>
</properties>

<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>
        <dependency>
            <groupId>com.hicoview</groupId>
            <artifactId>product-common</artifactId>
            <version>${product-common.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

product-common初始pom.xml:

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>com.hicoview</groupId>
    <artifactId>product</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-common</artifactId>

<dependencies>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
    </dependency>
</dependencies>

product-client初始pom.xml:

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>com.hicoview</groupId>
    <artifactId>product</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-client</artifactId>

<dependencies>
    <dependency>
        <groupId>com.hicoview</groupId>
        <artifactId>product-common</artifactId>
    </dependency>
</dependencies>

product-server的初始pom.xml:

<modelVersion>4.0.0</modelVersion>
<parent>
    <groupId>com.hicoview</groupId>
    <artifactId>product</artifactId>
    <version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>product-server</artifactId>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.hicoview</groupId>
        <artifactId>product-common</artifactId>
    </dependency>
</dependencies>

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

接下来我们调整product-server工程

商品服务需注册到Eureka Server,添加Eureka Client相关依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

启动类加注解@EnableDiscoveryClient:

package com.hicoview.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {

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

}

配置application.yml:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: product
server:
  port: 8080

启动product-server,访问注册中心http://localhost:8761,PRODUCT注册上去了,再继续往下看。

我们写个简单的服务调用示例一下,订单服务下单逻辑调用商品服务进行扣减库存。

继续修改product-server工程

package com.hicoview.product.service;

import java.util.List;

public interface ProductService {
    // 扣减库存
    void decreaseStock();
}
package com.hicoview.product.service.impl;

import com.hicoview.product.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class ProductServiceImpl implements ProductService {

    @Override
    public void decreaseStock() {
        log.info("------扣减库存-----");
    }
}

spring cloud的RPC服务是使用HTTP方式调用的,所以还要创建ProductController:

package com.hicoview.product.controller;

import com.hicoview.product.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/product")
public class ProductController {

    @Autowired
    private ProductService productService;

    @PostMapping("/decreaseStock")
    public void decreaseStock() {
        productService.decreaseStock();
    }

}

接下来修改product-client工程

pom.xml增加Feign依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

创建ProductClient:

package com.hicoview.product.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;

@FeignClient(name="product")
public interface ProductClient {
    
    // 通过Feign来代理对PRODUCT服务的HTTP请求
    @PostMapping("/product/decreaseStock")
    void decreaseStock();

}

再次启动product-server,没问题的话,把product上传到本地maven仓库:

mvn -Dmaven.test.skip=true -U clean install

然后初始化订单微服务工程后继续。

修改order-server工程

order-server的服务可能会被User等其他服务调用,也是个Eureka client,添加依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-web</artifactId>
</dependency>

启动类加注解@EnableDiscoveryClient

package com.hicoview.product;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class ProductApplication {

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

}

配置application.yml:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: order
server:
  port: 8081

启动order-server,访问注册中心http://localhost:8761/
clipboard.png

ORDER服务注册成功后,继续调整order-server工程

由于订单服务要调用商品服务,需添加对product-client依。修改pom.xml:

<dependency>
    <groupId>com.hicoview</groupId>
    <artifactId>product-client</artifactId>
</dependency>

对版本的管理统一交给上层,修改上层order的pom.xml,增加以下配置:

<properties>
    ...
    <product-client.version>0.0.1-SNAPSHOT</product-client.version>
</properties>

<dependencyManagement>
    <dependencies>
        ...
        <dependency>
            <groupId>com.hicoview</groupId>
            <artifactId>product-client</artifactId>
            <version>${product-client.version}</version>
        </dependency>
    </dependencies>
</dependencyManagement>

回到order-server,修改启动类,添加Feign扫描路径:

package com.hicoview.order;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "com.hicoview.product.client")
public class OrderApplication {

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

}

然后按顺序创建Controller、Service:

package com.hicoview.order.controller;

import com.hicoview.order.service.OrderService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/order")
public class OrderController {

    @Autowired
    private OrderService orderService;

    // 创建订单
    @PostMapping("/create")
    public void create() {
        orderService.createOrder();
    }

}
public interface OrderService {
    void createOrder();
}
@Service
@Slf4j
public class OrderServiceImpl implements OrderService {

    @Autowired
    private ProductClient productClient;

    @Override
    public void createOrder() {
        log.info("------创建订单-----");
        // 调用商品扣减服务
        productClient.decreaseStock();
    }
}

启动order-server,发起下单请求:

curl -X POST http://localhost:8081/order/create

服务调用成功,order-server和product-server输出:

2019-02-26 11:09:58.408  INFO 3021 --- [nio-8081-exec-3] c.h.order.service.impl.OrderServiceImpl  : ------创建订单-----
2019-02-26 11:09:58.430  INFO 3015 --- [nio-8080-exec-3] c.h.p.service.impl.ProductServiceImpl    : ------扣减库存-----

为了示例的极简,上面服务调用没有涉及到入参和返回值。如果需要定义参数或返回值,考虑到内外部都会用到,需将参数bean定义在product-common中作为公共bean。


Feign和RestTemplate

Rest服务的调用,可以使用Feign或RestTemplate来完成,上面示例我们使用了Feign。

Feign(推荐)

Feign是一个声明式的Web Service客户端,它的目的就是让Web Service调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。

Feign会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。Feign整合了Ribbon和Hystrix(关于Hystrix我们后面再讲),可以让我们不再需要显式地使用这两个组件。

RestTemplate

RestTemplate提供了多种便捷访问远程Http服务的方法,可以了解下。

第一种方式,直接使用RestTemplate,URL写死:

// 1. 
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject("http://localhost:8080/product/decreaseStock", String.class);

这种方式直接使用目标的IP,而线上部署的IP地址可能会切换,同一个服务还会启多个进程,所以有弊端。

第二种方式,利用LoadBalancerClient,通过应用名获取URL,然后再使用RestTemplate:

@Autowired
private LoadBalancerClient loadBalancerClient;
// 1. 第二种方式,通过应用名字拿到其中任意一个host和port
ServiceInstance serviceInstance = loadBalancerClient.choose("PRODUCT");
String url = String.format("http://%s:%s", serviceInstance.getHost(), serviceInstance.getPort() + "/product/decreaseStock");
RestTemplate restTemplate = new RestTemplate();
restTemplate.getForObject(url, String.class);

第三种方式,写一个config把RestTemplate作为一个bean配置上去:

@Component
public class RestTemplateConfig {
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}
@Autowired
private RestTemplate restTemplate;
// 使用时url里直接用应用名PRODUCT即可
restTemplate.getForObject("http://PRODUCT/product/decreaseStock", String.class);

Ribbon客户端负载均衡器

Spring Cloud Ribbon是一个基于HTTP和TCP的客户端负载均衡工具,它基于Netflix Ribbon实现。通过Spring Cloud的封装,可以让我们轻松地将面向服务的REST模版请求自动转换成客户端负载均衡的服务调用。Spring Cloud Ribbon虽然只是一个工具类框架,它不像服务注册中心、配置中心、API网关那样需要独立部署,但是它几乎存在于每一个Spring Cloud构建的微服务和基础设施中。

在Spring Cloud中,Ribbon可自动从Eureka Server获取服务提供者地址列表,并基于负载均衡算法,请求其中一个服务提供者实例。还有微服务之间的调用,通过Feign或RestTemplate找到一个目标服务。以及API网关Zuul的请求转发等内容,实际上都是通过Ribbon来实现的。

微应用之间的异步交互

AmqpTemplate

MQ常用于分布式系统应用解耦,流量销峰、异步处理的场景。spring-cloud集成了RabbitMQ,使用AmqpTemplate对消息进行发送。前面介绍Spring Cloud Bus时已安装过RabbitMQ了。

我们尝试在product-server扣减库存时发送消息,由order-server订阅。

修改product-server,引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>

调整productServiceIml:

@Service
@Slf4j
public class ProductServiceImpl implements ProductService {

    @Autowired
    private AmqpTemplate amqpTemplate;

    @Override
    public void decreaseStock() {
        log.info("------扣减库存-----");
        // 使用AmqpTemplate发送异步消息
        amqpTemplate.convertAndSend("productInfo", "库存扣减消息");
    }
}

接下来是订阅方order-server,引入依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
package com.hicoview.order.message;

import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class MessageReceiver {

    @RabbitListener(queuesToDeclare = @Queue("productInfo"))
    public void process(String message) {
        log.info("MqReceiver: {}", message);
    }

}

下单测试下:

curl -X POST http://localhost:8081/order/create

order-server控制台输出:

2019-02-27 11:27:25.777  INFO 7816 --- [cTaskExecutor-1] c.h.order.message.MessageReceiver        : MqReceiver: 库存扣减消息

Spring Cloud Stream

各种各样的消息队列的产生和更新,使MQ组件学习成本越来越高,String Cloud Stream为一些供应商的消息中间件产品(目前支持rabbit和kafka)提供了个性化的自动化配置,引入发布订阅、消费组、以及分区这3个概念,有效的简化了上层研发人员对MQ使用的复杂度,让开发人员更多的精力投入到核心业务的处理。

统一配置中心

Spring Cloud Config为各应用环境提供了一个中心化的外部配置。配置服务器默认采用git来存储配置信息,这样就有助于对配置进行版本管理,并且可以通过git客户端工具来方便维护配置内容。当然它也提供本地化文件系统的存储方式。

使用集中式配置管理,在配置变更时,可以通知到各应用程序,应用程序不需要重启。

Config Server

创建Config Server端工程config-server:

File -> New->Product... -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next
(springboot选择2.0以上)
选择Cloud Discovery -> 选择Eureka Discovery
选择Cloud Config -> 选择Config Server

由于选择了Eureka Discovery和Config Server,创建成功后pom.xml里已经帮你引入了以下依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-server</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

Config Server也是要注册到Eureka,作为Eureka Client,我们还要加入如下依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 避免后面的数据库配置出错,mysql依赖也加了 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

再给启动类加上注解@EnableDiscoveryClient和@EnableConfigServer:

package com.hicoview.config;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.config.server.EnableConfigServer;

@SpringBootApplication
@EnableDiscoveryClient
@EnableConfigServer
public class EurekaApplication {

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

}

配置application.yml:

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
spring:
  application:
    name: config
  cloud:
    config:
      server:
        git:
          uri: http://code.hicoview.com:8000/backend/config.git
          username: root
          password: 8ggf9afd6g9gj
          # 配置文件下载后存储的本地目录
          basedir: /Users/zhutx/springcloud/config-server/basedir
server:
  port: 9999

然后按照配置的git仓库地址,在github或gitlab上创建config仓库。

以商品微服务的配置来演示,在仓库创建product-dev.yml:

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: passwd_1986
    url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false

启动作为Config Server的config-server工程,查看注册中心http://localhost:8761

clipboard.png

访问config-server的以下任一地址,都可以显示出对应格式的配置内容:
http://localhost:9999/product-dev.yml
http://localhost:9999/product-dev.properties
http://localhost:9999/product-dev.json

接下来我们看看配置客户端Config Client(即product-server)怎么引用配置。

Config Client

我们给product-server加入配置客户端的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-config-client</artifactId>
</dependency>

修改application.yml配置:

spring:
  application:
    name: product
  cloud:
    config:
      discovery:
        enabled: true
        service-id: CONFIG
      profile: dev
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

这样子就可以从Eureka服务注册中心找到CONFIG服务,并拿到product-dev.yml了。

启动product-server,查看Eurake注册中心,PRODUCT服务成功注册:

clipboard.png

如果代码里有操作数据库,那么启动时会初始化数据库连接,这种情况下启动其实会出错,因为spring boot不知道配置加载顺序。

我们期望是先拿到CONFIG的配置,再初始化数据库。

解决办法是把product-server的application.yml改成bootstrap.yml就好,让本地配置项优先级更高。

微服务工程使用配置服务的情况下,注意将application.yml都改成bootstrap.yml。并且,让bootstrap.yml文件只保留Eureka Client的配置和获取CONFIG的配置;

另外,如果生产环境要使用统一配置中心,可以启动多个Config Server进程,保持高可用。


同样的操作,把order-server配置也抽取到外部


Spring Cloud Bus

下图是当前的配置工作机制,config-server拉取远端git配置,并在本地存一份。然后config-server通过把自身注册到Eureka从而提供了拉取配置的服务,而配置客户端(product和order)通过引入config-client依赖,在启动时便能获取加载到了配置。

clipboard.png
我们需要做到修改远程配置,应用程序不重启,还需要借助Spring Cloud Bus。Spring Cloud Bus集成了RabbitMQ,并为config-server自动引入配置刷新服务(bus-refresh)。

如下图所示,做法是远端git修改配置后,主动调用config-server的/bus-refresh服务,发布RabbitMQ消息,config-client接收消息并更新配置。

clipboard.png

我们先安装RabbitMQ:

# docker安装rabbitmq
docker run -d --hostname my-rabbit -p 5672:5672 -p 15672:15672 rabbitmq:3.7.9-management
# 验证下
docker ps | grep 'rabbitmq'

能成功访问RabbitMQ控制台http://localhost:15671,继续。

修改config-server,增加依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

修改application.yml,增加以下配置,把包括bus-refresh在内的所有config server的服务都暴露出来:

management:
  endpoints:
    web:
      exposure:
        include: "*"

我们拿product-server来演示,修改product-server的pom增加依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

然后为product-server提供一个WEB接口,方便我们在配置变化时验证是否已加载新的配置:

@RestController
@RequestMapping("/env")
@RefreshScope
public class EnvController {

    @Value("${env}")
    private String env;

    @GetMapping("/print")
    public String print() {
        return env;
    }
}

测试下,我们修改远端git配置,先增加env配置:

server:
  port: 8080
spring:
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    username: root
    password: passwd_1986
    url: jdbc:mysql://127.0.0.1:3306/SpringCloud_Sell?characterEncoding=utf-8&useSSL=false
# 增加了该配置
env:
  dev

重启下config-server和product-server。

访问 product-server http://localhost:8080/env/print,显示dev

然后我们把远端env配置项改成test,调用config-server的配置刷新服务 bus-refresh:

curl -v -X POST http://localhost:9999/actuator/bus-refresh

再次访问 product-server http://localhost:8080/env/print,显示test

至此,已经做到了变更配置不重启应用。我们再借助Git仓库的webhook功能,在push指令发生后帮我们发个bus-refresh请求就完美了。Gitlab的话在仓库的这个位置:

Repository -> Settings -> Integrations -> Add webhook

clipboard.png

服务网关Zuul

Zuul简介

Zuul 是在云平台上提供动态路由,监控,弹性,安全等边缘服务的框架。Zuul 相当于是设备和 Netflix 流应用的 Web网站后端所有请求的前门。如下图:

clipboard.png

Filter是Zuul的核心,用来实现对外服务的控制。Filter的生命周期有4个,分别是“PRE”、“ROUTING”、“POST”、“ERROR”,整个生命周期可以用下图来表示:

clipboard.png

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

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

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 处理正常的请求响应

Zuul高并发性能不如Nginx,我们可以使用Zuu微服务网关实现统一鉴权、限流、参数校验、请求参数和返回结果包装,结合Nginx作为前置负载均衡服务器,构建如下图所示的微服务架构:

clipboard.png

使用Zuul微服务网关

创建网关工程api-gateway:

File -> New->Product... -> 选择Spring Initializr -> Project SDK用1.8 -> Next -> 输入Product Metadata -> Next
(springboot选择2.0以上)
选择Cloud Discovery -> 选择Eureka Discovery
选择Cloud Config -> 选择Config Client
选择Cloud Routing -> 选择Zuul

按以上配置,已经帮我们引入了以下依赖:

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-config</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-config-client</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

增加依赖:

<!-- 作为Eureka Client,需增加web依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 作为Config Client,需添加spring cloud bus依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-bus-amqp</artifactId>
</dependency>

启动类添加@EnableZuulProxy注解:

@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
    
    // 远程Zull配置变更后,自动载入新配置
    @ConfigurationProperties("zuul")
    @RefreshScope
    public ZuulProperties zuulProperties() {
        return new ZuulProperties();
    }

}

Git中央仓库添加api-gateway-dev.yml

server:
  port: 9000
zuul:
  # 全部服务忽略敏感头(可传递cookie)
  sensitive-headers: 

默认路由规则,比如访问product微服务的/product/list,需要请求/product/product/list

server:
  port: 9000
zuul:
  # 全部服务忽略敏感头(可传递cookie)
  sensitive-headers: 
management:
  endpoints:
    web:
      exposure:
        # 通过http://localhost:9000/actuator/routes 可以查看路由规则
        include: "*"

修改路由规则:

server:
  port: 9000
zuul:
  # 全部服务忽略敏感头(全部服务都可传递cookie)
  sensitive-headers: 
  # 修改网关路由规则
  routes:
    # /myProduct/product/list -> /product/product/list
    product: /myProduct/**
  # 禁用特定的一些路由请求
  ignored-patterns:
    - /order/order/list
    - /order/order/create
    # - /**/order/list
management:
  endpoints:
    web:
      exposure:
        include: "*"

自定义Filter实现鉴权

@Component
public class AuthFilter extends ZuulFilter {
    
    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    // 鉴权是前置过滤器
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    @Override
    public int filterOrder() {
        return PRE_DECORATION_FILTER_ORDER - 1;
    }

    @Override
    public boolean shouldFilter() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        // 除登录外的其他请求,都要进行权限校验
        if (!"/login".equals(request.getRequestURI())) {
            return true;
        }
        
        return false;
    }

    @Override
    public Object run() {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();

        // 假设请求url里不携带cookie就看做未登录
        Cookie cookie = CookieUtil.get(request, "token");
        if (cookie == null || StringUtils.isEmpty(cookie.getValue())) {
            requestContext.setSendZuulResponse(false);
            requestContext.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
        }
        
        return null;
    }
}

自定义Filter实现限流

直接用google的guava组件的令牌桶实现限流
package com.hicoview.apigateway.filter;

import com.google.common.util.concurrent.RateLimiter;
import com.hicoview.apigateway.exception.RateLimitException;
import com.netflix.zuul.ZuulFilter;
import org.springframework.stereotype.Component;

import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.PRE_TYPE;
import static org.springframework.cloud.netflix.zuul.filters.support.FilterConstants.SERVLET_DETECTION_FILTER_ORDER;

@Component
public class RateLimitFilter extends ZuulFilter {

    // 每秒限流1000
    private static final RateLimiter RATE_LIMITER = RateLimiter.create(1000);

    // 限流作为前置过滤器
    @Override
    public String filterType() {
        return PRE_TYPE;
    }

    // 作为限流,优先级要比最高的还要高
    @Override
    public int filterOrder() {
        return SERVLET_DETECTION_FILTER_ORDER - 1;
    }

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

    @Override
    public Object run() {
        if (!RATE_LIMITER.tryAcquire()) {
            throw new RateLimitException();
        }

        return null;
    }
}

配置允许域访问

@Configuration
public class CorsConfig {

    @Bean
    public CorsFilter corsFilter() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        final CorsConfiguration config = new CorsConfiguration();

        config.setAllowCredentials(true);
        config.setAllowedOrigins(Arrays.asList("*"));
        config.setAllowedHeaders(Arrays.asList("*"));
        config.setAllowedMethods(Arrays.asList("*"));
        config.setMaxAge(300l);

        source.registerCorsConfiguration("/**", config);
        return new CorsFilter(source);
    }

}

zhutianxiang
1.5k 声望328 粉丝