SpringCloud系列之Eureak服务注册(一)

从今天开始陆续分享一下springcloud的相关知识,一方面在分享的同时,也是为了知识的储备,今天要介绍的springcloud的服务治理组件-eureka,这个组件很基础但却非常重要,因为如果没有它的话基本上也就啥都玩不了(不是说少了eukrea啥都玩不了,因为服务治理的组件有很多,比如consul,zookeeper等)我强调的是在springcloud的组件中,必不可少的组件是服务治理相关组件,至于具体选择哪一种实现,后面会给出建议和对比,大家也可以根据自己的需求来选择,在没有正式开始介绍springcloud之前,先说一个前提,就是对于springcloud而言,个人觉得从开发的角度来说没有多大的难度,因为它完全是基于springboot开发的,所以开发起来非常的简便可快捷,所以在学springcloud之前,最好先熟悉一下springboot相关知识,ok概括一下本章的内容提纲

& springcloud的整体介绍

& eureka的概括

& eureka单节点演示

& eureka多节点演示(HA机制)

& eureka+server provider+ server consumer整合

& eureka的核心源码分析(核心方法以及和springcloud如何集成)


一 springcloud的介绍

Spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中涉及的配置管理、服务发现、断路器、智能路由、微代理、控制总线、全局锁、决策竞选、分布式会话和集群状态管理等操作提供了一种简单的开发方式。

Spring Cloud包含了多个子项目(针对分布式系统中涉及的多个不同开源产品),比如:Spring Cloud Config、Spring Cloud Netflix、Spring Cloud0 CloudFoundry、Spring Cloud AWS、Spring Cloud Security、Spring Cloud Commons、Spring Cloud Zookeeper、Spring Cloud CLI等项目。

二 eureka的概括

由于Spring Cloud为服务治理做了一层抽象接口,所以在Spring Cloud应用中可以支持多种不同的服务治理框架,比如:Netflix Eureka、Consul、Zookeeper。在Spring Cloud服务治理抽象层的作用下,我们可以无缝地切换服务治理实现,并且不影响任何其他的服务注册、服务发现、服务调用等逻辑。

所以,下面我们通过介绍两种服务治理的实现来体会Spring Cloud这一层抽象所带来的好处。

三 eureka单节点的演示

后面会用一个项目来整体演示springcloud的所有核心功能,首先看一下项目结构


首先看一下父pom.xml的配置文件内容,这里有个细节地方要特别注意

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.suning.cloud</groupId>
    <artifactId>spring-cloud</artifactId>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>user_server_provider</module>
        <module>eureka_server</module>
        <module>eureka_server_ha</module>
    </modules>
    <packaging>pom</packaging>

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

    <!--spring-boot-starter-parent  spring-cloud-dependencies 版本集成和依赖 -->
    <!--1.2.x vs A Angel-->
    <!--1.3.x vs B Brixton-->
    <!--1.4.x vs C Camden-->
    <!--1.5.x vs D Dalston-->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>1.5.2.RELEASE</version>
    </parent>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Dalston.SR5</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

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

</project>

上面这个配置文件主要指定了springboot的版本和springcloud的版本,有个地方一定要注意,对于springcloud和springboot的版本是依赖和匹配的关系,如果版本匹配的不正确,会出现各种错误,所以把它们之间的依赖版本关系整理如下所示:

<!--spring-boot-starter-parent  spring-cloud-dependencies 版本集成和依赖 -->
<!--1.2.x vs A Angel-->
<!--1.3.x vs B Brixton-->
<!--1.4.x vs C Camden-->
<!--1.5.x vs D Dalston-->

前面的版本是指定springboot的版本,后面的版本是指定springcloud的版本,最好按照这个来,避免一开始搭建项目就包各种错误.

接下来建立一个叫eureka_server的module,这个模块主要包含一个类,一个配置文件


首先看一下对应的pom.xml文件内容:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud</artifactId>
        <groupId>com.suning.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka_server</artifactId>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>

        <!--集成eurekaserver-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka-server</artifactId>
        </dependency>

        <!--集成spring-security权限验证功能-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
    </dependencies>
</project>

集成eurekaserver和权限安全验证功能(是否要开启该功能,可自由选择,不需要就不要引进),看一下对应的java类内容也很简单

