Spring Cloud OpenFeign
Feign 是一个声明式的 Web 服务客户端。它使编写 Web 服务客户端变得更加容易。要使用 Feign,请创建一个接口并对其进行注释。它具有可插入的注释支持,包括 Feign 注释和 JAX-RS 注释。Feign 还支持可插入的编码器和解码器。Spring Cloud 添加了对 Spring MVC 注释的支持,并使用 HttpMessageConverters
在 Spring Web 中默认使用的注释。Spring Cloud 集成了 Eureka,Spring Cloud CircuitBreaker,以及 Spring Cloud LoadBalancer,在使用 Feign 时提供负载均衡的 http 客户端。
必要依赖
提示
如果不需要模块之间服务名调用可以不引用 spring-cloud-starter-loadbalancer
依赖。如:仅与第三方对接的 MTK 模块。不会请求内部任何模块,则不需要引用;如果需要调用内部模块,则需要引用。
<!-- RPC 组件 openfeign 远程接口调用 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<!-- openfeign 负载均衡依赖-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- caffeine 缓存 -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
这里我们推荐使用使用我们的通用包引入,里面添加的通用的配置,如果不需要通用配置。 这个通用工具包中会包含我们系统内全部的 RPC 调用客户端,如果你的模块也需要提供给他人调用,那么也要在这里写好 Client
。
<!-- Open Feign 通用配置模块 -->
<dependency>
<groupId>com.simperfect.bp</groupId>
<artifactId>basic-paper-open-feign-client</artifactId>
</dependency>
启用 FeignClients
在启动类添加注解 @EnableFeignClients
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients("com.simperfect.*")
@ComponentScan("com.simperfect.*")
@MapperScan(basePackages = {"com.simperfect.bp.ticket.dao", "com.simperfect.commons.cache.dao"})
public class BasicPaperTicketApplication {
public static void main(String[] args) {
SpringApplication.run(BasicPaperTicketApplication.class, args);
}
}
添加通用配置
我们在 basic-paper-open-feign-client
的 SpOpenFeignConfiguration
中进行了配置,其中 userHeaderInterceptor
过滤器您可以创建一个同名的 bean 覆盖它, 但为了方便问题跟踪,请一定保留 feignLoggerLevel
。例如:
package com.simperfect.bp.ticket.config;
import com.simperfect.commons.consts.UserHeaderConsts;
import com.simperfect.commons.util.StringUtils;
import feign.Logger;
import feign.RequestInterceptor;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
/**
* 自定义的 OpenFeign 通用配置
* @author 王金城
* @date 2023/6/20
**/
@Configuration
public class TicketOpenFeignConfiguration {
/**
* OpenFeign header 过滤器,为请求添加用户信息支持
*
* @return feign.RequestInterceptor
* @author 王金城
* @date 2023/6/20
*/
@Bean
public RequestInterceptor headerInterceptor() {
if (DataSourceUtils.isTenantEnabled()) {
return template -> {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
// 如果没有 request 则可能是异步调用,从当前线程中获取 TENANT_SN
if (requestAttributes == null) {
String threadLocalTenantSN = DataSourceUtils.getThreadLocalTenantSN();
template.header(UserHeaderConsts.TENANT_SN, threadLocalTenantSN);
return;
}
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
String userSessionId = servletRequest.getHeader(UserHeaderConsts.USER_SESSION_ID);
String tenantSn = servletRequest.getHeader(UserHeaderConsts.TENANT_SN);
template.header(UserHeaderConsts.USER_SESSION_ID, userSessionId);
template.header(UserHeaderConsts.TENANT_SN, tenantSn);
};
} else {
return template -> {
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
if (requestAttributes == null) {
return;
}
HttpServletRequest servletRequest = ((ServletRequestAttributes) requestAttributes).getRequest();
String userSessionId = servletRequest.getHeader(UserHeaderConsts.USER_SESSION_ID);
template.header(UserHeaderConsts.USER_SESSION_ID, userSessionId);
};
}
}
/**
* 开启 openfeign 日志增强,记录请求和响应的标头、正文和元数据
*
* @return feign.Logger.Level
* @author 王金城
* @date 2023/6/20
*/
@Bean
public Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
修改日志输出级别
框架的日志框架使用的是 logback
,默认我们都是只输出 info 级别的日志,但是为了收集到详细的模块调用信息,我们需要将 feign client 的日志级别配置为 debug,我们需要将全部的 feign client 放在 client
包下,命名规则为 ${具体业务名}Client
;如:SkillClient
、EmployeeClient
。
在 application.yml
中加入如下代码,其中 com.simperfect.bp.openfeign.client
为包路径
logging:
level:
com.simperfect.bp.openfeign.client: debug
开启 openfeign 热更新
在 application.yml
中加入一下配置信息:
spring:
cloud:
openfeign:
client:
refresh-enabled: true
httpclient:
hc5:
enabled: false
ok-http:
protocols:
- H2_PRIOR_KNOWLEDGE
okhttp:
enabled: true
创建模块调用客户端接口
在项目中创建 client
包,并创建 Client 接口类,我们以 SkillClient
为例:
- 为接口类添加
@FeignClient
注解,name 为 要调模块名称,即:spring.application.name
的值。 - 创建对应方法,使用 spring mvc 注解写入要调用的接口。
@FeignClient("basic-paper-config")
public interface SkillClient {
// 普通的 get 请求可以直接使用 @GetMapping 注解
@GetMapping("skill/query_skill_tree")
ResultVO<List<SkillTreeDTO>> querySkillTree();
// 提交普通参数可以使用 @SpringQueryMap 注解
@GetMapping("skill/query_skill_page")
ResultVO<PageInfo<SkillDTO>> querySkillPage(@SpringQueryMap QueryPageSkillVO skillVO);
// json 形式提交可以使用 @RequestBody 注解
@PostMapping("create_skill")
ResultVO<Long> createSkill(@RequestBody CreateSkillVO createSkillVO);
// 同样支持 @PutMapping 注解
@PutMapping("update_skill")
ResultVO updateSkill(@RequestBody UpdateSkillVO updateSkillVO);
// 路径参数可以使用 @PathVariable 注解
@DeleteMapping("/stores/{storeId}")
void delete(@PathVariable Long storeId);
}
创建第三方调用客户端接口接口
警告
虽然 openfeign 可以用来请求第三方接口,但是我们推荐只用它进行 RPC 调用,第三方接口使用 WebClient
只有 @FeignClient
的参数不同,需要配置 name
可以为协议名 http 或 https,url
为三方接口访问地址
@FeignClient(name = "http", url = "127.0.0.1:8080")
public interface SkillClient {
}
第三方调用客户端动态 url 配置
- 创建一个客户端,仅填写客户端名称,如:
alibabaSkillClient
@FeignClient(name = "alibabaSkillClient")
public interface AlibabaSkillClient {
}
- 在
配置中心
中添加alibabaSkillClient
的 url 配置,其中alibabaSkillClient
为 FeignClient 的 name, 如下所示:
spring:
cloud:
openfeign:
client:
config:
alibabaSkillClient:
url: http://127.0.0.1:8080
注意
- 要 开启 openfeign 热更新 功能
- 启动程序前
配置中心
中要存在 url 的配置,否则将无法实现热更新 - 如果不想给第三方传输用户信息相关 header 记得修改 headerInterceptor 过滤器
调用客户端接口
与正常的 bean 使用相同,可以使用 @Resource
注解注入
@Service
public class TicketService implements ITicketService {
@Resource
private SkillClient skillClient;
@Override
public ResultVO<Void> createTicket() {
// 调用要查询的数据
ResultVO<List<SkillTreeDTO>> list = skillClient.querySkillTree();
return null;
}
}
超时处理
我们可以在默认客户端和指定客户端上配置超时。OpenFeign 使用两个超时参数:
connectTimeout
连接超时,防止由于服务器处理时间长而阻塞调用者。默认值:10000msreadTimeout
从建立连接开始应用,并在返回响应时间过长时触发。默认值:60000ms
一般情况下 connectTimeout
我们一直使用默认值即可,readTimeout
可以根据实际情况在 配置中心
中进行调整
spring:
cloud:
openfeign:
client:
config:
default:
readTimeout: 30000