RPC全称是Remote Procedure Call,即远程过程调用,其对应的是我们的本地调用。
RPC的目的是:让我们调用远程方法像调用本地方法一样。
//本地调用
R result = orderService.findOrderByUserId(id);
//RPC远程调用 orderService为代理对象
R result = orderService.findOrderByUserId(id);
RPC框架设计架构:
那么微服务之间如何方便优雅的实现服务间的远程调用?
什么是Feign
Feign是Netflix开发的声明式、模板化的HTTP客户端,Feign可帮助我们更加便捷、优雅地调用HTTP API。
Feign可以做到使用HTTP请求远程服务时就像调用本地方法一样的体验,开发者完全感知不到这是远程方法,更感知不到这是个 HTTP请求。它像Dubbo一样,consumer直接调用接口方法调用provider,而不需要通过常规的Http Client构造请求再解析返回数据。它解决了让开发者调用远程接口就跟调用本地方法一样,无需关注与远程的交互细节,更无需关注分布式环境开发。
Spring Cloud openfeign对Feign进行了增强,使其支持Spring MVC注解,另外还整合了Ribbon和Eureka,从而使得Feign的使用更加方便。
Feign的设计架构:
Spring Cloud Alibaba快速整合Feign
引入依赖
<!-- openfeign 远程调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
编写调用接口+@FeignClient注解
package com.morris.user.client;
import com.morris.user.entity.Order;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import java.util.List;
@FeignClient(value = "order-service", path = "/order")
public interface OrderClient {
@GetMapping("findOrderByUserId")
List<Order> findOrderByUserId(@RequestParam("userId") Long userId);
}
调用端在启动类上添加@EnableFeignClients注解
package com.morris.user;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients // 开启OpenFeign
@SpringBootApplication
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}
发起调用,像调用本地方式一样调用远程服务
package com.morris.user.controller;
import com.morris.user.client.OrderClient;
import com.morris.user.entity.Order;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import java.util.List;
@RestController
@RequestMapping("user3")
public class OpenFeignController {
@Resource
private OrderClient orderClient;
@GetMapping("findOrderByUserId1")
public List<Order> findOrderByUserId1(Long userId) {
return orderClient.findOrderByUserId(userId);
}
}
Feign的继承特性可以让服务的接口定义单独抽出来,作为公共的依赖,以方便使用。
GET请求
没有参数
没有参数就很简单,只需要使用@GetMapping标识好请求路径就可以了。
@GetMapping("getOrder")
Order getOrder();
多个基础类型参数
Get请求多个参数时,需要使用@RequestParam或者@PathVariable注解,这是因为在加载方法元数据的时候,如果该形参没有注解,默认会直接将其放在请求体中,这样GET请求时就会报错。
而且注解中的value属性必须指定绑定的参数名,不然会报错RequestParam.value() was empty on parameter 0
。
@GetMapping("findOrderByUserId")
List<Order> findOrderByUserId(@RequestParam("userId") Long userId);
@GetMapping("findOrderByUserId/{userId}")
List<Order> findOrderByUserId2(@PathVariable("userId") Long userId);
集合数据参数
使用GET请求传递集合参数时,需要这么写:
@GetMapping("getOrders")
List<Order> getOrders(@RequestParam("idList") List<Long> idList);
在请求时,URL集合值使用逗号隔开:
http://localhost:8030/user3/getOrders=1,2
在使用Feign调用时,可以从日志看到,Feign自动将集合类型的参数进行了解析拼接。
2023-08-14 16:07:23.283 DEBUG 20288 --- [nio-8030-exec-6] com.morris.user.client.OrderClient : [OrderClient#getOrders] ---> GET http://order-service/order/getOrders?idList=1&idList=2 HTTP/1.1
单个对象参数
当Get请求参数超过三个时,就需要进行查询参数封装为对象。
上面说到当参数没有注解时,就会放入到请求体中,但是@RequestParam不支持直接传递对象类,这时就需要使用@SpringQueryMap注解,它可以将对象属性及值,转为键值对拼接在URL后面。
@GetMapping("checkOrder")
Order checkOrder(@SpringQueryMap Order order);
从日志中可以看出对象被转为键值对拼接在URL后面:
2023-08-14 16:10:05.108 DEBUG 20288 --- [nio-8030-exec-9] com.morris.user.client.OrderClient : [OrderClient#checkOrder] ---> GET http://order-service/order/checkOrder?goodName=Iphone%2013&price=9999&id=1&userId=1 HTTP/1.1
多个对象参数(不支持)
多个对象参数时,只需要使用多个@SpringQueryMap即可:
@GetMapping("getOrderUser")
Order getOrderUser(@SpringQueryMap Order order, @SpringQueryMap User user);
从日志中可以看出第二个对象不会被转为键值对拼接在URL后面:
2023-08-14 16:21:09.061 DEBUG 24776 --- [nio-8030-exec-3] com.morris.user.client.OrderClient : [OrderClient#getOrderUser] ---> GET http://order-service/order/getOrderUser?goodName=Iphone%2013&price=9999&id=1&userId=1 HTTP/1.1
一个对象+基础类型参数
需要注意实体类中的属性和这个基础类型参数名不能相同,不然拼接到URL中,两个同名参数传递过去就会覆盖了。
@GetMapping("getOrderName")
Order getOrderName(@SpringQueryMap Order order, @RequestParam("name") String name);
下载文件
下载文件需要注意的是,应该返回二进制数据,还有GET请求时,需要对URL进行编码,不然会报错编码不符合规范,其他没什么区别。
@GetMapping("/download")
byte[] download(@RequestParam("filePath") String filePath);
POST请求
POST请求也支持将参数放在URL上,使用方式与GET请求类似。
传递单个对象参数
服务消费者的参数不需要增加任何注解,就会放入到请求体中,这个时候会使用编码器,将对象编码,并以Content-Type: application/json形式发送请求。
@PostMapping("saveOrder")
Long saveOrder(Order order);
所以只要注意在服务提供者中,添加@RequestBody注解,将请求体转为对象即可。
传递多个对象参数(不支持)
如果像下面这样使用两个@RequestBody,消费者启动时是会报错的:
@PostMapping("saveOrderAndUser")
Long saveOrderAndUser(Order order, User user);
报错日志如下:
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.morris.user.client.OrderClient': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method has too many Body parameters: public abstract java.lang.Long com.morris.user.client.OrderClient.saveOrderAndUser(com.morris.user.entity.Order,com.morris.user.entity.User)
Warnings:
这时候需要将两个对象封装在一个对象中,变成单个对象进行请求传递。
上传文件
上传文件时,需要注意的是需要使用@RequestPart注解,Fiegn会解析这个注解,标记为上传文件请求,还需要指定consumes为MULTIPART_FORM_DATA_VALUE,编码器会根据这个配置,将文件对象进行编码。
@PostMapping(value = "upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
String upload(@RequestPart("file") MultipartFile file);
在调用方,上传文件时,则需要将文件对象转为MultipartFile。
@GetMapping("upload")
public String upload() throws URISyntaxException {
File file = new File(OpenFeignController.class.getResource("/application.yml").toURI());
MultipartFile multipartFile =fileToMultipartFile(file);
return orderClient.upload(multipartFile);
}
private MultipartFile fileToMultipartFile(File file) {
String fieldName = "file";
FileItemFactory factory = new DiskFileItemFactory(16, null);
FileItem item = factory.createItem(fieldName, "multipart/form-data", true, file.getName());
int bytesRead = 0;
byte[] buffer = new byte[8192];
try {
FileInputStream fis = new FileInputStream(file);
OutputStream os = item.getOutputStream();
while ((bytesRead = fis.read(buffer, 0, 8192)) != -1) {
os.write(buffer, 0, bytesRead);
}
os.close();
fis.close();
} catch (IOException e) {
e.printStackTrace();
}
return new CommonsMultipartFile(item);
}
一般不采用Feign去上传文件,这种方式请求链路比较长,性能很低,一般都是前端直接上传到文件服务器,然后再告诉后台上传了哪个文件。
header中加参数
有时候接口验签的场景header中需要追加一些参数,可以通过@RequestHeader注解实现。
@GetMapping("header1")
String header1(@RequestHeader(name = "authorization")String authorization);
如果是固定参数,可以通过@Headers指定:
@PostMapping("header2")
@Headers({"Content-Type:application/json","Authorization:{authorization}"})
String header2(@RequestHeader(name = "authorization")String authorization, User user);