package com.eureka.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;
import org.springframework.context.annotation.Profile;

/**
 * @Author 18011618
 * @Description  启动服务注册与发现组件
 * @Date 10:00 2018/7/9
 * @Modify By
 */
@EnableAutoConfiguration   //启动自动依赖配置检查
@SpringBootApplication      //启动springboot
@EnableEurekaServer         //启动eurekaserver
public class CloudEurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(CloudEurekaServerApplication.class,args);
    }
}
 

这就完成了eureka实例的创建,而主要就是通过一个这个注解来标识开启eurekaserver.....,下面简单的介绍一下其他注解的功能:

:开启依赖配置文件的检查

:通过springboot的方式启动当前应用,到此代码就完成了,还缺一个配置文件(一般都叫application.yml或者application.properties),内容如下

security:  #配置spring-security功能
  basic:
    enabled: true  #开启用户登录权限验证
  user:
    name: admin   #设置登录用户为admin
    password: admin #设置账号密码为admin

server:
  port: 8080  #服务端口号
eureka:
  instance:
    hostname: localhost #访问域名或者IP地址
  client:
    register-with-eureka: false #服务发现与注册组件自身不进行注册
    fetch-registry: false
    service-url:
       defaultZone: http://${security.user.name}:${security.user.password}@${eureka.instance.hostname}:${server.port}/eureka/ #注册服务地址url


#注意yml文件也是支持表达式的,这样配置可以更加的灵活

上面的配置文件内容如下:

>指定开启eureka登录安全的权限验证以及对应的登录用户和密码

>指定服务端的端口号

>指定服务访问的域名或者IP地址

>指定当前服务并不参与注册

>指定服务访问的域名

接下来就可以启动当前的应用,如下图所示


如果此时服务没有报错,成功启动应用,访问http://localhost:8080/ 会出现如下的界面,


因为上面的配置文件包含了所以才会出现登录界面,这时候输入admin和admin,界面如下


代表一个服务注册的eureka就成功启动了,红色标注的是,代表当前还没有任何应用注册到这个服务组件上,所以接下来就创建一个

服务提供方且注册到该组件上,再建立一个叫模块叫user_server_provider,项目结构如下所示:

这里为了方便演示,数据的产生使用了h2+jpa的注解功能,首先看一下pom.xml的内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud</artifactId>
        <groupId>com.suning.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user_server_provider</artifactId>

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

    <dependencies>
        <!--集成JPA-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>

        <!--启动WEB-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--集成H2DTATABASE-->
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!--集成EUREKA-SERVER-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <!--系统自省和监控的集成功能-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>


</project>

下面看看其他的类:

package com.suning.provider.bean;

import java.math.BigDecimal;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
/**
* @Author 18011618
* @Date 9:49 2018/7/9
* @Function 使用JPA注解
*/
@Entity
public class User {

  public User(Long id, String username) {
    super();
    this.id = id;
    this.username = username;
  }

  public User() {
    super();
  }

  @Id
  @GeneratedValue(strategy = GenerationType.AUTO)
  private Long id;

  @Column
  private String username;

  @Column
  private String name;

  @Column
  private Short age;

  @Column
  private BigDecimal balance;

  public Long getId() {
    return this.id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return this.username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Short getAge() {
    return this.age;
  }

  public void setAge(Short age) {
    this.age = age;
  }

  public BigDecimal getBalance() {
    return this.balance;
  }

  public void setBalance(BigDecimal balance) {
    this.balance = balance;
  }
}
 

定义数据访问接口

package com.suning.provider.respoitory;

import com.suning.provider.bean.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

/**
* @Author 18011618
* @Date 9:50 2018/7/9
* @Function 定义数据访问接口
*/
@Repository
public interface UserRepository extends JpaRepository<User, Long> {

}

定义数据访问controller:

package com.suning.provider.controller;

import com.suning.provider.bean.User;
import com.suning.provider.respoitory.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author 18011618
 * @Description 用户提供服务的controller
 * @Date 9:51 2018/7/9
 * @Modify By
 */
@RestController
public class UserController {

    @Autowired
    private UserRepository userRepository; //数据访问层接口

