OpenFeign 服务调用

在上一篇文章,利用nacos实现了服务的注册和发现,利用RestTemplate实现了服务的远程调用。但是远程调用的代码太复杂了。

而且这种调用方式,与原本的本地方法调用差异太大,编程时的体验也不统一,一会儿远程调用,一会儿本地调用。

因此,我们必须想办法改变远程调用的开发模式,让远程调用像本地方法调用一样简单。而这就要用到OpenFeign组件了。

其实远程调用的关键点就在于四个:

  • 请求方式
  • 请求路径
  • 请求参数
  • 返回值类型

所以,OpenFeign就利用SpringMVC的相关注解来声明上述4个参数,然后基于动态代理帮我们生成远程调用的代码,而无需我们手动再编写,非常方便。

1.快速入门

1.1 导入依赖

1
2
3
4
5
6
7
8
9
10
<!--openFeign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!--负载均衡器-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

1.2 启用OpenFeign

在对应的服务启动类上添加注解,启动OpenFeign功能:

1
2
3
4
5
6
7
8
@MapperScan("com.hmall.item.mapper")
@EnableFeignClients//开启openfeign
@SpringBootApplication
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class, args);
}
}

1.3 编写OpenFeign客户端(接口)

只需要为需要调用的服务(提供者)编写接口,也就是把对应被调用服务需要被调用的controller层的接口写成第三方接口(OpenFeign客户端),然后通过@FeignClient(“item-service”)声明被调用的是哪个服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.hmall.cart.client;

import com.hmall.cart.domain.dto.ItemDTO;
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("item-service")
public interface ItemClient {

@GetMapping("/items")
List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}

这里只需要声明接口,无需实现方法。接口中的几个关键信息:

  • @FeignClient("item-service") :声明服务名称
  • @GetMapping :声明请求方式
  • @GetMapping("/items") :声明请求路径
  • @RequestParam("ids") Collection<Long> ids :声明请求参数
  • List<ItemDTO> :返回值类型

有了上述信息,OpenFeign就可以利用动态代理帮我们实现这个方法,并且向http://item-service/items发送一个GET请求,携带ids为请求参数,并自动将返回值处理为List<ItemDTO>

我们只需要直接调用这个方法,即可实现远程调用了

1.4 使用FeignClient

通过注入上面声明接口,来调用其中的方法:

image-20231221132121859

这时候openfeign就提我们服务拉取,负载均衡,发送http请求,通过调用此接口中的方法就会找到对应声明的服务中controller对应的接口,完成服务调用。

1.5 连接池

Feign底层发起http请求,依赖于其它的框架。其底层支持的http客户端实现包括:

  • HttpURLConnection:默认实现,不支持连接池
  • Apache HttpClient :支持连接池
  • OKHttp:支持连接池

因此我们通常会使用带有连接池的客户端来代替默认的HttpURLConnection。比如,我们使用OK Http.

导入依赖然后在配置文件中开启即可:

1
2
3
4
5
<!--OK http 的依赖 -->
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
1
2
3
feign:
okhttp:
enabled: true # 开启OKHttp功能

2.抽取公共模块

为了避免代码重复编写,最好是将feign接口抽取到一个公共模块。

比如新建一个模块hm-api:

image-20231221162054810

将需要被调用的服务的接口以及需要用上的实体类,还有openfeign的配置都写在这个模块里面。

导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
<dependencies>
<!--open feign-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- load balancer-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
</dependencies>

最后只需在使用远程调用的服务加上这个模块的坐标即可:

1
2
3
4
5
<dependency>
<groupId>com.heima</groupId>
<artifactId>hm-api</artifactId>
<version>1.0.0</version>
</dependency>

然后需要在启动类上声明这个包的接口的位置:

1
2
3
4
5
6
7
8
9
10
11
@MapperScan("com.hmall.item.mapper")
//basePackages声明扫描包,找到接口
//defaultConfiguration声明配置的类,在EnableFeignClients中声明表示此配置全局生效(该服务调用所有的feignclint都生效)
//要想局部生效只需在单独的feignclient中声明
@EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class) //开启openfeign
@SpringBootApplication
public class ItemApplication {
public static void main(String[] args) {
SpringApplication.run(ItemApplication.class, args);
}
}
1
2
3
4
5
6
//局部feignclient启用配置,只是调用当前的feignclint生效
@FeignClient(value = "cart-service",configuration = DefaultFeignConfig.class)
public interface CartClient {
@DeleteMapping("/carts")
void deleteCartItemByIds(@RequestParam("ids") Collection<Long> ids);
}

DefaultFeignConfig:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
**
* @Author wzy
* @Date 2023/12/17 16:20
* @description: feign服务调用配置
*/
public class DefaultFeignConfig {
/**
* feign日志级别
* @return
*/
@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}

/**
* 由于微服务获取用户信息是通过拦截器在请求头中读取,
* 因此要想实现微服务之间的用户信息传递,就必须在微服务发起调用时把用户信息存入请求头
* 借助Feign中提供的一个拦截器接口:feign.RequestInterceptor
* @return
*/
@Bean
public RequestInterceptor userInfoRequestInterceptor(){
return new RequestInterceptor() {
@Override
public void apply(RequestTemplate requestTemplate) {
Long userId = UserContext.getUser();
if(userId!=null){
requestTemplate.header("user-info", userId.toString());
}
}
};
}

/**
* 服务降级实例化
* @return
*/
@Bean
public ItemClientFallback itemClientFallback(){
return new ItemClientFallback();
}

}