5. openfeign 远程调用
1. 背景知识
在单体应用,不同功能模块之间的调用过程多是通过调用方法来实现的,而拆分成微服务架构后,由于物理部署方式的改变(主要表现形式是:单个部署包变成多个部署包),不同模块之间的调用方式也发生了本质上的改变。
微服务架构中,服务之间的调用大多是通过 RPC 调用完成的,RPC 就是远程服务调用。通俗点讲就是被调用者通过暴露接口,调用者调用被调用者暴露出来的接口来实现调用过程。
简单说一下 RPC 和 SOA 的区别,SOA 的主要特点是构建一种消息组件,让所有的服务统一通过这个消息组件实现相互调用,它主要的特点是有一个统一的消息组件,另外一个特点是所有的服务提供的接口都是经过统一格式定义过的,Dubbo 和 WebService 都属于 SOA 的范畴。而 RPC 则是取消了统一的消息组件,服务与服务之间的调用是通过直接的网络调用,调用过程优雅,像本地调用一样简单便捷,SpringCloud 技术栈属于这个范畴。
在微服务的发展的早期,远程服务调用多是通过 JDK 原生的 URLConnection 来实现的,后期出现了 HttpClient、OkHttp 的组件。目前微服务架构中,多是使用 RestTemplat 和 OpenFeign 来完成远程服务调用的。
2. 技术选型
OpenFeign 是 SpringCloud 官方提供的远程服务调用的组件,我们“别无选择”,就它了。需要注意的是,SpringCloud 还提供了 RestTemplate 组件,也可以完成远程服务调用。关于 RestTemplate 方式调用和 OpenFeign 方式的简单调用,也可以参考笔者的系列文章的《服务治理之 Nacos》章节进行查阅。
3. 实战
要想使用 OpenFeign,我们就要先明白它的大概原理: 服务生产者与服务消费者都注册到服务治理中心上,服务调用者在调用过程中,先从服务治理中心获取服务生产者的地址,然后再组装实际的调用地址,之后完成服务的调用。因此我们还需要一个服务治理中心,这里我们还是使用 Nacos。
下面带大家一起搭建一下所需要的测试框架。
3.1. 基本使用
3.1.1. 服务生产者(nacos-provider)
- 引入服务发现的依赖
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
- 注册中心相关配置
spring:
application:
# 注册到nacos上的服务名称,也是服务发现的名称,必写
name: nacos-provider
cloud:
nacos:
discovery:
# nacos的注册地址
server-addr: 192.168.1.150:8848
# gateway
namespace: deeaeca6-bed9-4fb1-b5b7-9c79278561ca
- 开启服务注册与发现
@SpringBootApplication
@EnableDiscoveryClient // 开启服务的注册与发现
public class NacosProviderApplication {
public static void main(String[] args) {
SpringApplication.run(NacosProviderApplication.class, args);
}
}
- 对外暴露接口进行测试启动
@RestController
@RequestMapping("provider")
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello, provider!!!";
}
}
启动后,查看 nacos 控制台,服务已经注册到 nacos 上面,postman 请求 hello 接口,请求成功。
3.1.2. 服务消费者(nacos-consumer)
服务消费者框架的搭建过程与服务生产者的搭建过程一样,不再赘述。不同的是服务消费者要使用 OpenFeign,因此还需要下面几个步骤。
- 引入 OpenFeign 的依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
- 创建 OpenFeign 的 Service
@FeignClient(value = "nacos-provider")
public interface ProviderService {
@RequestMapping("/provider/hello")
String hello();
}
- 开启服务注册与发现并添加客户端的扫描包路径
@EnableFeignClients("com.tianqingxiaozhu.nacos.consumer.service") // 扫描包路径
@EnableDiscoveryClient // 开启服务的注册与发现
@SpringBootApplication
public class NacosConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(NacosConsumerApplication.class, args);
}
}
- 消费接口
@RestController
@RequestMapping("consumer")
public class HelloController {
/**
* 注入OpenFeign的接口
*/
@Autowired
private ProviderService providerService;
/**
* 消费接口
*/
@GetMapping("/hello2")
public String hello2(){
return providerService.hello();
}
}
- 启动测试
查看 nacos 控制台,发现服务已经注册到 nacos 上,请求接口也成功。
OpenFeign 的基本用法是:
- 服务生产者对外暴露接口;
- 服务消费者创建 service 链接接口;
- 控制层注入 service 后进行消费;
3.2. OpenFeign 高级特性
微信扫码关注“天晴小猪”(ID: it-come-true),回复“springcloud”,获取本章节实战源码。
在实际的项目开发过程中,我们大多情况下使用的是 POST 请求,但是会有多种不同的情况,下面我们分别介绍一下 POST 协议下不同的请求方式。但是服务生产者会提供多种接口,例如:包含多个参数、路径中携带参数、需要传递对象、文件上传、下载等,下面我们实践一下。
3.2.1. 开启日志
在开发过程中,有时我们需要开启远程调用的日志信息,以便我们进行排错,开启只需要在消费者中配置:
logging:
level:
com.tianqingxiaozhu.nacos.consumer.service: debug
com.tianqingxiaozhu.nacos.consumer.service
是消费者接口所在的包路径。
3.2.2. 多参数
- 服务生产者对外提供 test01 接口,此接口有两个参数:
name
和age
,
@PostMapping("/test01")
public String test01(String name, String age) {
return "name: " + name + " age: " + age;
}
- 服务消费者使用 @RequestParam 标注服务生产者接受的两个参数的参数名,
@PostMapping("/provider/test01")
String test01(@RequestParam("name")String name, @RequestParam("age")String age);
- 测试结果,
3.2.3. URL 中携带参数
- 服务生产者对外提供 test02 接口,此接口需要在路径中携带参数
name
和age
,
/**
* URL中携带参数
* @param name
* @param age
* @return
*/
@GetMapping("/test02/{name}/{age}")
public String test02(@PathVariable("name") String name, @PathVariable("age")Integer age) {
return "name: " + name + ", age: " + age.toString();
}
- 服务消费者使用 @PathVariable 注解进行标注生产者的参数名,
@GetMapping("/provider/test02/{name}/{age}")
String test02(@PathVariable("name") String name, @PathVariable("age")Integer age);
- 测试结果,
3.2.4. 传递对象
- 服务生产者提供的接口需要使用 Student 对象进行访问,
@PostMapping("/test03")
public void test03(@RequestBody Student student) throws InterruptedException {
log.info(student.toString());
}
- 服务消费者 servicd 需要使用@RequestBody 注解进行标注生产者接口的参数类型,
@PostMapping("/provider/test03")
void test03(@RequestBody Student student);
- 消费者 controller 创建一个对象传递给消费者,
@PostMapping("/test03")
public void test03() {
Student student = new Student();
student.setName("laowang");
student.setAge(13);
providerService.test03(student);
}
- 测试结果,
3.2.5. 文件上传
- 生产者接收到文件后,返回文件的名称,
@PostMapping(value = "/test04", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String test04(@RequestPart("file") MultipartFile file) {
return file.getOriginalFilename();
}
- 消费者使用 @RequestPart 注解进行标注生产者的参数名,特别注意的是需要 @PostMapping 注解要添加 produces 和 consumes 的值,这两个属性标注了生产者接受的媒体类型和需要处理的媒体类型。
@PostMapping(value = "/provider/test04",
produces = {MediaType.APPLICATION_JSON_VALUE},
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String test04(@RequestPart("file") MultipartFile file);
- 消费者 controller 上传文件后转发给生产者接口
@PostMapping(value = "/test04")
public String test04(MultipartFile file) {
return providerService.test04(file);
}
- 测试
3.2.6. 文件下载
文件下载有多种实现方式,比如
- 客户端传入文件路径,服务端根据文件路径,读取文件内容转成输入流,把输入流返回给客户端,客户端再把流写入文件后保存到本地;
- 使用 oss 对象存储
下面使用第一种方式进行实践,
- 生产者根据传入文件的的路径以及文件名读取文件流,并转成输出流返回给客户端
/**
* 文件下载
* @param response
* @param myFiles
*/
@PostMapping(value = "/test05", consumes = MediaType.APPLICATION_JSON_VALUE)
public void test05(@RequestBody MyFiles myFiles, HttpServletResponse response) {
String filepath = myFiles.getPath()+"/"+myFiles.getFileName();
response.setContentType("application/x-download;charset=" + Charsets.UTF_8.displayName());
response.addHeader("Content-Disposition",
"attachment;filename=" + myFiles.getFileName());
FileInputStream fileInputStream = null;
try {
fileInputStream = new FileInputStream(filepath);
OutputStream out = response.getOutputStream();
byte buffer[] = new byte[1024];
int length = 0;
while ((length = fileInputStream.read(buffer)) >= 0){
out.write(buffer,0,length);
}
ServletOutputStream outputStream = response.getOutputStream();
IOUtils.copy(fileInputStream, outputStream);
outputStream.flush();
} catch (IOException e) {
log.info("下载失败....");
} finally {
if(fileInputStream != null){
try {
fileInputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
- 消费者使用 @RequestBody 标注生产者接受的参数的类型
@PostMapping(value = "/provider/test05", consumes = MediaType.APPLICATION_JSON_VALUE)
Response test05(@RequestBody MyFiles myFiles);
- 消费者 controller
/**
* 下载
* @param myFiles
* @return
*/
@PostMapping("/test05")
public void test05(@RequestBody MyFiles myFiles, HttpServletResponse httpServletResponse) {
String filename = myFiles.getPath() + "/" + myFiles.getFileName();
httpServletResponse
.setContentType("application/x-download;charset=" + Charsets.UTF_8.displayName());
httpServletResponse.addHeader("Content-Disposition",
"attachment;filename=" + myFiles.getFileName());
Response response = providerService.test05(myFiles);
Response.Body body = response.body();
try (OutputStream outputStream = httpServletResponse.getOutputStream();
InputStream inputStream = body.asInputStream()) {
IOUtils.copy(inputStream, outputStream);
outputStream.flush();
} catch (IOException e) {
log.error("下载文件异常 filepath:[{}]", filename, e);
}
}
- 测试结果
3.2.7. 开启 GZIP 压缩
有时为了节省网络带宽,我们需要对 OpenFeign 进行请求的压缩,其原理大概是,对请求的数据提供一种压缩算法,使用这种算法对数据进行压缩,接收到数据再使用同样的算法对数据进行解压。
开启 GZIP 压缩也很简单,只需要在服务消费端配置上:
feign:
compression:
request:
enabled: true
mime-types: text/xml,application/xml,application/json # 压缩的请求类型
min-request-size: 2048 # 压缩数据的下限
response:
enabled: true # 开启响应的zip压缩
3.2.8. 超时控制
有时候由于网络问题,生产者提供的接口的响应时间过长,就会造成消费者接口的请求时间过长,继而响应异常,此时我们就需要修改客户端的默认链接时间:
feign:
# 修改客户端的连接超时时间
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
实验时,我们可以在生产者提供的接口中休眠 3s,就会发现消费者接口报错。然后我们通过配置消费者的客户端链接超时时间,重新启动后就会发现响应正常。
3.2.9. 替换默认的客户端
OpenFeign 底层用的是 JDK 原生的 URLConnection 作为客户端的,但是我们可以自定义客户端,如可以配置 Apache 的 HttpClient 或者 Okhttp。
替换默认的客户端的方法很简单,主要有两步,下面以替换成 Okhttp 为例:
- 添加要替换的客户端的依赖坐标
<!--okhttp客户端-->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
- 在配置文件中开启
feign:
okhttp:
enabled: true # 启用okhttp
3.2.10. 其他
此外 OpenFeign 还有其他高级特性,有兴趣的读者可以自行实践一下:
- 自定义配置某一个 Feign 客户端的相关属性;
- Feign 调用时透传特定请求头
- ...
4. OpenFeign 的原理
- 由于启动类上添加了 @EnableFeignClients 注解,并标注了需要扫描@FeignClients 客户端所在的包路径,程序启动时会扫描包路径,并把这些信息注入到 Spring 的容器中;
- 当发生调用时,Spring 会通过 JDK 代理的方式,为客户端生成具体的 RestTemplate;
- 在把 RestTemplate 生成的 Request 交给具体的 Client,最后 Client 被封装到 LoadBalanceClient 类,这个类会结合 Ribbon 负载均衡发起服务的调用;
5. 总结
- 介绍了 SpringCloud 的远程调用组件及在调用过程中的两个服务角色: 生产者 和 消费者;
- 实践了 OpenFeign 的几种高级特性;
- 介绍了 OpenFeign 的工作原理;