SpringCloud筆記

spring cloud面向開發人員,對分佈式系統從編程模型上提供了強大的支持。能夠說是分佈式系統解決方案的全家桶,極大地下降了開發與構建分佈式系統的門檻。java

包括了諸以下列功能:mysql

  • Eureka服務註冊發現
  • 統一配置中心
  • Spring Cloud Stream異步調用
  • Zuul服務網關
  • Hystrix服務降級容錯
  • 服務調用跟蹤

Netflix是一家互聯網流媒體播放商,美國視頻巨頭,最近買下《流浪地球》,並在190多個國家播出的就是它了。隨着Netflix轉型爲一家雲計算公司,也開始積極參與開源項目。Netflix OSS(Open Source)就是由Netflix公司開發的框架,解決上了規模以後的分佈式系統可能出現的一些問題。spring cloud基於spring boot,爲spring boot提供Netflix OSS的集成。git

這段時間對spring cloud進行了學習總結,留下點什麼,好讓網友上手spring cloud時少躺坑。github

我用的版本以下:web

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

spring boot不一樣版本之間在配置和依賴上會有差別,強烈建議建立工程後先把spring boot和spring cloud的版本依賴調整成與本篇一致,快速上手少躺坑。算法

上手完後,你能夠嘗試升到最新版本,官方spring-cloud頁(進去後拉到最下面)提供了spring-cloud與spring-boot版本匹配表。升級完後建議從新測試一下相關功能。spring

Eureka服務註冊與發現

Eureka Server

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

File -> New->Product... -> 選擇Spring Initializr -> Project SDK用1.8 -> Next -> 輸入Product Metadata -> Next -> 選擇Cloud Discovery -> 選擇Eureka Server(springboot版本選個2.0以上的)

注意建立工程後調整spring boot和spring cloud的版本:docker

<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裏已經幫你引入瞭如下依賴:shell

<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);
    }

}
相關文章
相關標籤/搜索