转载

SpringCloud服务间调用

本篇简介

在上一篇我们介绍了SpringCloud中的注册中心组件Eureka。Eureka的作用是做服务注册与发现的,目的是让不同的服务与服务之间都可以通过注册中心进行间接关联,并且可以通过注册中心有效的管理不同服务与服务的运行状态。但在微服务的架构中,服务与服务只知道对方的服务地址是没有用的,它们的本质还是需要彼此进行通信的,这也是微服务最核心的功能之一。

既然提到了服务与服务之间的通信,那我们自然而然会想到大名鼎鼎的HttpClient。因为在其它的项目架构中我们基本都可以通过它来进行不同服务与服务之间的调用。在SpringCloud中我们依然可以使用HttpClient进行服务与服务调用,只不过如果采用HttpClient调用的话,会有一些弊端。例如: 如果同一个服务有多个负载的话,采用HttpClient调用时,没有办法处理负载均衡的问题。还有另一个问题就是HttpClient只是提供了核心调用的方法并没有对调用进行封装,所以在使用上不太方便,需要自己对HttpClient进行简单的封装。

调用方式

在SpringCloud中为了解决服务与服务调用的问题,于是提供了两种方式来进行调用。也就是RestTemplate和Feign。虽然从名字上看这两种调用的方式不同,但在底层还是和HttpClient一样,采用http的方式进行调用的。只不过是对HttpClient进行的封装。下面我们来详细的介绍一下这两种方式的区别,我们首先看一下RestTemplate的方式。

RestTemplate方式调用

  • RestTemplate

为了方便掩饰我们服务间的调用,所以我们需要创建三个项目。它们分别为eureka(注册中心)、server(服务提供方)、client(服务调用方)。因为上一篇中我们已经介绍了eureka的相关内容。所以在这一篇中我们将不在做过多的介绍了。下面我们看一下server端的配置。因为实际上Server端和Client端是相互的。不一定client端一定要调用server端。server端一样可以调用client端。但对于eureka来说,它们都是client端。因为上一篇中我们已经介绍了eureka是分为server端和client端的,并且已经介绍client端相关内容。所以我们下面我们直接看一下server端的配置内容:

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/
spring:
  application:
    name: jilinwula-springcloud-feign-server
server:
  port: 8082

为了掩饰我们服务间的调用,所以我们需要创建一个Controller,并编写一个简单的接口来供client调用。下面为server的源码。

package com.jilinwula.feign.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/server")
public class Controller {

    @GetMapping("/get")
    public Object get() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("code", "0");
        map.put("msg", "success");
        map.put("data", "吉林乌拉");
        return map;
    }
}

下面我们访问一下这个接口看看,是否能正确返回数据。(备注:注意别忘记了在启动类上添加@EnableEurekaClient注解。)下面我们还是使用.http文件的方式发起接口请求。

GET http://127.0.0.1:8082/server/get

返回结果:

GET http://127.0.0.1:8082/server/get

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 15 Mar 2019 08:20:33 GMT

{
  "msg": "success",
  "code": "0",
  "data": "吉林乌拉"
}

Response code: 200; Time: 65ms; Content length: 42 bytes

我们看已经成功的返回了接口的数据了。下面我们看一下eureka。看看是否成功的检测到了server端的服务。下面为eureka管理界面地址:

http://127.0.0.1:8761

SpringCloud服务间调用

我们看eureka已经成功的检测到了server端注册成功了。下面我们看一下client端的代码,我们还是向server端一样,创建一个Controller,并编写一个接口。下面为具体配置及代码。

application.yml:

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/
spring:
  application:
    name: jilinwula-springcloud-feign-client
server:
  port: 8081

Controller:

package com.jilinwula.feign.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/client")
public class Controller {

    @GetMapping("/get")
    public Object get() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("code", "0");
        map.put("msg", "success");
        map.put("data", "吉林乌拉");
        return map;
    }
}

下面为访问的接口地址:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Fri, 15 Mar 2019 08:56:42 GMT

{
  "msg": "success",
  "code": "0",
  "data": "吉林乌拉"
}

Response code: 200; Time: 273ms; Content length: 42 bytes

现在我们在访问一下Eureka地址看一下Client服务注册的是否成功。

http://127.0.0.1:8761