    @GetMapping("/findUser/{id}")
    public User findUserById(@PathVariable long id){
        return this.userRepository.findOne(id);
    }
}

 

OK,核心代码就写完了,讲几个区别,就是这里不再使用@controller,@requestmapping,而是使用了@restcontroller,@getmapping,后者的注解功能更强大,也是的开发更加的简单,比如@restcontroller=@controller+@reponsebody,对如果之前开发的话,对于想返回json格式的数据,一般会加一个@responsebody,使用了就不需要了,(后面会对常用的注解进行一个汇总介绍),接下来还缺一个main方法的入口:

package com.suning.provider;

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

/**
 * @Author 18011618
 * @Description
 * @Date 9:55 2018/7/9
 * @Modify By
 */
@SpringBootApplication      //服务启动是以springboot方式
@EnableAutoConfiguration        //自动检查相关配置的依赖
@EnableEurekaClient         //当前服务注册到eurekaserverpublic class UserServerProviderApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServerProviderApplication.class,args);
    }
}
 

这里出现了一个新注解:,用来标识当前是eureka的客户端会注册到服务端上,代码全部写完了,接下里看对应的配置文件

server:
  port: 7900  #配置服务端口号
spring:
  jpa:          #配置对JPA的支持
    generate-ddl: false
    show-sql: true
    hibernate:
      ddl-auto: none
  datasource:
    platform: h2
    schema: classpath:schema.sql
    data: classpath:data.sql
  application:
    name: service-provider-user #配置注册在eukea上的名称
logging:
  level:
    root: INFO
    org.hibernate: INFO
    org.hibernate.type.descriptor.sql.BasicBinder: TRACE
    org.hibernate.type.descriptor.sql.BasicExtractor: TRACE #设置日志格式
    com.itmuch: DEBUG
eureka:
  client:
    healthcheck:
      enabled: true #启动服务健康状态检查
    serviceUrl:
      defaultZone: http://admin:admin@localhost:8080/eureka #服务注册组件的url--单个节点
      #defaultZone: http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka
      #服务高可用HA,多个地址以,号分开
  instance:
    prefer-ip-address: true
    instance-id: ${spring.application.name}:${spring.cloud.client.ipAddress}:${spring.application.instance_id:${server.port}}
    metadata-map:
      zone: ABC      # eureka可以理解的元数据
      lilizhou: BBC  # 不会影响客户端行为
    lease-renewal-interval-in-seconds: 5  #时间间隔

上面的配置文件内容如下所示

>指定当前服务访问的端口号

>指定当前服务注册的应用名称

>指定当前服务开启健康状态检查

>指定当前服务检查间隔时间

>指定当前服务注册的URL

>指定当前应用支持JPA

看一下对应的数据产生文件和数据表创建文件

insert into user(id,username, name, age, balance) values(1,'user1', '王立', 20, 100.00);
insert into user(id,username, name, age, balance) values(2,'user2', '姚杰', 20, 100.00);
insert into user(id,username, name, age, balance) values(3,'user3', '帅虎', 20, 100.00);
insert into user(id,username, name, age, balance) values(4,'user4', '张军', 20, 100.00);
drop table user if exists;
create table user(
 id bigint generated by default as identity,
 username varchar(40),
 name varchar(20),
 age int(3),
 balance decimal(10,2), 
 primary key(id)
);

OK 这时候,就可以启动,然后重新刷新http://localhost:8080,界面如下


应用里面出现了一个名称,这个名称就是服务提供者的名称,是在配置文件里面指定的,UP代表服务是在线状态,如果是DOWN就代表服务是下线状态,此时就代表一个服务提供者已经成功注册到Eureka上,此时访问http://localhost:7900/findUser/1,会出现如下的结果


数据格式也是我们想要的-json格式,证明@restcontroller的确更强大,OK 至此关于启动eureka和注册服务到eureka上就讲解完了.

四 Eureka的高可用

实际生产环境要注册到的服务成千上万,如果只靠单节点肯定难以支持应用,所以接下来就演示一下,如何配置eureka的高可用.

做点主备工作,在这里因为只有一台电脑,所以需要映射几个伪域名,在C:\Windows\System32\drivers\etc增加下面几行内容:

127.0.0.1 eureka01
127.0.0.1 eureka02

