SpringCloud微服务治理:服务的注册与发现

最近两年,java技术圈最火爆的技术架构莫过于SpringCloud的出现,但其实SpringCloud也并非是一个新的技术架构,SpringCloud是以Springboot为基础,并且集成目前最优秀的一些组件和技术,SpringCloud应该说是java技术架构的集大成者。

SpringCloud的出现是有很大社会需要的,目前各大公司提供的服务数量众多且复杂,服务之间交互往往复杂且难以管理。而SpringCloud的出现即是对这一现状的回应。那么数量众多的服务之间SpringCloud是如何治理的呢,这就得说到Eureka和feign了。那么什么是Eureka和feign呢。下面先看官方给的定义:

Eureka是Netflix开发的服务发现框架,本身是一个基于REST的服务,主要用于定位运行在AWS域中的中间层服务,以达到负载均衡和中间层服务故障转移的目的。SpringCloud将它集成在其子项目spring-cloud-netflix中,以实现SpringCloud的服务发现功能。

Feign是Netflix开发的声明式、模板化的HTTP客户端, Feign可以帮助我们更快捷、优雅地调用HTTP API。Spring Cloud Feign是基于Netflix feign实现并对其进行了增强,整合了Spring Cloud Ribbon和Spring Cloud Hystrix,除了提供这两者的强大功能外,还提供了一种声明式的Web服务客户端定义的方式。

通过上述说明可以看出Eureka提供了服务的注册与发现,feign则提供了基于REST规范的接口调用,且feign默认集成了Ribbon负载均衡和Hystrix断路器,即feign是服务之间沟通的桥梁。

那下面我们就分别说说Eureka和feign以及他们是如何实现服务的注册发现与调用。

一、Eureka

Eureka包含两个组件:Eureka Server和Eureka Client。

Eureka Server提供服务注册服务,一个服务声明了为Eureka Server,那该服务就是服务注册中心。其他声明了为Eureka Client的服务启动后,会在Eureka Server中进行注册,这样EurekaServer中的服务注册表中将会存储所有可用服务节点的信息,服务节点的信息可以在界面中直观的看到。

Eureka Client是一个java客户端,用于简化与Eureka Server的交互,客户端同时也就是一个内置的、使用轮询(round-robin)负载算法的负载均衡器。在应用启动后,将会向Eureka Server发送心跳,默认周期为30秒,如果Eureka Server在多个心跳周期内没有接收到某个节点的心跳,Eureka Server将会从服务注册表中把这个服务节点移除(默认90秒)。

Eureka Server之间通过复制的方式完成数据的同步,Eureka还提供了客户端缓存机制,即使所有的Eureka Server都挂掉,客户端依然可以利用缓存中的信息消费其他服务的API。综上,Eureka通过心跳检查、客户端缓存等机制,确保了系统的高可用性、灵活性和可伸缩性。

Eureka的系统结构就是有一个服务注册中心(服务注册中心也可以集群,即多个中心)。其他的服务既是服务提供者,也是服务消费者。如图所示:

SpringCloud微服务治理:服务的注册与发现

下面我们来一步步实现Eureka Server和Eureka Client。

1、Eureka Server服务注册中心:

使用idea创建一个Eureka Server服务,可以采用创建一个空模板的方式创建项目,也可以利用idea中自带的Eureka Server的模板创建。如果采用模板创建如下:

SpringCloud微服务治理:服务的注册与发现

 SpringCloud微服务治理:服务的注册与发现

 SpringCloud微服务治理:服务的注册与发现

 这样就使用模板简单的创建了一个服务注册中新的项目。但其实无论采用模板还是不用模板都不影响我们项目的使用。下面是服务注册中心的配置:

<properties>
    <!--encode-->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>

    <!-- spring boot -->
    <spring.boot.version>1.5.13.RELEASE</spring.boot.version>
    <spring.cloud.version>Edgware.SR4</spring.cloud.version>
    <openfeign.version>9.5.0</openfeign.version>
    <io.zipkin.version>2.6.1</io.zipkin.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>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- actuator监控引入 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

 

以上为pom.xml文件的依赖。其中重点是

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

 

 这两个依赖可以让我们创建的项目声明并变成一个Eureka Server服务注册中心。dependencyManagement标签的作用在于对依赖的管理。主要是依赖版本的统一管理。然后具体引入的依赖根据dependencies中的内容,此时就无需再对版本进行声明了。