SpringCloud服务间调用

RestTemplate实例化

我们发现server和client端都已经成功的在注册中心注册成功了。这也就是我们接下来要介绍的服务间调用的前提条件。在开发Spring项目时我们知道如果我们想要使有哪个类或者哪个对象,那就需要在xml中或者用注解的方式实例化对象。所以既然我们打算使用RestTemplate类进行调用,那我们必须要先实例化RestTemplate类。下面我们就看一下怎么在实例化RestTemplate类。因为不论采用的是RestTemplate方式调用还是采用Feign方式,均是在服务的client端进行开发的,在服务的server是无需做任何更改的。所以下面我们看一下client端的改动。下面为项目源码:

package com.jilinwula.feign;

import org.springframework.boot.SpringApplication;
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;

@SpringBootApplication
@EnableEurekaClient
public class JilinwulaSpringcloudFeignClientApplication {

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

    @Bean
    public RestTemplate initRestTemplate() {
        return new RestTemplate();
    }

}

RestTemplate调用方式一

为了掩饰方便我们直接在启动类上添加了一个@Bean注解。然后手动实例化了一个对象,并且要特别注意,在使用RestTemplate时,必须要先实例化,否则会抛出空指针异常。下面我们演示一下怎么使用RestTemplate来调用server端的接口。下面为Controller中的代码的改动。

package com.jilinwula.feign.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/client")
public class Controller {

    @Autowired
    private RestTemplate template;

    @GetMapping("/get")
    public Object get() {
        String result = template.getForObject("http://127.0.0.1:8082/server/get", String.class);
        return result;
    }
}

上面的代码比较简单,我们就不详细的介绍了,主要是RestTemplate中提供了getForObject方法(实际上RestTemplate提供了很多种调用的方法,主要分为Get或者Post),可以指定要调用接口的地址,指定返回的值的类型。然后就会直接返回要调用接口的结果。下面我们测试一下,还是调用client接口,看看能否正确的返回server端的数据。

http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Fri, 15 Mar 2019 09:42:02 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 362ms; Content length: 42 bytes

RestTemplate调用方式二

我们看结果,已经成功的返回的server端的数据了,虽然返回的数据没有格式化,但返回的结果数据确实是server端的数据。这也就是RestTemplate的简单使用。但上述的代码是有弊端的,因为我们直接将调用的server端的接口地址直接写死了,这样当服务接口变更时,是需要更改客户端代码的,这显示是不合理的。那怎么办呢?这时就知道注册中心的好处了。因为注册中心知道所有服务的地址,这样我们通过注册中心就可以知道server端的接口地址,这样就避免了server端服务更改时,要同步更改client代码了。下面我们在优化一下代码,看看怎么通过注册中心来获取server端的地址。

package com.jilinwula.feign.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/client")
public class Controller {

    @Autowired
    private RestTemplate template;

    @Autowired
    private LoadBalancerClient loadBalancerClient;

    @GetMapping("/get")
    public Object get() {
        ServiceInstance serviceInstance = loadBalancerClient.choose("jilinwula-springcloud-feign-server");
        String url = String.format("http://%s:%s/server/get", serviceInstance.getHost(), serviceInstance.getPort());
        String result = template.getForObject(url, String.class);
        return result;
    }
}

在SpringClourd中提供了LoadBalancerClient接口。通过这个接口我们可以通过用户中心的Application的名字来获取该服务的地址和端口。也就是下图中红色标红的名字(注意名字大小写)。

SpringCloud服务间调用

通过这些我们就可以获取到完整的服务接口地址了,这样就可以直接通过RestTemplate进行接口调用了。下面我们在看一下调用的结果。接口地址:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 09:08:32 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 53ms; Content length: 42 bytes

RestTemplate调用方式三

这样我们就解决了第一次服务接口地址写死的问题了。但上述的接口还有一个弊端就是我们每次调用服务时都要先通过Application的名字来获取ServiceInstance对象,然后才可以发起接口调用。实际上在SpringCloud中为我们提供了@LoadBalanced注解,只要将该注解添加到RestTemplate中的获取的地方就可以了。下面为具体修改:

启动类:

package com.jilinwula.feign;

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

@SpringBootApplication
@EnableEurekaClient
public class JilinwulaSpringcloudFeignClientApplication {

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

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

}