127.0.0.1 eureka03

其实完全可以不用重新建立模块,只不过这里为了更好的区分,所以再重新建立一个叫eureka_server_ha的模块,项目结构如下所示


这里的代码都是直接copy上面的那个模块,所以就不多讲,这里主要讲解一下配置文件

spring:
  application:
    name: EUREKA-HA     #指定应用的名称
---
server:
  port: 8761    #服务端口号
spring:
  profiles: peer1  #激活配置文件标识
eureka:
  instance:
    hostname: eureka01 #指定配置文件为peer1的是hostnameeureka01
  client:
    serviceUrl:
      defaultZone: http://eureka02:8762/eureka/,http://eureka03:8763/eureka/ #把它注册到eureka0203的服务上
---
server:
  port: 8762
spring:
  profiles: peer2
eureka:
  instance:
    hostname: eureka02
  client:
    serviceUrl:
      defaultZone: http://eureka01:8761/eureka/,http://eureka03:8763/eureka/  #把它注册到eureka01eureka03
---
server:
  port: 8763
spring:
  profiles: peer3
eureka:
  instance:
    hostname: eureka03
  client:
    serviceUrl:
      defaultZone: http://eureka01:8761/eureka/,http://eureka02:8762/eureka/ #把它注册到eureka01eureka02


#注意这里因为是要在一台电脑上演示EurekaHA所以需要通过指定spring.profiles.active这种来进行不同实例的启动
#在生产环境中是不需要配置spring.profiles这个参数项


具体内容可以看红色标注的地方,既然是模拟高可用肯定要模拟多个实例,所以这里就创建了三个实例,然后通过spring.profiles来启动每个实例,然后都是把自己注册到其它的两个服务上,我们来验证一下看一下效果,这里通过Edit Configruation来进行spring.profiles的配置..然后依次启动三个实例。按照下面这个截图来操作


这时候会出现这样的对话框


然后按照这个进行重复操作,分别制定为peer2,peer3,(当然在三个节点没有完全成功启动完,会出现异常,不过没有关系,等3个实例都启动好了,就不会出现异常了.)

这个时候访问http://localhost:8761,出现如下界面


说明eureka01和02,03成了副本的关系,同时application下面有3个服务实例,代表他们三个都注册到这个上面,

此时换http://locahost:8762节点,出现如下界面


再访问http://localhost:8763/ 出现如下界面


下面修改一下user_server_provider的配置文件,其它都不需要修改,只需要修改一行:


就是把单一的服务注册节点改成了多个节点(多个节点用逗号进行分割..),这个时候访问http://localhost:7900/findUser/2

结果还是正常的,

可是到这里还是没有办法演示高可用(后面再讲解ribbon的时候会进行详细演示)

& Eurkea+Server Provider+ Server Consumer整合

为了整合,需要再创建一个服务消费者,通过服务消费者来调用服务提供者,建立一个模块叫user_server_consumer,项目结构如下


看一下pom.xml的内容

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud</artifactId>
        <groupId>com.suning.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user_server_consumer</artifactId>

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

    <dependencies>

        <!--启动WEB-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>


        <!--集成EUREKA-SERVER-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-eureka</artifactId>
        </dependency>

        <!--系统自省和监控的集成功能-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
    </dependencies>
</project>

以及对应的java类

package com.user.server.consumer.bean;

import java.math.BigDecimal;
/**
* @Author 18011618
* @Date 16:11 2018/7/9
* @Function
*/
public class User {
  private Long id;

  private String username;

  private String name;

  private Short age;

  private BigDecimal balance;