下面在Springboot的主启动类中进行如下声明:

package com.yougu.eureka.server;

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

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args){
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

 此处关键在于引入注解@EnableEurekaServer,使我们的服务声明为一个Eureka Server服务注册中心。当然,我们只在这里声明了还不够,还需要相应的配置(建议默认在application.yml文件配置,即springboot的约定):

server:
  port: 8070

spring:
  application:
    name: eureka

eureka:
  instance:
    hostname: localhost
    prefer-ip-address: true  
  client:
    register-with-eureka: false 
    fetch-registry: false     
    registry-fetch-interval-seconds: 5  
    serviceUrl:
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/  

 1)、prefer-ip-address: true #表示是否注册为ip,true表示注册为ip,false表示注册为域名,读者应根据自己的开发环境进行选择,如在局域网中,建议设置为true

2)、register-with-eureka: false #默认true,单机环境设置为false,表示不向注册中心注册自己。本服务自己是注册中心,所以选择false

3)、fetch-registry: false #默认true,单机环境设置为false,表示不需要向注册中心检索自己。本服务自己是注册中心,所以选择false

4)、 registry-fetch-interval-seconds: 5  #从eureka服务器注册表中获取注册信息的时间间隔(s),默认为30秒

5)、defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/ #设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka 多个地址可使用 , 分隔。因为该服务本身是注册中心。所以应该指向自身。

需要注意的是spring: application: name: eureka这里的eureka是可以自己自定义的,和5中的说明中的eureka不是一个概念。这里仅仅是表示服务注册的名称,就像我们的名字一样,可以叫张三、也可以叫李四。但是其身为注册中心的功能是不变的。

下面我们启动EurekaServerApplication服务。启动成功后。在浏览器中访问http://localhost:8070/。则出现以下内容:

SpringCloud微服务治理:服务的注册与发现

 很显然,此时我们只启动了服务注册中心,所以我们看到这里并没有其他服务实例。下面我们就创建服务提供者和消费者,即Eureka Client。

2、Eureka Client

首先,我们先创建服务提供者。同样的我们可以采用模板,也可以不采用模板。如果采用模板,前几步和创建Eureka Server一样,最后一步在选择时选择Eureka Discovery即可,如下:

SpringCloud微服务治理:服务的注册与发现

 

 作为一个服务提供者/消费者,也就是Eureka Client。需要引入的核心依赖是:

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

 借此可以让我们的服务声明为服务提供者/消费者。为了大家方便,下面贴出完整的Eureka Client服务的配置:

pom.xml

<properties>
    <!--encode-->
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>

    <!-- spring boot -->
    <spring.boot.version>1.5.13.RELEASE</spring.boot.version>
    <spring.cloud.version>Edgware.SR4</spring.cloud.version>
    <openfeign.version>9.5.0</openfeign.version>
    <io.zipkin.version>2.6.1</io.zipkin.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>
    </dependencies>
</dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <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</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- actuator监控引入 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

 启动类:

@EnableFeignClients
@EnableEurekaClient
@EnableAutoSwagger2
@SpringBootApplication(scanBasePackages = "com.yougu.core")
public class ServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServerApplication.class, args);
    }
}

其中

@EnableFeignClients 注解表示声望本服务为一个feign客户端,既可以去调用远程服务,是该服务作为消费者的功能

@EnableEurekaClient 注解表示声明本服务是一个EurekaClient,即服务提供者,被其他服务消费

@EnableAutoSwagger2 注解是我本人自定义的引入Swagger的注解,读者不能直接使用,读者想要引入Swagger应该按照Swagger的集成方式集成,后续我也会给出Swagger的自定义方法。

 application.yml

server:
  port: 8080

spring:
  application:
    name: server-dev
###eureka 配置。该服务作为Eureka服务注册者使用,也作为调用者
eureka:
  instance:
    hostname: localhost
    prefer-ip-address: true #注册服务的ip
  client:
    register-with-eureka: true #使用Eureka注册服务
    fetch-registry: true #在本地缓存注册表
    service-url:
      defaultZone: http://localhost:8070/eureka #Eureka服务位置