我们在RestTemplate实例化的地方添加了@LoadBalanced注解,这样在我们使用RestTemplate时就该注解就会自动将调用接口的地址替换成真正的服务地址。下面我们看一下Controller中的改动:

package com.jilinwula.feign.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

@RestController
@RequestMapping("/client")
public class Controller {

    @Autowired
    private RestTemplate template;

    @GetMapping("/get")
    public Object get() {
        String url = String.format("http://%s/server/get", "jilinwula-springcloud-feign-server");
        String result = template.getForObject(url, String.class);
        return result;
    }
}

代码和第一次的代码基本一样,唯一的区别就是获取服务地址和端口的地方替换成了注册中心中的Application的名字,并且我们的RestTemplate在使用上和第一次没有任何区别,只是在url中不同。下面我们看一下返回的结果。

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 09:55:46 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 635ms; Content length: 42 bytes

默认负载均衡策略

上述内容就是使用RestTemplate来进行服务间调用的方式。并且采用这样的方式可以很方便的解决负载均衡的问题。因为@LoadBalanced注解会自动采用默信的负载策略。下面我们看验证一下SpringCloud默认的负载策略是什么。为了掩饰负载策略,所以我们在新增一个server服务,并且为了掩饰这两个server返回结果的不同,我们故意让接口返回的数据不一致,来方便我们测试。下面为新增的server服务端的配置信息及controller源码。

application.yml:

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/
spring:
  application:
    name: jilinwula-springcloud-feign-server
server:
  port: 8083

Controller:

package com.jilinwula.feign.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/server")
public class Controller {

    @GetMapping("/get")
    public Object get() {
        Map<String, String> map = new HashMap<String, String>();
        map.put("code", "0");
        map.put("msg", "success");
        map.put("data", "jilinwula");
        return map;
    }
}

调用以下接口:

GET http://127.0.0.1:8083/server/get

返回结果:

GET http://127.0.0.1:8083/server/get

HTTP/1.1 200 
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Sat, 16 Mar 2019 10:49:07 GMT

{
  "msg": "success",
  "code": "0",
  "data": "jilinwula"
}

Response code: 200; Time: 100ms; Content length: 47 bytes

现在我们访问一下注册中心看一下现在注册中心的变化。注册中心地址:

http://127.0.0.1:8761

SpringCloud服务间调用

我们看上图注册中心已经显示Application名字为JILINWULA-SPRINGCLOUD-FEIGN-SERVER的有两个服务已经注册成功了。下面我们直接调用client中的接口,看一下client默认会返回哪个server端的信息。client接口地址:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 47
Date: Sat, 16 Mar 2019 10:58:39 GMT

{"msg":"success","code":"0","data":"jilinwula"}

Response code: 200; Time: 24ms; Content length: 47 bytes

看上面返回的结果是server2的接口数据。我们在请求一下接口在看一下返回的结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 11:01:01 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 15ms; Content length: 42 bytes

更改默认负载均衡策略一

我们看这回返回的接口数据就是第一个server端的信息了。并且我们可以频繁的调用client中的接口,并观察发现它们会交替返回的。所以我们基本可以确定SpringCloud默认的负载策略为轮询方式。也就是会依次调用。在SpringCloud中提供了很多种负载策略。比较常见的为:随机、轮询、哈希、权重等。下面我们介绍一下怎么修改默认的负载策略。SpringCloud底层采用的是Ribbon来实现的负载均衡。Ribbon是一个负载均衡器,Ribbon的核心组件为IRule,它也就是所有负载策略的父类。下面为IRule接口的源码:

package com.netflix.loadbalancer;

public interface IRule {
    Server choose(Object var1);

    void setLoadBalancer(ILoadBalancer var1);

    ILoadBalancer getLoadBalancer();
}

该类只提供了3个方法,它们的作用分别是选择一个服务名字、设置ILoadBalancer和返回ILoadBalancer。下面我们看一下IRule接口的常见策略子类。常见的有RandomRule、RoundRobinRule、WeightedResponseTimeRule等。分别对应着随机、轮询、和权重。下面我们看一下怎么更改默认的策略方式。更改默认策略也是在client端中操作的,所以我们看一下client端的代码更改:

package com.jilinwula.feign;