  public Long getId() {
    return this.id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getUsername() {
    return this.username;
  }

  public void setUsername(String username) {
    this.username = username;
  }

  public String getName() {
    return this.name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public Short getAge() {
    return this.age;
  }

  public void setAge(Short age) {
    this.age = age;
  }

  public BigDecimal getBalance() {
    return this.balance;
  }

  public void setBalance(BigDecimal balance) {
    this.balance = balance;
  }

}
 

和请求的controller

package com.user.server.consumer.controller;

import com.user.server.consumer.bean.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

/**
* @Author 18011618
* @Date 16:12 2018/7/9
* @Function
*/
@RestController
public class UserInfoController {
  //这里注入一个http简化请求的模版
  //且要在main方法的入口加上一个@bean 否则在这里直接使用会报找不到这个resttempleate错误
  @Autowired
  private RestTemplate restTemplate;

  @Value("${user.userServicePath}")
  private String userServicePath;

  @GetMapping("/getUser/{id}")
  public User findById(@PathVariable Long id) {
    //简化http模版请求
    return this.restTemplate.getForObject(this.userServicePath + id, User.class);
  }
}
 

这里有一个新的知识点讲解一下,可能大多数人在开发http请求的时候,都会写一大堆的代码,其实spring里面已经提供了对应的实现,而且非常简单,只需要直接调用API即可,只需要开发者注入一个 然后调用对应的方法,

注意上面的那个

userServicePath 是来源于配置文件中提供的参数,指定服务者提供的URL

,那么就看一下配置文件的内容

spring:
  application:
    name: service-consumer-movie  #服务应用名称
server:
  port: 7901 #服务端口号
user: 
  userServicePath: http://localhost:7900/findUser/ #服务提供者的url
eureka:
  client:
    healthcheck:
      enabled: true #启动服务健康状态的检查
    serviceUrl:
      defaultZone:http://localhost:8761/eureka,http://localhost:8762/eureka,http://localhost:8763/eureka
 #注册服务组件的url
  instance:
    prefer-ip-address: true #使用ip前缀

主要有以下内容

1 指定自身服务应用的名称以及端口号

2 指定服务提供者的URL

3 指定启动当前服务健康状态的检查

4 指定当前服务注册到服务组件上-url

ok 最后再编写一个启动类

package com.user.server.consumer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

/**
 * @Author 18011618
 * @Description
 * @Date 16:13 2018/7/9
 * @Modify By
 */
@EnableAutoConfiguration
@SpringBootApplication
@EnableEurekaClient
public class UserServerConsumerApplication {
    //这里还要使用@Bean注解来注入到启动类中 否则会保错
    //required a bean of type 'org.springframework.web.client.RestTemplate' that
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();//封装http的请求的模版
    }
    public static void main(String[] args) {
        SpringApplication.run(UserServerConsumerApplication.class,args);
    }
}
 

注意这里的启动类和之前的不一样,因为它需要加入一个@Bean的注解来注入一个resttemplate模板,如果这里不注入的话会报红色标注的错误.接下来开始测试效果: 访问http://localhost:7901/getUser/1,效果如下

证明服务消费者成功调用了服务提供者,再看一下服务注册情况,访问http://localhost:8761,界面如下

到这里服务提供者+服务注册中心+服务消费者就打通了,最后再强调一下细节点

1 main方法入口类的编写位置

>> 需要有包的存在,否则会保存,因为springboot会扫描包路径

>>最好把它放在所有子包的外面,比如下面

不要放在子包里面

六 Eureka源码分析

上面把eureka怎么用的讲的比较详细,但是所谓知其然要只其所以然,如果要真正理解eukeka机制,看源码是必须的,所以下面就分析它的核心源码.其实springcloud的很多组件都不是spring的自身,比如eureka,hystrix等都是基于netflix等,spingcloud只是集成和扩展,springcloud通过进行server和client的管理

,从client端开始吧,整个代码并不多

& 服务获取:

通过上面的例子应该知道注册一个服务需要使用注解

EnableEurekaClient,那么接下来看一下这个注解的源码,

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@EnableDiscoveryClient
public @interface EnableEurekaClient {

}

可以发现该注解还引用了一个其它注解,那么就看一下它的源码,

/**
 * Annotation to enable a DiscoveryClient implementation.
 * @author Spencer Gibb
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(EnableDiscoveryClientImportSelector.class)
public @interface EnableDiscoveryClient {

   /**
    * If true, the ServiceRegistry will automatically register the local server.
    */
   boolean autoRegister() default true;
}
 

通过上面注解的注释,可以知道该注解是标致开启一个D的实现,那么就继续搜索一下这个类的源码,这个类不是springcloud,而是com.netflix.discovery包下面的,通过搜索DiscoveryClient,我们可以发现有一个类和一个接口。通过梳理可以得到如下图的关系:

其中,左边的org.springframework.cloud.client.discovery.DiscoveryClient是Spring Cloud的接口,它定义了用来发现服务的常用抽象方法,而org.springframework.cloud.netflix.eureka.EurekaDiscoveryClient是对该接口的实现,从命名来就可以判断,它实现的是对Eureka发现服务的封装。所以EurekaDiscoveryClient依赖了Eureka的com.netflix.discovery.EurekaClient接口,EurekaClient继承了LookupService接口,他们都是Netflix开源包中的内容,它主要定义了针对Eureka的发现服务的抽象方法,而真正实现发现服务的则是Netflix包中的com.netflix.discovery.DiscoveryClient类。

那么,我们就看看来详细看看DiscoveryClient类,类的注释如下:

1 表明该类是和eurekaserver进行协助通信

2 通过该类向eurekaserver进行服务的注册

3 通过该类向eurekaserver进行服务的续约

4 通过该类向eurekaserver进行服务的解约

5 通过该类向eurekaserver进行服务列表的获取,OK 下面就具体分析每一个方法

在具体研究Eureka Client具体负责的任务之前,我们先看看对Eureka Server的URL列表配置在哪里。根据我们配置的属性名:eureka.client.serviceUrl.defaultZone,通过serviceUrl我们找到该属性相关的加载属性,但是在SR5版本中它们都被@Deprecated标注了,并在注视中可以看到@link到了替代类com.netflix.discovery.endpoint.EndpointUtils,我们可以在该类中找到下面这个函数:

public static Map<String, List<String>> getServiceUrlsMapFromConfig(EurekaClientConfig clientConfig, String instanceZone, boolean preferSameZone) {
    Map<String, List<String>> orderedUrls = new LinkedHashMap<>();
    String region = getRegion(clientConfig);
    String[] availZones = clientConfig.getAvailabilityZones(clientConfig.getRegion());
    if (availZones == null || availZones.length == 0) {
        availZones = new String[1];
        availZones[0] = DEFAULT_ZONE;
    }
    logger.debug("The availability zone for the given region {} are {}", region, Arrays.toString(availZones));
    int myZoneOffset = getZoneOffset(instanceZone, preferSameZone, availZones);

    String zone = availZones[myZoneOffset];
    List<String> serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
    if (serviceUrls != null) {
        orderedUrls.put(zone, serviceUrls);
    }
    int currentOffset = myZoneOffset == (availZones.length - 1) ? 0 : (myZoneOffset + 1);
    while (currentOffset != myZoneOffset) {
        zone = availZones[currentOffset];
        serviceUrls = clientConfig.getEurekaServerServiceUrls(zone);
        if (serviceUrls != null) {
            orderedUrls.put(zone, serviceUrls);
        }
        if (currentOffset == (availZones.length - 1)) {
            currentOffset = 0;
        } else {
            currentOffset++;
        }
    }

    if (orderedUrls.size() < 1) {
        throw new IllegalArgumentException("DiscoveryClient: invalid serviceUrl specified!");
    }
    return orderedUrls;
}
 

上面这个方法主要包含了region和zone,serverUrls的获取,下面分别来分析,看第一个子

> getRegion方法,

public static String getRegion(EurekaClientConfig clientConfig) {
    String region = clientConfig.getRegion();
    if (region == null) {
        region = DEFAULT_REGION;
    }
    region = region.trim().toLowerCase();
    return region;
}

通过从配置中获取到一个region返回,如果region为空的话,就返回一个默认的region,所以由此可以知道,对于一个服务只能属于一个唯一的region.

>getAvaliableZones方法:它是基于EurekaClientConfigBean的实现

函数,我们可以知道当我们没有特别为Region配置Zone的时候,将默认采用defaultZone,这也是我们之前配置参数eureka.client.serviceUrl.defaultZone的由来。若要为应用指定Zone,我们可以通过eureka.client.availability-zones属性来进行设置。从该函数的return内容,我们可以Zone是可以有多个的,并且通过逗号分隔来配置。由此,我们可以判断Region与Zone是一对多的关系,源码如下

public String[] getAvailabilityZones(String region) {
    String value = (String)this.availabilityZones.get(region);
    if (value == null) {
        value = "defaultZone";
    }

    return value.split(",");
}