需要注意的是,因为这是一个Eureka Client,所以此服务的配置中:

defaultZone: http://localhost:8070/eureka #Eureka服务位置

即是刚才我们启动的服务注册中心。读者应该根据自己的服务注册中进行配置。

下面启动我们创建的Eureka Client服务提供者/消费者。然后在刚才的服务注册中心页面刷新,即可看到如下页面:

SpringCloud微服务治理:服务的注册与发现

很显然,我们可以发现在服务注册中心多了一条服务注册的实例。SERVER-DEV,就是我们刚启动的服务 。上边还有一串红色的警告语,大意是说Eureka可能未被正确地声明,为了安全起见,实例不会过期。 这个是Eureka的一个自我保护机制,开发过程中可能会时常遇到这个,但是并不是说我们的开发有问题,现在我们可以不用管它,直接忽略即可。我们再点击下  localhost:server-dev:8080查看详情,展示了我们在application.yml中配置的info参数信息(此处就不再贴出info的配置信息了,很简单,配置不配置都不影响服务运行以及注册和调用)。:

SpringCloud微服务治理:服务的注册与发现

 好了,我们的服务提供者已经有了,并且已经注册到注册中心。那么我们的服务提供者到底提供了哪些服务呢,下面就可以借助swagger技术进行查看服务提供的接口了。如下:

SpringCloud微服务治理:服务的注册与发现

这些接口服务是我之前已经写好的,此处就不细说swagger以及提供的接口服务了。 

现在我们使用Eureka Client创建了服务提供者,同时也是消费者。但是如果我自己使用我自己的服务那就不需要远程调用了,就无需如此创建了,显然这些服务是要给其他服务调用的,下面我们还需要在创建一个服务消费者,该服务消费者使用feign客户端来优雅快捷的调用我们提供的服务。

2、Eureka Client + feign

在创建一个Eureka Client的服务消费者,其实也是提供者,那么就和我们刚才创建的demo项目是一样的。读者好好回顾下我们刚才的过程就可以了。例如我们再创建一个叫plm的服务。pom.xml和demo的一样。application.yml的配置也和demo的大致一样,只需要注意根据项目的端口号和项目名称进行相应改变即可。而plm服务的启动类如下:

package com.yougu.plm.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

@SpringBootApplication(scanBasePackages = "com.yougu.core")
@EnableEurekaClient
@EnableFeignClients("com.yougu.client.feign.api")
public class PlmServerApplication {
    public static void main(String[] args){
        SpringApplication.run(PlmServerApplication.class, args);
    }
}

细心的读者可以发现此处的@EnableFeignClients("com.yougu.client.feign.api")注解指明了一个package路径。该路径就是我们在plm服务这是作为Feign客户端的接口。pim作为服务消费者,想要调用我们刚才启动的demo服务中的接口服务,那就要和demo中提供的接口保持一致。我们先看看demo提供的接口,以提供的dept接口为例:

@Api(value = "dept Api Client", description = "部门-API(幽谷)", protocols = "application/json")
public interface DeptApi {
    String BASE_PATH = "/server/dept";

    @ApiOperation(value = "通过ID查询部门信息 #幽谷/2019-05-03#", notes = "通过ID查询部门信息", nickname = "dept-get")
    @ApiImplicitParams({@ApiImplicitParam(name = "id", value = "查询对象", paramType = "path", dataType = "int", required = true)})
    @RequestMapping(value = BASE_PATH + "/get/{id}" ,method = RequestMethod.GET)
    DeptDto get(@PathVariable("id") int id);

    @ApiOperation(value = "通过条件分页查询部门信息 #幽谷/2019-05-03#", notes = "通过条件分页查询部门信息", nickname = "dept-pageDept")
    @ApiImplicitParams({@ApiImplicitParam(name = "deptParamDto", value = "查询对象", paramType = "body", dataType = "DeptParamDto", required = true)})
    @RequestMapping(value = BASE_PATH + "/pageDept" ,method = RequestMethod.POST)
    PageData<DeptDto> pageDept(@RequestBody DeptParamDto deptParamDto);