import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.RandomRule;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@SpringBootApplication
@EnableEurekaClient
public class JilinwulaSpringcloudFeignClientApplication {

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

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

    @Bean
    public IRule initIRule() {
        return new RandomRule();
    }

}

我们在启动类上新实例化了一个IRule对象,并且指定该对象实例化的子类为RandomRule,也就是随机的方式。所以当我们Client端启动服务调用服务时,就会采用随机的方式进行调用,因为我们已经将IRule对象默认的实例化方式更改了。下面我们测试一下,继续访问Client端接口:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 11:36:01 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 15ms; Content length: 42 bytes

更改默认负载均衡策略二

在这里我们就不依依演示了,但如果我们多次调用接口就会发现,Client接口返回的结果不在是轮询的方式了,而是变成了随机了,这就说明我们已经成功的将SpringCloud默认的负载策略更改了。下面我们换一种方式来更改默认的负载策略。这种方式和上面的有所不同,而是在配置文件中配置的,下面为具体的配置。(备注:为了不影响测试效果,我们需要将刚刚在启动类中的实例化的IRule注释掉)

eureka:
  client:
    service-url:
      defaultZone: http://127.0.0.1:8761/eureka/
spring:
  application:
    name: jilinwula-springcloud-feign-client
server:
  port: 8081
jilinwula-springcloud-feign-server:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

我们在配置文件中指定了注册中心中的server端的Application名字,然后指定了默认的负载策略类。下面我们测试一下。访问以下接口:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 11:54:42 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 13ms; Content length: 42 bytes

Feign方式调用

我们在实际的开发中,可以使用上述两种方式来更改SpringCloud中默认的负载策略。下面我们看一下SpringCloud中另一种服务间调用方式也就是Feign方式。使用Feign方式和RestTemplate不同,我们需要先添加Feign的依赖,具体依赖如下(备注:该依赖同样是在client端添加的):

pom.xml:

<dependency>
  <groupId>org.springframework.cloud</groupId>
  <artifactId>spring-cloud-starter-openfeign</artifactId>
  <version>2.1.1.RELEASE</version>
</dependency>

其次我们还需要在启动类中添加@EnableFeignClients注解。具体代码如下:

package com.jilinwula.feign;

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
@EnableEurekaClient
@EnableFeignClients
public class JilinwulaSpringcloudFeignClientApplication {

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

}

接下来我们需要在Client端创建一个新的接口并定义Client端需要调用的服务方法。具体代码如下:

package com.jilinwula.feign.server;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "jilinwula-springcloud-feign-server")
public interface ServerApi {

    @GetMapping("/server/get")
    String get();
}

上述接口基本上和server端的Controller一致,唯一的不同就是我们指定了@FeignClient注解,该注解的需要指定一个名字,也就是注册中心中Applicaiton的名字,也就是要调用的服务名字。下面我们看一下Controller中的代码更改:

package com.jilinwula.feign.controller;

import com.jilinwula.feign.server.ServerApi;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/client")
public class Controller {

    @Autowired
    private ServerApi serverApi;

    @GetMapping("/get")
    public Object get() {
        String result = serverApi.get();
        return result;
    }
}

我们在Controller中直接使用了我们自定义的接口,并直接调用我们接口中定义的方法,下面我们调用一下Client接口看看这样的方式是否可以调用成功。接口地址:

GET http://127.0.0.1:8081/client/get

返回结果:

GET http://127.0.0.1:8081/client/get

HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 50
Date: Sat, 16 Mar 2019 12:54:50 GMT

{"msg":"success","code":"0","data":"吉林乌拉"}

Response code: 200; Time: 14ms; Content length: 42 bytes

我们看这样的方式也是可以成功的调用server端的接口的,只不过这样的方式可能会让觉的不太方便,因为这样的方式是需要Client端定义和Server端一样的接口的。

上述内容就是本篇的全部内容,在实际的项目开发中,这两种方式均可实现服务与服务间的调用,并且这两种方式都有彼此的弊端,所以并没有特别推荐的方式。在下一篇中,我们将介绍配置中心相关内容,谢谢。

项目源码

https://github.com/jilinwula/jilinwula-springcloud-feign

原文链接

http://jilinwula.com/article/...

原文  https://segmentfault.com/a/1190000018531262
正文到此结束
Loading...