>getEurekaServerServiceUrls方法:也是基于EurekaClientConfigBean实现的,看一下方法对应的源码

public List<String> getEurekaServerServiceUrls(String myZone) {
    String serviceUrls = (String)this.serviceUrl.get(myZone);
    if (serviceUrls == null || serviceUrls.isEmpty()) {
        serviceUrls = (String)this.serviceUrl.get("defaultZone");
    }

    if (!StringUtils.isEmpty(serviceUrls)) {
        String[] serviceUrlsSplit = StringUtils.commaDelimitedListToStringArray(serviceUrls);
        List<String> eurekaServiceUrls = new ArrayList(serviceUrlsSplit.length);
        String[] var5 = serviceUrlsSplit;
        int var6 = serviceUrlsSplit.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            String eurekaServiceUrl = var5[var7];
            if (!this.endsWithSlash(eurekaServiceUrl)) {
                eurekaServiceUrl = eurekaServiceUrl + "/";
            }

            eurekaServiceUrls.add(eurekaServiceUrl);
        }

        return eurekaServiceUrls;
    } else {
        return new ArrayList();
    }
}

总结:当客户端在服务列表中选择实例进行访问时,对于Zone和Region遵循这样的规则:优先访问同自己一个Zone中的实例,其次才访问其他Zone中的实例。通过Region和Zone的两层级别定义,配合实际部署的物理结构,我们就可以有效的设计出区域性故障的容错集群。

& 服务注册

服务注册是基于DiscoveryClient里面的,该方法是执行所有的任务调度,重点看一下这个方法,

if (clientConfig.shouldFetchRegistry()) {
    // registry cache refresh timer
    int registryFetchIntervalSeconds = clientConfig.getRegistryFetchIntervalSeconds();
    int expBackOffBound = clientConfig.getCacheRefreshExecutorExponentialBackOffBound();
    scheduler.schedule(
            new TimedSupervisorTask(
                    "cacheRefresh",
                    scheduler,
                    cacheRefreshExecutor,
                    registryFetchIntervalSeconds,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new CacheRefreshThread()
            ),
            registryFetchIntervalSeconds, TimeUnit.SECONDS);
}

初始化注册缓存刷新的定时器,

if (clientConfig.shouldRegisterWithEureka()) {
    int renewalIntervalInSecs = instanceInfo.getLeaseInfo().getRenewalIntervalInSecs();
    int expBackOffBound = clientConfig.getHeartbeatExecutorExponentialBackOffBound();
    logger.info("Starting heartbeat executor: " + "renew interval is: " + renewalIntervalInSecs);

    // Heartbeat timer
    scheduler.schedule(
            new TimedSupervisorTask(
                    "heartbeat",
                    scheduler,
                    heartbeatExecutor,
                    renewalIntervalInSecs,
                    TimeUnit.SECONDS,
                    expBackOffBound,
                    new HeartbeatThread()
            ),
            renewalIntervalInSecs, TimeUnit.SECONDS);

    // InstanceInfo replicator
    instanceInfoReplicator = new InstanceInfoReplicator(
            this,
            instanceInfo,
            clientConfig.getInstanceInfoReplicationIntervalSeconds(),
            2); // burstSize

    statusChangeListener = new ApplicationInfoManager.StatusChangeListener() {
        @Override
        public String getId() {
            return "statusChangeListener";
        }

        @Override
        public void notify(StatusChangeEvent statusChangeEvent) {
            if (InstanceStatus.DOWN == statusChangeEvent.getStatus() ||
                    InstanceStatus.DOWN == statusChangeEvent.getPreviousStatus()) {
                // log at warn level if DOWN was involved
                logger.warn("Saw local status change event {}", statusChangeEvent);
            } else {
                logger.info("Saw local status change event {}", statusChangeEvent);
            }
            instanceInfoReplicator.onDemandUpdate();
        }
    };

    if (clientConfig.shouldOnDemandUpdateStatusChange()) {
        applicationInfoManager.registerStatusChangeListener(statusChangeListener);
    }

    instanceInfoReplicator.start(clientConfig.getInitialInstanceInfoReplicationIntervalSeconds());
} else {
    logger.info("Not registering with Eureka server per configuration");
}
心跳监测定时器,最重要的是这一行代码,
instanceInfoReplicator = new InstanceInfoReplicator(
        this,
        instanceInfo,
        clientConfig.getInstanceInfoReplicationIntervalSeconds(),
        2); // burstSize

