SpringCloud服務發現註冊Eureka +Ribbon + Feign

 在本期將學習如下知識點:

  • 什麼是服務註冊和發現?
  • 基於Eureka的註冊服務器
  • 服務生產者
  • 結合Ribbon服務消費者
  • 結合Feign的服務生產者和消費者

什麼是服務註冊和發現

   假設有2個微服務A和B分別在端點http:// localhost:8181 /和http:// localhost:8282 /上運行,若是想要在A服務中調用B服務,那麼咱們須要在A服務中鍵入B服務的url,這個url是負載均衡器分配給咱們的,包括負載平衡後的IP地址,那麼很顯然,B服務與這個URL硬編碼耦合在一塊兒了,若是咱們使用了服務自動註冊機制,就可使用B服務的邏輯ID,而不是使用特定IP地址和端口號來調用服務。java

   咱們可使用Netflix Eureka Server建立Service Registry服務器,並將咱們的微服務同時做爲Eureka客戶端,這樣一旦咱們啓動微服務,它將自動使用邏輯服務ID向Eureka Server註冊。而後,其餘微服務(一樣也是Eureka客戶端)就可使用服邏輯務ID來調用REST端點服務了。git

   Spring Cloud使用Load Balanced RestTemplate建立Service Registry並發現其餘服務變得很是容易。github

   除了使用Netflix Eureka Server做爲服務發現,也可使用Zookeeper,可是根據CAP定理,在須要P網絡分區容忍性狀況下,強一致性C和高可用性A只能選擇一個,Zookeeper是屬於CP,而Eureka是屬於AP,在服務發現方面,高可用性纔是更重要,不然沒法完成服務之間調用,而服務信息是否一致則不是最重要,A服務發現B服務時,B服務信息沒有及時更新,可能發生調用錯誤,可是調用錯誤總比沒法鏈接到服務註冊中心要強。不然,服務註冊中心就成爲整個系統的單點故障,存在極大的單點風險,這是咱們爲何須要分佈式系統的首要緣由。web

基於Eureka的註冊服務器

  讓咱們使用Netflix Eureka建立一個Service Registry,它只是一個帶有Eureka Server啓動器的SpringBoot應用程序。spring

  使用Intellij的Idea開發工具是很是容易啓動Spring cloud的:數據庫

能夠從https://start.spring.io/網址,選擇相應組件便可。bash

因爲咱們須要創建一個註冊服務器,所以選擇Eureka Server組件便可,經過這些自動工具其實是能自動生成Maven的配置:服務器

org.springframework.cloud網絡

spring-cloud-starter-netflix-eureka-server併發

咱們須要給SpringBoot啓動類添加**@EnableEurekaServer**註釋,以使咱們的SpringBoot應用程序成爲基於Eureka Server的Service Registry。

import org.springframework.boot.SpringApplication;

import org.springframework.boot.autoconfigure.SpringBootApplication;

import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer

@SpringBootApplication

public class ServiceRegistryApplication {

public static void main(String[] args) {

SpringApplication.run(ServiceRegistryApplication.class, args);

}

}

默認狀況下,每一個Eureka服務器也是Eureka客戶端,客戶端必定會須要一個服務器URL來定位,不然就會不斷報錯,因爲咱們只有一個Eureka Server節點(獨立模式),咱們將經過在application.properties文件中配置如下屬性來禁用此客戶端行爲。

SpringCloud有properties和YAML兩種配置方式,這兩種配置方式其實只是形式不一樣,properties配置信息格式是a.b.c,而YAML則是a:b:c:,二者本質是同樣的,只須要其中一個便可,這裏以properties爲案例:

spring.application.name=jdon-eureka-server
server.port=1111
eureka.instance.hostname=localhost
eureka.client.register-with-eureka=false
eureka.client.fetch-registry=false
複製代碼

如今運行ServiceRegistryApplication並訪問http:// localhost:1111,若是不能訪問,說明沒有正常啓動,請檢查三個環節:pom.xml是否配置正確?須要Eureka和配置

