SpringCloud服务间调用

42次阅读

共计 15813 个字符,预计需要花费 40 分钟才能阅读完成。

本篇简介
在上一篇我们介绍了 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

我们看 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

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 的名字来获取该服务的地址和端口。也就是下图中红色标红的名字 (注意名字大小写)。

通过这些我们就可以获取到完整的服务接口地址了,这样就可以直接通过 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

我们看上图注册中心已经显示 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/…

正文完
 0