    @ApiOperation(value = "新增部门信息 #幽谷/2019-05-03#", notes = "新增部门信息", nickname = "dept-create")
    @ApiImplicitParams({@ApiImplicitParam(name = "deptInsertDto", value = "新增部门对象", paramType = "body", dataType = "DeptInsertDto", required = true)})
    @RequestMapping(value = BASE_PATH + "/create" ,method = RequestMethod.POST)
    String create(@RequestBody DeptInsertDto deptInsertDto);

    @ApiOperation(value = "修改部门信息 #幽谷/2019-05-03#", notes = "修改部门信息", nickname = "dept-update")
    @ApiImplicitParams({@ApiImplicitParam(name = "deptModifyDto", value = "修改部门对象", paramType = "body", dataType = "DeptModifyDto", required = true)})
    @RequestMapping(value = BASE_PATH + "/update" ,method = RequestMethod.POST)
    String update(@RequestBody DeptModifyDto deptModifyDto);

    @ApiOperation(value = "删除部门信息 #幽谷/2019-05-03#", notes = "删除部门信息", nickname = "dept-delete")
    @ApiImplicitParams({@ApiImplicitParam(name = "idsDto", value = "删除部门ID集", paramType = "body", dataType = "IdsDto", required = true)})
    @RequestMapping(value = BASE_PATH + "/delete" ,method = RequestMethod.POST)
    String delete(@RequestBody IdsDto idsDto);
}

那么我们在plm服务的com.yougu.client.feign.api的package下面创建一个接口作为该服务作为Feign客户端和demo接口的对接。如我们需要调用DeptDto get(@PathVariable("id") int id);接口。即创建如下:

package com.yougu.client.feign.api;

import com.yougu.cient.mis.response.ApiResponse;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@FeignClient(name = "server-dev")
public interface DemoApi {
    String BASE_PATH = "/server/dept";

    @RequestMapping(value = BASE_PATH + "/get/{id}" ,method = RequestMethod.GET)
    ApiResponse get(@PathVariable("id") int id);
}

 在plm中我们是用一个ApiResponse去接收server-dev服务的返回结果,是因为接口负责返回具体的数据类型,但是在demo中对整个服务返回的模型是本人对结果的封装成一个ApiResponse,ApiResponse结构如下。其中的data即是存放接口返回的数据类型的,在此接口中即是DeptDto。

private int code = SUCCESS_CODE;
private String message = HttpStatus.OK.getReasonPhrase();
private Object data = "";
private Object errorData = "";
private Boolean success = false;
private String md5;
private String alg = ALG;

@FeignClient(name = "server-dev")该注解的引用表明plm作为一个Feign客户端,server-dev表明和该服务做关联,即该接口服务是向server-dev调用的。此处已经声明了接口服务了,下面就是我们在plm服务中具体去调用该接口了。因此,我们创建一个Controller如下:

package com.yougu.core.controller;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.yougu.client.api.DeptApi;
import com.yougu.client.feign.api.DemoApi;
import com.yougu.client.output.dto.DeptDto;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;


@Slf4j
@RestController
public class DeptController implements DeptApi {

    @Autowired
    private DemoApi demoApi;

    @Autowired
    private ObjectMapper mapper;

    @Override
    public DeptDto get(@PathVariable("id") int id) {

        return mapper.convertValue(demoApi.get(id).getData(), DeptDto.class);
    }
}

在Controller中我们接收到feign客户端调用远程接口的返回对象后,我们需要对对象处理成我们需要的类型。使用jackson的convertValue转换data成DeptDto类型。

下面启动我们的plm服务,既可以验证。刷新服务注册中心页面。如下:

SpringCloud微服务治理:服务的注册与发现

可以发现此时有两个服务注册实例了,即是我们启动的SERVER-DEVPLM

此时我们在plm中使用get服务去调用远程的server-dev的接口,即顺利的返回了我们需要的数据。过程如下:

plm请求:

SpringCloud微服务治理:服务的注册与发现

 可以看出我们访问的是plm的服务。然后返回了数据。

同时我们在server-dev服务中的后台日志也可以很明显的看出服务被调用:

SpringCloud微服务治理:服务的注册与发现

在 server-dev服务中处理接口调用后返回的模型。

以上就是SpringCloud的微服务治理了。我们实现了服务的注册发现与调用。其中feign默认集成了负载均衡,如果存在分布式集群的环境,它会采用轮询的方式去调用服务提供者的实例。