SpringBoot的註釋@EnableEurekaServer是否增長了?

最後,application.properties是否配置?

SpringCloud其實很是簡單,約定大於配置,默認只要配置服務器端口就能夠了,而後是一條註釋**@EnableEurekaServer**,就能啓動Eurek服務器了。

服務器準備好後,咱們就要準備服務生產者,向服務器裏面註冊本身,服務消費者則是從服務器中發現註冊的服務而後調用。

服務生產者

   服務生產者其實首先是Eureka的客戶端,生產者將本身註冊到前面啓動的服務器當中,引若是是idea的導航,選擇CloudDiscovery的EurekaDiscovery,若是是 Maven則引入包依賴是:

org.springframework.cloud

spring-cloud-starter-netflix-eureka-client

這樣,spring-cloud-starter-netflix-eureka-client這個jar包就放入咱們系統的classpath,爲了可以正常使用這個jar包,還須要配置,只須要在application.properties中配置eureka.client.service-url.defaultZone屬性便可自動註冊Eureka Server:

eureka.client.service-url.defaultZone=http://localhost:1111/eureka/

當咱們的服務在Eureka Server註冊時,它會持續發送必定時間間隔的心跳。若是Eureka服務器沒有從任何服務的實例接收到心跳,它將認爲這個服務實例已經關閉並從本身的池中剔除它。

以上是服務生產者註冊服務的過程,比較簡單,爲了使咱們的服務生產者能的演示代碼夠運行起來,咱們還須要新建一個服務生產者代碼:

@RestController
public class ProducerService {

    @GetMapping("/pengproducer")
    public String sayHello(){
        return "hello world";
    }
}
複製代碼

這段代碼是將服務暴露成RESTful接口,@RestController是聲明Rest接口,/pengproducer是REST的訪問url,經過get方式可以得到字符串:hello world

由於REST屬於WEB的一種接口,所以須要在pom.xml中引入Web包:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
複製代碼

而後在application.properties中加入有關REST接口的配置:

spring.application.name=PengProducerService
server.port=2111
複製代碼

指定咱們的生產者服務的名稱是PengProducerService,REST端口開在2111。

如今能夠在idea中啓動咱們的應用了,這樣咱們啓動這個項目,就能夠在http://127.0.0.1:2111/ 訪問這個REST服務。同時,由於咱們以前已經啓動了註冊服務器,訪問http://localhost:1111/你會發現PengProducerService出如今服務列表中:

上面啓動應用服務是在idea編輯器中,咱們還能夠經過命令行啓動咱們的服務生產者:

java -jar -Dserver.port=2112 producer-0.0.1-SNAPSHOT.jar

這個是在端口2112開啓咱們的服務端點了。如今再問http://localhost:1111/,你會看到可用節點Availability Zones下面已經從(1)變爲(2),如今咱們的服務生產者已經有兩個實例在運行,當服務的消費者訪問這個兩個實例時,它能夠根據負載平衡策略好比輪詢訪問其中一個服務生產者實例。

總結一下,爲了讓服務生產者註冊到Euraka服務器中,只須要兩個步驟:

  • 1. 引入spring-cloud-starter-netflix-eureka-client包
  • 2. 配置Eurake服務器的地址

請注意,spring-cloud-starter-netflix-eureka-client包是Spring Cloud升級後最新的包名,原來是spring-cloud-starter-eureka,裏面沒有netflix,這是過去版本,Spring Boot 1.5之後都是加入了netflix的,見Spring Cloud Edgware Release Notes

另外,這裏不須要在SpringBoot主代碼中再加入@enablediscoveryclient 或 @enableeurekaclient,只要eureka的client包在maven中配置,也就會出如今系統的classpath中,這樣就會默認自動註冊到eureka服務器中了。

這部分源碼下載:百度網盤

下面咱們準備訪問這個服務生產者PengProducerService的消費者服務:

結合Ribbon的服務消費者

   上個章節咱們已經啓動了兩個服務生產者實例,如何經過負載平衡從兩個中選擇一個訪問呢?這時就須要Ribbon,爲了使用Ribbon,咱們須要使用@LoadBalanced元註解,那麼這個註解放在哪裏呢?通常有兩個DiscoveryClient 和 RestTemplate,這兩個的區別是:

1. DiscoveryClient能夠得到服務提供者(生產者)的多個實例集合,能讓你手工決定選擇哪一個實例,這裏負載平衡的策略好比round robin輪詢就不會派上,實際就沒有使用Ribbon:

List<ServiceInstance> instances=discoveryClient.getInstances("PengProducerService");
ServiceInstance serviceInstance=instances.get(0);
複製代碼

2.RestTemplate則是使用Ribbon的負載平衡策略,使用@LoadBalanced註釋resttemplate並使用zuul代理服務器做爲邊緣服務器。那麼對zuul邊緣服務器的任何請求將默認使用Ribbon進行負載平衡,而resttemplate將以循環方式路由請求。這部分代碼以下:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.web.client.RestTemplate;

@Controller
public class ConsumerService {

    @Autowired
    private RestTemplate restTemplate;

    public String callProducer() {
        ResponseEntity<String> result =
                this.restTemplate.getForEntity(
                        "http://PengProducerService/pengproducer",
                        String.class,
                        "");
        if (result.getStatusCode() == HttpStatus.OK) {
            System.out.printf(result.getBody() + " called in callProducer");
            return result.getBody();
        } else {
            System.out.printf(" is it empty");
            return " empty ";
        }
    }
}
複製代碼

RestTemplate是自動注射進這個控制器,在這控制器,咱們調用了服務生產者http://PengProducerService/pengproducer,而後得到其結構。

這個控制器的調用咱們能夠在SpringBoot啓動函數裏調用:

@SpringBootApplication
public class ConsumerApplication {

    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

    public static void main(String[] args) {
        ApplicationContext ctx  =  SpringApplication.run(ConsumerApplication
                .class, args);
        ConsumerService consumerService = ctx.getBean(ConsumerService.class);
        System.out.printf("final result RestTemplate=" + consumerService
                .callProducer() + " \n");
    }
}
複製代碼

注意到@LoadBalanced是標註在RestTemplate上,而RestTemplate是被注入到ConsumerService中的,這樣經過調用RestTemplate對象實際就是得到負載平衡後的服務實例。這個能夠經過咱們的服務提供者裏面輸出hashcode來分辨出來,啓動兩個服務提供者實例,每次運行ConsumerService,應該是依次打印出不一樣的hashcode:

hello world1246528978 called in callProducerfinal result RestTemplate=hello world1246528978

再次運行結果:

hello world1179769159 called in callProducerfinal result RestTemplate=hello world1179769159

hellow world後面的哈希值不一樣,可見是來自不一樣的服務提供者實例。

若是系統基於https進行負載平衡,那麼只須要兩個步驟:

1.application.properties中激活ribbon的https:

ribbon.IsSecure=true

2.代碼中RestTemplate初始化時傳入ClientHttpRequestFactory對象:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    CloseableHttpClient httpClient = HttpClientUtil.getHttpClient();
    HttpComponentsClientHttpRequestFactory clientrequestFactory = new HttpComponentsClientHttpRequestFactory();
    clientrequestFactory.setHttpClient(httpClient);
    RestTemplate restTemplate = new RestTemplate(clientrequestFactory);
    return restTemplate;
}
複製代碼

這部分源碼下載:百度網盤

結合Feign的服務生產者和消費者

   上篇是使用Ribbon實現對多個服務生產者實例使用負載平衡的方式進行消費,在調用服務生產者時,返回的是字符串類型,若是返回是各類本身定義的對象,這些對象傳遞到消費端是經過JSON方式,那麼咱們的消費者須要使用Feign來訪問各類Json對象。

   須要注意的是:Feign = Eureka +Ribbon + RestTemplate,也就是說,使用Feign訪問服務生產者,無需前面章節那麼關於負載平衡的代碼了,前面咱們使用RestTemplate進行負載平衡訪問,代碼仍是挺複雜  

   如今咱們開始Feign的實現:首先咱們在服務的生產者那邊進行修改,讓咱們生產者項目變得接近實戰中項目,增長領域層、服務層和持久層。

   假設新增Article領域模型對象,咱們就須要倉儲保存,這裏咱們使用Spring默認約定,使用JPA訪問h2數據庫,將Article經過JPA保存到h2數據庫中:

要啓用JPA和h2數據庫,首先只要配置pom.xml:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>
複製代碼

Article領域模型對象做爲須要持久的實體對象:配置實體@Entity和@Id主鍵便可:

@Entity
public class Article {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;
    private String title;
    private String body;
    private Date startDate;
複製代碼

而後咱們創建一個空的Article倉儲接口便可:

@Repository
public interface ArticleRep extends JpaRepository<Article,Long> {
}
複製代碼

這樣,關於Article的CRUD實現就已經有了,不須要本身再編寫任何SQL語句。這樣咱們編寫一個Service就能夠提供Article對象的CRUD方法,這裏只編寫插入和查詢批量兩個方法:

@Service
public class ArticleService {

    @Autowired
    ArticleRep articleRep;

    public List<Article> getAllArticles(){
        return articleRep.findAll();
    }

    public void insertArticle(Article article){
        articleRep.save(article);
    }
}
複製代碼

咱們在REST接口中暴露這兩種方法:

  • 1. get /articles是查詢全部對象
  • 2. post /article是新增
@RestController
public class ProducerService {

    @Autowired
    ArticleService articleService;

    @GetMapping("/articles")
    public List<Article> getAllArticles(){
        return articleService.getAllArticles();
    }

    @GetMapping("/article")
    public void publishArticle(@RequestBody Article article){
        articleService.insertArticle(article);
    }
複製代碼

上面服務的生產者提供了兩個REST url,咱們在消費者這邊使用/articles以得到全部文章:

@FeignClient(name="PengProducerService")
public interface ConsumerService {

    @GetMapping("/articles")
    List<Article> getAllArticles();
}
複製代碼

這是咱們消費者的服務,調用生產者 /articles,這是一個接口,無需實現,注意須要標註FeignClient,其中寫入name或value微服務生產者的application.properties配置:

spring.application.name=PengProducerService

固然,這裏會直接耦合PengProducerService這個名稱,咱們之後能夠經過配置服務器更改,這是後話。

而後須要在應用Application代碼加入@EnableFeignClients:

@SpringBootApplication
@EnableFeignClients
public class FeignconsumerApplication {

    public static void main(String[] args)  {
        ApplicationContext context = SpringApplication.run(FeignconsumerApplication
                        .class, args);
        ConsumerService consumerService = context.getBean(ConsumerService
                .class);
        System.out.printf("#############all articles ok" + consumerService
                .getAllArticles());
    }
複製代碼

在FeignconsumerApplication咱們調用了前面接口ConsumerService,而ConsumerService則經過負載平衡調用另一個生產者微服務,若是咱們給那個生產者服務加入一些Articles數據,則這裏就能返回這些數據:

#############all articles ok[com.example.feignconsumer.domain.Article@62b475e2, com.example.feignconsumer.domain.Article@e9474f]

說明調用成功。

在調試過程當中,曾經出現錯誤:

Load balancer does not have available server for client:PengProducerService

常常排查是因爲生產者項目中pom.xml導入的是spring-cloud-starter-netflix-eureka-client,改成pring-cloud-starter-netflix-eureka-server就能夠了,這是SpringBoot 2.0發現的一個問題。

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

本章的代碼下載:百度網盤

總結

   經過這個項目學習,咱們如同蠶絲剝繭層層搞清楚了Spring Cloud的微服務之間同步調用方式,發現基於REST/JSON的調用代碼最少,也是最方便,Feign封裝了Ribbon負載平衡和Eureka服務器訪問以及REST格式處理。