前言
在微服务当中,当一个服务调用另外一个服务的时候,我们的方式是使用 RestTemplate 的方式来调用另外一个服务:
而使用 RestTemplate 就需要先构造出 URL,如果需要传递的参数很多的话,就需要构造出很复杂的 URL,不仅构造 URL 的时候很容易出错,而且 URL 也会显得很臃肿,那么如何更加优雅的实现服务和服务之间的调用呢?
OpenFeign
OpenFeign 是一个声明式的 Web 服务客户端工具,通常用于简化与 RESTful Web 服务的通信。它可以自动生成 HTTP 请求的客户端代码,使得开发者能够通过接口定义来轻松发起 HTTP 请求。通过 OpenFeign,开发者可以避免手动编写大量的 HTTP 请求相关代码,从而提高开发效率。
就类似 controller 调用 service,只需要创建一个接口,然后添加注解即可使用 OpenFeign。
Feign 是 Netflix 公司开源的一个组件。
- 2013年6月,Netflix 发布 Feign 的第一个版本 1.0.0
- 2016年7月,Netflix 发布 Feign 的最后一个版本8.18.0
2016年,Netflix 将 Feign 捐献给社区。
- 2016年7月 OpenFeign 的首个版本 9.0.0 发布,之后一直持续发布到现在
可以理解为 Netflix Feign 是 OpenFeign 的祖先,或者说 OpenFeign 是 Netflix Feign 的升级版。OpenFeign 是 Feign 的一个更强大更灵活的实现
Spring Cloud Feign
Spring Cloud Feign 是 Spring 对 Feign 的封装,将 Feign 项目集成到 Spring Cloud 生态系统中。并且受 Feign 更名的影响,Spring Cloud Feign 也有两个 starter:
- spring-cloud-starter-feign
- spring-cloud-starter-openfeign
而我们使用的就是 spring-cloud-starter-openfeign。OpenFeign 官方文档:https://github.com/OpenFeign/feign,Spring Cloud Feign 文档:https://spring.io/projects/spring-cloud-openfeign
Spring Cloud OpenFeign的使用
引入依赖
首先我们需要在要调用其他服务的服务中添加 openfeign 依赖:
<!-- openfeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
添加注解
当添加完成依赖之后,我们需要在调用其他服务的服务的启动类上添加 @EnableFeignClients
注解,表示开启 OpenFeign 功能。
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
@EnableFeignClients
@SpringBootApplication
public class OrderServiceApplication {
public static void main(String[] args) {
SpringApplication.run(OrderServiceApplication.class,args);
}
}
编写 OpenFeign 客户端代码
因为服务的具体实现是由其他服务实现的,所以我们在当前服务中就创建一个接口,然后声明出需要调用的服务的方法就可以了:
import org.example.model.ProductInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
}
我们需要在接口上添加 @FeignClient
注解,其中 value 就是需要调用的微服务的名称,也就是我们在 properties/yml 配置文件中配置的 spring.application.name 服务名称:
path 则是定义当前 FeignClient 的统一前缀,因为我们的 product-service 服务上添加了类注解 @RequestMapping
,所以使用这个 path 就可以避免在当前 FeignClient 的每个方法声明的注解上加上 /product 注解。
这个类除了类型是接口,以及方法没有具体的实现之外,其他的和正常的 Spring MVC 是一样的:
远程调用
当我们完成上面的配置之后,就可以将 RestTemplate 更换为 OpenFeign 远程调用了:
@Autowired
private ProductApi productApi;
public OrderInfo selectOrderById(Integer id) {
OrderInfo orderInfo = orderMapper.selectOrderById(id);
ProductInfo productInfo = productApi.getProductById(orderInfo.getProductId());
orderInfo.setProductInfo(productInfo);
return orderInfo;
}
可以看到,使用 OpenFeign 简化了与 HTTP 服务交互的过程,把 REST 客户端的定义转换为了 Java 接口,并通过注解的方式来声明请求参数,请求方式等信息,使得远程调用更加方便和直接。
上面我们传递的参数数量是一个,并且类型也是 Java 的基本数据类型,那么如果我们要传入多个并且参数类型也不是 Java 的基本数据类型该怎么做呢?
OpenFeign 参数传递
传递单个参数
服务方代码:
@RequestMapping("/product")
@RestController
public class ProductController {
@Autowired
private ProductService productService;
@RequestMapping("/p1")
public String p1(Integer id) {
return "p1接收到参数:" + id;
}
}
OpenFeign 客户端代码:
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi {
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
}
注意: 参数绑定的 @RequestParam
不可以省略,但是里面绑定的值是可以省略的。
远程调用方:
@RequestMapping("/feign")
@RestController
public class FeignController {
@Autowired
private ProductApi productApi;
@RequestMapping("/o1")
public String o1(Integer id) {
return productApi.p1(id);
}
}
传递多个普通类型的参数
当传递多个普通类型的参数的时候,也没什么其他需要注意的,就是只是每个参数需要使用 @RequestParam
进行参数绑定。
服务方 product-service 代码:
@RequestMapping("/p2")
public String p2(Integer id,String name) {
return "接收到参数,id:" + id + ",name:" + name;
}
OpenFeign 客户端代码:
@RequestMapping("/p2")
String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);
远程调用方代码:
@RequestMapping("/o2")
public String o2() {
return productApi.p2(2,"张三");
}
传递对象
当远程调用方传递的参数类型是 Java 对象的时候,就不能使用 @RequestParam
注解了,而是需要使用 SpringQueryMap
注解:
服务方 product-service 代码:
@RequestMapping("/p3")
public String p3(ProductInfo productInfo) {
return "接收到对象,productInfo:" + productInfo.toString()
}
OpenFeign 客户端代码:
import org.springframework.cloud.openfeign.SpringQueryMap;
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo);
远程调用代码:
@RequestMapping("/o3")
public String o3() {
ProductInfo productInfo = new ProductInfo();
productInfo.setId(1024);
productInfo.setProductName("李四");
return productApi.p3(productInfo);
}
传递JSON
当远程调用方传递的参数的类型是 JSON 类型的话,就需要使用 @RequestBody
来进行参数绑定。
服务方 product-service 代码:
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo) {
return "接收到的对象,productInfo:" + productInfo.toString();
}
OpenFeign 客户端代码:
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
远程调用代码:
@RequestMapping("/o4")
public String o4(@RequestBody ProductInfo productInfo) {
return productApi.p4(productInfo);
}
然后我们调用的话,就使用 postman 来构造 json 请求:
最佳实践
通过观察 OpenFeign 的客户端和服务提供方的代码我们可以发现;
OenFeign 客户端就可以看作是被调用服务的一个接口,这两个部分的代码非常的相近,换句话说,有很多重复的代码,而且不仅仅是重复的代码,比如说 ProductInfo 这个实体类:
我们在 order-service 这个服务中调用 product-service 这个服务,但是却需要知道 product-service 中的实体类,而微服务中服务和服务之间应该只存在调用和被调用的关系,一个服务是不应该知道另一个服务的具体实现细节的,那么这个 ProductInfo 该如何得到呢?
有人说,我们直接在 order-service 这个服务中引入 product-service jar 包不就可以直接使用 ProductInfo 这个实体类吗?那么你既然引入了 product-service 这个 jar 包,那么product-service 中具体的实现细节你不就全部知道了吗,所以这个方法是肯定行不通的。
那么到底该如何解决这个问题呢?我们可以将公共的代码以及需要用到的实体类全部封装一个公共的 jar 包中,然后这个公共的 jar 包中只存放这些内容,然后我们的服务实现方和服务调用方只需要导入这个公共的 jar 包就可以了。
1.Feign继承方式
我们可以将一些常见的操作封装到一个公共的接口里,然后服务提供方实现这个接口,服务调用方在编写 Feign 接口的时候直接继承这个接口就可以了。
我们可以将接口放在一个公共的 jar 包中,这样服务调用方和服务提供方只需要导入这个 jar 包就可以了:
新建 module
在新建的 module 下添加需要的依赖 spring boot 和 spring cloud openfeign:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
编写接口:
package org.example.api;
import org.example.model.ProductInfo;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
public interface ProductInterface {
@RequestMapping("/{productId}")
ProductInfo getProductById(@PathVariable("productId") Integer productId);
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
@RequestMapping("/p2")
String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo);
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
}
编写 ProductInfo 类:
package org.example.model;
import lombok.Data;
import java.util.Date;
@Data
public class ProductInfo {
private Integer id;
private String productName;
private Integer productPrice;
private Integer state;
private Date createTime;
private Date updateTime;
}
当编写完成接口以及实体类之后,服务调用方和服务实现方该如何导入这个 jar 包呢?我们可以将这个 jar 包发布到 maven 中央仓库中,然后从中央仓库中导入这个 jar 包,但是 maven 中央仓库中发布内容是比较麻烦的,所以我们可以将 jar 包下载到本地,然后从本地导入这个 jar 包。
当我们将提取出来的公共 module 进行打包之后,接下来就在服务提供方和服务调用方引入这个 jar 包:
<dependency>
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
服务实现方实现接口:
package org.example.controller;
import lombok.extern.slf4j.Slf4j;
import org.example.api.ProductInterface;
import org.example.model.ProductInfo;
import org.example.service.ProductService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RequestMapping("/product")
@RestController
public class ProductController implements ProductInterface {
@Autowired
private ProductService productService;
@RequestMapping("/{productId}")
public ProductInfo getProductById(@PathVariable("productId") Integer productId) {
log.info("接收到参数: productId"+productId);
return productService.selectProductById(productId);
}
@RequestMapping("/p1")
public String p1(Integer id) {
return "p1接收到参数:" + id;
}
@RequestMapping("/p2")
public String p2(Integer id,String name) {
return "接收到参数,id:" + id + ",name:" + name;
}
@RequestMapping("/p3")
public String p3(ProductInfo productInfo) {
return "接收到对象,productInfo:" + productInfo.toString();
}
@RequestMapping("/p4")
public String p4(@RequestBody ProductInfo productInfo) {
return "接收到的对象,productInfo:" + productInfo.toString();
}
}
服务调用方继承接口:
package org.example.api;
import org.springframework.cloud.openfeign.FeignClient;
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi extends ProductInterface{
}
2. Feign 抽取方式
官方推荐 Feign 的使用方式为继承的方式,但是企业开发中,更多的是把 Feign 接口抽取为一个独立的模块(做法和继承相似,但是理念不同)
如何实现这个做法呢?将 Feign 的 Client 抽取为一个独立的模块,并把涉及到的实体类都放在这个模块中,打成一个 jar 服务。消费方只需要依赖该 jar 包即可,这种方式在企业中比较常见,jar 包通常由服务提供方来实现。
新建一个 module:
引入依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
</dependencies>
编写 API 和实体类:
package org.example.api;
import org.example.model.ProductInfo;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.SpringQueryMap;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@FeignClient(value = "product-service",path = "/product")
public interface ProductApi{
@RequestMapping("/p1")
String p1(@RequestParam("id") Integer id);
@RequestMapping("/p2")
String p2(@RequestParam("id") Integer id,@RequestParam("name") String name);
@RequestMapping("/p3")
String p3(@SpringQueryMap ProductInfo productInfo) ;
@RequestMapping("/p4")
String p4(@RequestBody ProductInfo productInfo);
}
package org.example.model;
import lombok.Data;
import java.util.Date;
@Data
public class ProductInfo {
private Integer id;
private String productName;
private Integer productPrice;
private Integer state;
private Date createTime;
private Date updateTime;
}
打包:
服务消费方使用 product-api:
首先引入 product-api 依赖:
<groupId>org.example</groupId>
<artifactId>product-api</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>system</scope>
<systemPath>D:/Maven/.m2/repository/org/example/product-api/1.0-SNAPSHOT/product-api-1.0-SNAPSHOT.jar</systemPath>
</dependency>
然后将我们之前导入的 ProductApi 和 ProudctInfo 的包改为 product-api 下的 ProductApi 和 ProductInfo,然后启动项目:
我们使用这个方法,直接使用 product-api 包下的 Feign-client 的话,项目是启动不了的,还需要添加上 @EnableFeignClients(basePackages = {})
注解,这个属性指定了Feign客户端接口所在的包路径。
@EnableFeignClients(basePackages = {"com.example1.api"})
@RequestMapping("/order")
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@Autowired
private ObjectMapper objectMapper;
@RequestMapping("/{orderId}")
public OrderInfo getOrderById(@PathVariable("orderId") Integer orderId) {
OrderInfo orderInfo = orderService.selectOrderById(orderId);
return orderInfo;
}
}