这个实例里面有一个很重要的方法:start方法,最终会调用run方法(它本身就是一个线程而已),好看一下run方法源码


红色标注的才是我们需要的,在这里会调用register方法,好继续回来看discoveryclient的对应方法:


这里可以看出原来client和server的通信是基于http请求,最终的register的请求是基于


& 服务续约:renew方法

该方法还是在DiscoveryClient中



那么客户端的请求,服务端是如何处理的?服务端的代码也是比较少的:


这里按类来讲解:

:接受客户端的请求,然后调用其它组件进行处理,返回结果给客户端

:这个类很关键,它的功能就是把eureka和spring进行了很好的整合,从这里也可以发现一个本质的问题,eureka是一个纯天然的servlet请求,springboot通过内嵌的embend tomcat服务容器来启动它

:执行服务的注册,服务的续约,服务的解约,但是它设计是基于模板方法的,所以真正的实现是在父类中

:实现了服务的注册,续约,解约-具体可以看源码。

上面这些代码都是有注册中心来调用的:位于com.netflix.eureka.sources.ApplicationResource


通过addInstance方法来实现注册的

@POST
@Consumes({"application/json", "application/xml"})
public Response addInstance(InstanceInfo info,
                            @HeaderParam(PeerEurekaNode.HEADER_REPLICATION) String isReplication) {
    logger.debug("Registering instance {} (replication={})", info.getId(), isReplication);
    // validate that the instanceinfo contains all the necessary required fields
    if (isBlank(info.getId())) {
        return Response.status(400).entity("Missing instanceId").build();
    } else if (isBlank(info.getHostName())) {
        return Response.status(400).entity("Missing hostname").build();
    } else if (isBlank(info.getAppName())) {
        return Response.status(400).entity("Missing appName").build();
    } else if (!appName.equals(info.getAppName())) {
        return Response.status(400).entity("Mismatched appName, expecting " + appName + " but was " + info.getAppName()).build();
    } else if (info.getDataCenterInfo() == null) {
        return Response.status(400).entity("Missing dataCenterInfo").build();
    } else if (info.getDataCenterInfo().getName() == null) {
        return Response.status(400).entity("Missing dataCenterInfo Name").build();
    }

    // handle cases where clients may be registering with bad DataCenterInfo with missing data
    DataCenterInfo dataCenterInfo = info.getDataCenterInfo();
    if (dataCenterInfo instanceof UniqueIdentifier) {
        String dataCenterInfoId = ((UniqueIdentifier) dataCenterInfo).getId();
        if (isBlank(dataCenterInfoId)) {
            boolean experimental = "true".equalsIgnoreCase(serverConfig.getExperimental("registration.validation.dataCenterInfoId"));
            if (experimental) {
                String entity = "DataCenterInfo of type " + dataCenterInfo.getClass() + " must contain a valid id";
                return Response.status(400).entity(entity).build();
            } else if (dataCenterInfo instanceof AmazonInfo) {
                AmazonInfo amazonInfo = (AmazonInfo) dataCenterInfo;
                String effectiveId = amazonInfo.get(AmazonInfo.MetaDataKey.instanceId);
                if (effectiveId == null) {
                    amazonInfo.getMetadata().put(AmazonInfo.MetaDataKey.instanceId.getName(), info.getId());
                }
            } else {
                logger.warn("Registering DataCenterInfo of type {} without an appropriate id", dataCenterInfo.getClass());
            }
        }
    }

    registry.register(info, "true".equals(isReplication));
    return Response.status(204).build();  // 204 to be backwards compatible
}

红色标注的很关键,因为它开始调用真正的注册处理实例来实现注册,到此为止就把eukeka整体知识都讲完了

总结:目前eureka还只是内嵌的服务,可扩展性或者性能来说还是有待提升,如果它未来能够作为独立的服务可以部署的话,那样

对我们在生产环境中使用的风险性会更低,目前其实推荐使用Consul组件,这个后面也会介绍到的.

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值