玩转Spring Cloud之服务注册发现(eureka)及负载均衡消费(ribbon、feign)

  如果说用Spring Boot+Spring MVC是开发单体应用(或单体服务)的利器,那么Spring Boot+Spring MVC+Spring Cloud将是开发分布式应用(快速构建微服务)的又一法宝,相信大家如果看到我近期总结的《JAVA WEB快速入门》系列文章,对Spring Boot+Spring MVC应该是比较熟悉了吧,从本文开始,一起来熟悉Spring Cloud、玩转Spring Cloud,至于什么是Spring Cloud?我这里就不再介绍了,网上资源太多了,比如:大话Spring CloudSpringCloud是什么?,当然介绍Spring Cloud系列文章也比较多(比如:https://blog.csdn.net/forezp/article/details/70148833),大家也可以参考,我这里只是结合当前最新的Spring Boot、Spring MVC、Spring Cloud来重新演练一遍,把重要的知识点、遇到的一些坑分享出来,一来是为自己做记录(所谓“好记性不如烂笔头”),二来可以避免大家学习时走弯路,又因为介绍Spring Cloud文章实在太多了,故玩转Spring Cloud系列文章更多的是以把实现的DEMO代码一步步贴出来,一些组件名词我就不再详细解释了,然后对于涉及的重要知识点及踩坑点进行说明,以便大家可以:知其然还能知其所以然。(注:所有示例代码均采用IDEA IDE编写)

一、实现eureka server(注册中心)

  1.1.通过IDEA来创建一个空的spring boot项目(类型是:maven-archtype-quickstart,这样最精简,当然如果你使用webapp项目也是可以,只是认为没有必要)。

    创建步骤有2种,第一种是使用maven创建: maven->maven-archtype-quickstart,然后手动添加相关的spring boot依赖;第二种是使用spring initializer->填写项目参数->选择相关依赖(可直接选择spring cloud相关依赖,如:eureka,这样就一步到位,这里全部先不选),最终的初始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>cn.zuowenjun.cloud</groupId>
  <artifactId>eurekaserver</artifactId>
  <version>1.0-SNAPSHOT</version>

  <name>eurekaserver</name>
  <!-- FIXME change it to the project's website -->
  <url>http://www.zuowenjun.cn</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
  </properties>

  <parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.3.RELEASE</version>
  </parent>



  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>


  </dependencies>

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

</project>

如上所示(如果不是请改成这样,如果只是多点依赖没关系,当然我认为此时只需要这么多的依赖即可,多了也无用),我们只是有spring boot的POM依赖,并没有spring cloud的相关依赖。

  1.2添加spring cloud相关依赖,如下所示:(添加了dependencyManagement节点,并配置spring-cloud-dependencies pom import依赖,目的是:便于依赖继承,与parent节点功能类似,添加具体依赖时,若包含在parent中或pom import依赖中则无需版本号,能够保证组件的一致性,详见:https://blog.csdn.net/mn960mn/article/details/50894022,相反如果没有配置spring-cloud-dependencies pom import依赖,则添加具体依赖时需要指定version版本号,而且需要注意各依赖组件间的兼容性问题,如下面我把version注释掉)

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

  <dependencies>
    ... ...其它原有依赖
    
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter</artifactId>
      <!--<version>2.1.0.RELEASE</version>-->
    </dependency>
    <dependency>
      <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
      <!--<version>2.1.0.RELEASE</version>-->
    </dependency>

  </dependencies>

  1.3.在resouces目录下(若没有请创建,注意设为souces root目录,方法:右键文件夹->Mark directory as->souces root)创建application.yml(或application.properties,本文示例全部使用yml),添加如下配置:

server:
  port: 8800

spring:
  applcation:
    name: demo-eurekaserver

# config detail:https://www.jianshu.com/p/98f4e5f6bca7  or https://blog.csdn.net/wo18237095579/article/details/83276352
eureka:
  instance:
    hostname: eurekaserver1 #实例主机名,集群时需要且唯一
  server:
    enable-self-preservation: true #自我保护,正式环境不要这么做
    eviction-interval-timer-in-ms: 5000 #定期清理失效节点,默认60s
    peer-eureka-nodes-update-interval-ms: 6000 #同步更新节点频率,默认10min
    renewal-percent-threshold: 0.49 #默认0.85
    response-cache-auto-expiration-in-seconds: 30

  client:
    registerWithEureka: false
    fetchRegistry: false
    serviceUrl:
      defaultZone: http://localhost:${server.port}/eureka/

1.4.在spring boot 启动类中添加@EnableEurekaServer即可,如下代码:

package cn.zuowenjun.cloud;

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

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

整个项目结构如下图示,启动后浏览地址:http://localhost:8800/,会出现spring eureka的主页,就表明eureka server成功了。

二、实现service provider(含eureka  client)--服务提供者

【即:具体微服务项目,注册服务信息,暴露API】,当然也有可能同时是service consumer【服务消费者】,需要远程调用其它服务

  2.1.参照1.1方式创建一个空的spring boot项目,然后添加spring cloud 相关依赖(这里主要是:eureka-client【实现服务自动发现与注册】、web【即:springMVC,实现服务API】),POM XML添加配置如下:

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

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </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-web</artifactId>
    </dependency>

    <!--当.yml配置不生效时,应添加snakeyaml依赖,但一般spring-boot-starter中默认有此依赖,非spring boot项目需要添加-->
    <dependency>
      <groupId>org.yaml</groupId>
      <artifactId>snakeyaml</artifactId>
      <version>1.23</version>
    </dependency>

  </dependencies>

  2.2.在application.yml文件中添加如下配置(若没有请参见1.3法创建):注意spring.application.name,这个是服务实例名,注册及服务消费时均需使用该名称

server:
  port: 8801

spring:
  application:
    name: helloservice
    ip: localhost #自定义配置,在demo代码中有用到

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8800/eureka/

  3.3.编写controller 服务相关代码,在spring boot启动类上添加@EnableDiscoveryClient注解,具体完整实现代码如下:(除了@EnableDiscoveryClient注解,基余代码与普通的spring MVC项目代码均相同)

//controller:
package cn.zuowenjun.cloud.controller;

import cn.zuowenjun.cloud.model.Result;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.client.discovery.DiscoveryClient;
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.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @Value("${spring.application.name}")
    private String serviceName;

    @Value("${spring.application.ip}")
    private String address;

    @Value("${server.port}")
    private String port;

    @Autowired
    DiscoveryClient discoveryClient;


    @GetMapping(value = "/")
    public String index(){
        return "demo service";
    }

    @RequestMapping("/hello")
    public  Object hello(){
        return discoveryClient.getServices();
    }


    @RequestMapping("/info")
    public Result info(){
        Result result = new Result();
        result.setServiceName(serviceName);
        result.setHost(String.format("%s:%s", address, port));
        result.setMessage("hello");
        return result;
    }

    @RequestMapping(value = "/multiply/{a}/{b}")
    public Result multiply(@PathVariable("a") int a,@PathVariable("b") int b){
        Result result = new Result();
        result.setServiceName(serviceName);
        result.setHost(String.format("%s:%s", address, port));
        result.setMessage("ok");
        result.setContent(a * b);
        return result;
    }
}

//model:
package cn.zuowenjun.cloud.model;

public class Result {

    private int code;

    private String message;

    private Object content;

    private String serviceName;

    private String host;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public Object getContent() {
        return content;
    }

    public void setContent(Object content) {
        this.content = content;
    }

    public String getServiceName() {
        return serviceName;
    }

    public void setServiceName(String serviceName) {
        this.serviceName = serviceName;
    }

    public String getHost() {
        return host;
    }

    public void setHost(String host) {
        this.host = host;
    }
}

//App spring boot启动类:

package cn.zuowenjun.cloud;

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

@EnableDiscoveryClient
@SpringBootApplication
public class App 
{
    public static void main( String[] args )
    {
        SpringApplication.run(App.class, args);
    }
}
View Code

  完成上述步骤后即实现了服务提供者项目,完整项目结构如下图示,启动运行http://localhost:8801/multiply/324/561(只需关注这个服务方法,后面服务消费会调用这个方法) ,可以看到正常响应出JSON结果,如:"code":0,"message":"ok","content":181764,"serviceName":"helloservice","host":"localhost:8801"}

为了后面服务消费者能体验出负载均衡的效果,可以把该项目再以另一个端口(server.port=8802)重新启动运行一个实例(IDEA启动多个实例的方法请参见:https://blog.csdn.net/forezp/article/details/76408139,最后不一定要改yml中的port配置,也可以直接在Edit Configuration--> program argements中指定:--server.port=8802即可,原理与直接通过命令:java -jar xxx --server.port=8802类似),这样就会有两个服务提供者了,如果查看eureka server主页(http://localhost:8800/)会在Instances currently registered with Eureka列表中展示出2个服务实例信息,如下图示:

 

三、实现service consumer(含eureka  client)--服务消费者

【即:需要调用微服务API的项目,相对eureka,service provider来讲,就是客户端,消费方】,当然也有可能是service provider【服务提供者】,暴露服务API给其它微服务项目

  3.0.参照1.1方式创建一个空的spring boot项目,然后添加spring cloud 相关依赖(这里仅先是:eureka-client【实现服务自动发现与注册】、web【即:springMVC,实现服务API】),POM XML添加配置如下:

    <properties>
        <java.version>1.8</java.version>
        <spring-cloud.version>Greenwich.RELEASE</spring-cloud.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</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-test</artifactId>
            <scope>test</scope>
        </dependency>

    </dependencies>

    <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>
View Code

  3.1方式一:使用restTemplate+ribbon实现服务消费(负载均衡调用远程服务)

    3.1.1.在POM XML中添加spring-cloud-starter-netflix-ribbon依赖,如下:

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

    3.1.2.编写controller相关代码(含远程服务调用类HelloService),修改spring boot 启动类,具体完整实现代码如下:

//spring boot启动类:
package cn.zuowenjun.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
class EurekaclientconsumerApplication {

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

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

}

//controller:
package cn.zuowenjun.cloud.controller;

import cn.zuowenjun.cloud.service.HelloRemoteService;
import cn.zuowenjun.cloud.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private  HelloService helloService;


    @RequestMapping("/x")
    public Object multiplyForRestTemplate(@RequestParam int a, @RequestParam int b) {
       return   helloService.multiply(a,b);
    }

}


//HelloService(远程服务代理类) :
package cn.zuowenjun.cloud.service;


import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class HelloService {

    @Autowired
    private RestTemplate restTemplate;

    @Value("${spring.application.helloServiceProvider}")
    private String  helloServiceName;

    public Object multiply(int a,int b){
        String url="http://"+ helloServiceName +"/multiply/" + a +"/" + b;
        return restTemplate.getForObject(url,String.class);
    }
}
View Code

 如上代码中最核心的是:HelloService类,通过这个类远程调用【消费】注册在eureka server上对应的服务API,而这个类中最核心的对象是:RestTemplate,而这个又是通过在spring boot启动类(EurekaclientconsumerApplication)中通过代码注入到Spring IOC容器中的(当然也可以自定义一个config类然后统一写BEAN注入的方法),重点请看这个restTemplate Bean注册方法上面的注解:@LoadBalanced,这个就是实现负载均衡(默认是采用轮询的负载均衡算法,还有其它的负载均衡Rule),就这么简单吗?是的,用起来简单,但内部实现还是非常复杂的,Ribbon的运行原理详见:深入理解Ribbon之源码解析,核心思路是:RestTemplate内部维护了一个被@LoadBalance注解的RestTemplate列表,而这些RestTemplate列表又被添加了LoadBalancerInterceptor拦截器,而LoadBalancerInterceptor内部又使用了LoadBalancerClient,而LoadBalancerClient(实现类:RibbonLoadBalancerClient)具体选择服务实例的逻辑又由ILoadBalancer来处理,ILoadBalancer通过配置IRule、IPing等信息,向EurekaClient获取注册列表的信息,并定时向EurekaClient发送“ping”心跳,进而检查是否更新了服务列表,最后得到注册服务实例列表后,ILoadBalancer根据IRule的策略进行负载均衡。

  3.1.3.在application.yml文件中添加如下配置(若没有请参见1.3法创建):

server:
  port: 8666

spring:
  application:
    name: ribbonclient
    helloServiceProvider: helloservice #自定义配置,指定访问远程服务名称,当然也可以写死在代码中

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8800/eureka/ #指向eureka server

完成上述步骤即实现了一个基于Ribbon的负载均衡服务消费者(客户端)项目。

  3.2方式二:使用feign实现服务消费(负载均衡调用远程服务调用)

  我们仍然基于3.1节原有项目基础上实现基于feign的负载均衡服务调用,注意feign的底层仍然使用了Ribbon。当然也可以单独创一个新的spring boot项目(参照第一节介绍)然后再按下文步骤操作即可。

  3.2.1.在POM XML中添加spring-cloud-starter-openfeign依赖,配置如下:

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

  3.2.2.在spring boot启动类(EurekaclientconsumerApplication)上添加:@EnableFeignClients 注解,然后在cn.zuowenjun.cloud.service包中添加自定义HelloRemoteService,这个就是远程服务调用接口类(或称:客户端代理类【接口】),这个就是与3.1中定义的HelloService作用完全类似,只是实现方式不同而矣,最后在controller中添加一个新的API ACTION方法,以便可以调用HelloRemoteService中的服务方法,完整实现代码如下:

//spring boot启动类
package cn.zuowenjun.cloud;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.client.loadbalancer.LoadBalancerInterceptor;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@EnableDiscoveryClient
@SpringBootApplication
@EnableFeignClients(basePackages = "cn.zuowenjun.cloud.service") // 如果启动类不在根目录需要指定basePackages,否则不需要
class EurekaclientconsumerApplication {

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

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

}

//HelloRemoteService:

package cn.zuowenjun.cloud.service;


import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

/*
*  bug-refer https://blog.csdn.net/zlh313_01/article/details/80309144
*  bug-refer https://blog.csdn.net/alinyua/article/details/80070890
 */
@FeignClient(name= "helloservice")
public interface HelloRemoteService {

    @RequestMapping("/multiply/{a}/{b}")
    Object  multiply(@PathVariable("a") int a, @PathVariable("b") int b);

}

//controller:

package cn.zuowenjun.cloud.controller;

import cn.zuowenjun.cloud.service.HelloRemoteService;
import cn.zuowenjun.cloud.service.HelloService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @Autowired
    private  HelloService helloService;

    @Autowired
    private HelloRemoteService helloRemoteService;

    @RequestMapping("/x")
    public Object multiplyForRestTemplate(@RequestParam int a, @RequestParam int b) {
       return   helloService.multiply(a,b);
    }

    @RequestMapping("/multiply/{a}/{b}")
    public Object multiplyForFeignClient(@PathVariable int a, @PathVariable int b) {
        return helloRemoteService.multiply(a,b);
    }
}
View Code

 如上代码HelloRemoteService是重点,需要注意:

a.必需是interface,因为@FeignClient注解只能用于interface中,而且很显然HelloRemoteService 是远程调用,本地不应有实现的,如果知道原理就更明白这个接口只是为了生成可供restTemplate调用的URL方法而矣;

b.@FeignClient注解的name(别名属性)或value必填,这个就是需要远程调用服务的应用名称【即:表明消费哪个服务】

c.接口中定义的方法应与远程服务的controller中的方法保持一致(方法签名,注解),同时注意方法上的一些映射请求的注解,如:@RequestMapping,这些与我们在spring MVC用法相同,但含义却不相同,spring MVC是指处理请求路径,而这里是调用请求路径,这个路径必需与服务提供者API 的对应的ACITON方法上的保持相同,否则将无法成功发送请求。常见问题及解决办法可参见:https://blog.csdn.net/zlh313_01/article/details/80309144

3.2.3.application.yml配置与3.1.3配置相同,即保持不变即可,最后启动项目即可(现在这个项目同时包含了Ribbon与Feign的负载均衡远程调用服务的方式),通过多次访问:http://localhost:8666/x?a=数字&b=数字 (基于Ribbon实现)、http://localhost:8666/multiply/数字/数字(基于Feign实现)可以看到远程调用服务成功(即:消费服务成功)。

FeignClient的运行原理详见:深入理解Feign之源码解析,核心思路是:spring boot项目启动时检查@EnableFeignClients,若有则扫描被@FeignClient注解接口并注入到spring IOC容器中,然后在请求被@ FeignCleint标注的接口方法时,会通过JDK动态代理来生成具体的RequesTemplate,RequesTemplate又会生成Request,Request交给Client去处理,最后Client被封装到LoadBalanceClient类,这个类Ribbon中的LoadBalancerClient相同,后面的负载均衡的处理请求相同。

项目结构及远程调用效果如下图所示:

  

四、下面分享相关可参考的博文资料链接:

Spring Cloud之Eureka服务注册与发现(概念原理篇)

微服务架构:Eureka参数配置项详解(转载)

Spring Cloud Netflix - Eureka Server源码阅读

Eureka 参数调优

 

提示:本文相关示例项目代码已上传GITHUB,地址如下:

https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaserver

https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaclient

https://github.com/zuowj/learning-demos/tree/master/java/demo-eurekaclientconsumer

说明:文中若有不足之处欢迎指出,码字不易,请多支持,谢谢!

 

posted @ 2019-02-21 17:54  梦在旅途  阅读(1907)  评论(0编辑  收藏  举报