服务调用/通信-OpenFeign最佳实践

程序猿阿嘴   2023-04-26 18:12:41

我正在参加「掘金·启航计划」

Spring Cloud OpenFeign 它是 Spring 官方推出的一种声明式服务调用与负载均衡组件。它底层基于 Netflix Feign,Netflix Feign 是 Netflix 设计的开源的声明式 WebService 客户端,用于简化服务间通信。


(资料图片仅供参考)

Spring Cloud openfeign 对 Feign 进行了增强,使其支持 Spring MVC 注解,另外还整合了 Ribbon 和 Nacos,从而使得 Feign 的使用更加方便。

docs.spring.io/spring-clou…

原理

用到了 动态代理

对比 Dubbo

OpenFeign 好像不能直接调用 service 层,一般的写法是服务提供方必须写 controller 接口,而 dubbo 是不用写 controller 接口的。如果不能直接调用 service 层,必须写 controller 层,那么这应该是一个缺点,因为每次都额外要写一个 controller 方法。

而且在服务消费者方,还要额外写个有 @FeignClient 注解的远程调用 service 接口。看上去 Spring Cloud 的 OpenFeign 比 dubbo 麻烦不少。

引入依赖

使用 OpenFeign 组件需要引入客户端依赖

org.springframework.cloudspring-cloud-starter-openfeign复制代码

使用案例

在启动类上加上 @EnableFeignClients 注解

/** * 被调用方 */@RestControllerpublic class WarehouseController {    /**     * 查询对应 skuId 的库存状况     * @param skuId skuId     * @return Stock 库存对象     */    @GetMapping(\"/stock\")    public Stock getStock(Long skuId){    // ...省略    }}/** * 调用方 */@FeignClient(\"warehouse-service\")public interface WarehouseServiceFeignClient {        @GetMapping(\"/stock\")    public Stock getStock(@RequestParam(\"skuId\") Long skuId);}复制代码

@FeignClient 注解说明当前接口为 OpenFeign 通信客户端,参数值 warehouse-service 为服务提供者 ID(注意,OpenFeign服务名称不支持下划线_,这是一个坑),这一项必须与 Nacos 注册 ID 保持一致。

在 OpenFeign 发送请求前会自动在 Nacos 查询 warehouse-service 所有可用实例信息,再通过内置的 Ribbon 负载均衡选择一个实例发起 RESTful 请求,进而保证通信高可用。

tips: 这里的返回值虽然写的都是同一个类型,但他们可不一定是同一个类,本质上是以 Json 交互的,只要属性对的上即可。

@FeignClient 注解属性详解

contextId: 如果配置了contextId,该值将会作为beanName。

fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口

fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码

url: url一般用于调试,可以手动指定@FeignClient调用的地址

Spring Cloud CircuitBreaker Fallbacks

当断路器打开或出现错误时执行的默认逻辑,可以通过 fallback 来配置

注意:您还需要将其声明为 Spring bean 。

@FeignClient(name = \"test\", url = \"http://localhost:${server.port}/\", fallback = Fallback.class)protected interface TestClient {    @RequestMapping(method = RequestMethod.GET, value = \"/hello\")    Hello getHello();    @RequestMapping(method = RequestMethod.GET, value = \"/hellonotfound\")    String getException();}@Componentstatic class Fallback implements TestClient {    @Override    public Hello getHello() {        throw new NoFallbackAvailableException(\"Boom!\", new RuntimeException());    }    @Override    public String getException() {        return \"Fixed response\";    }}复制代码

如果需要访问导致回退触发的原因,可以使用 fallbackFactory 属性

@FeignClient(name = \"testClientWithFactory\", url = \"http://localhost:${server.port}/\",        fallbackFactory = TestFallbackFactory.class)protected interface TestClientWithFactory {    @RequestMapping(method = RequestMethod.GET, value = \"/hello\")    Hello getHello();    @RequestMapping(method = RequestMethod.GET, value = \"/hellonotfound\")    String getException();}@Componentstatic class TestFallbackFactory implements FallbackFactory{    @Override    public FallbackWithFactory create(Throwable cause) {        return new FallbackWithFactory();    }}static class FallbackWithFactory implements TestClientWithFactory {    @Override    public Hello getHello() {        throw new NoFallbackAvailableException(\"Boom!\", new RuntimeException());    }    @Override    public String getException() {        return \"Fixed response\";    }}复制代码

开启日志

工作中我们有些生产问题出现在微服务 OpenFeign 的互相调用中,然而 OpenFeign 的调用默认是不打日志的。

我们可以通过代码配置或者配置文件配置

//全局配置@Configurationpublic class FeignLoggerConfiguration {    @Bean    Logger.Level feignLoggerLevel() {        return Logger.Level.FULL;    }}//局部配置//不需要加Configuration注解public class FeignConfig {    @Bean    public Logger.Level feignLogLevel(){        //设置feign客户端的日志打印级别为FULL        return Logger.Level.FULL;    }}//configuration属性,值为Class,配置信息对名称为userprivilege的服务生效@Resource@FeignClient(name=\"userprivilege\",fallback = FallBack_PrivilegeService.class ,configuration = FeignConfig.class)public interface PrivilegeService {}复制代码
feign:  client:    config:      default: # 项目全局        loggerLevel: HEADERS      order-service: #@FeignClient注解中配置的服务名        loggerLevel: FULL复制代码

Level 有四个等级

NONE 不打日志,默认值 BASIC 只记录 method、url、响应码,执行时间 HEADERS 只记录请求和响应的 header FULL 全部都记录

上面修改了 openfeign 的日志级别是 debug,但是 springboot 默认日志级别是 info,因为 debug

logging:    level:    com.base.service: debug # 这里是openfeign client 所在的包路径复制代码

上传文件

www.baeldung.com/java-feign-…

@PostMapping(value = \"/upload-file\")public String handleFileUpload(@RequestPart(value = \"file\") MultipartFile file) {    // File upload logic}public class FeignSupportConfig {    @Bean    public Encoder multipartFormEncoder() {        return new SpringFormEncoder(new SpringEncoder(new ObjectFactory() {            @Override            public HttpMessageConverters getObject() throws BeansException {                return new HttpMessageConverters(new RestTemplate().getMessageConverters());            }        }));    }}@FeignClient(name = \"file\", url = \"http://localhost:8081\", configuration = FeignSupportConfig.class)public interface UploadClient {    @PostMapping(value = \"/upload-file\", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)    String fileUpload(@RequestPart(value = \"file\") MultipartFile file);}复制代码

性能优化

替换默认通信组件

OpenFeign 默认使用 Java 自带的 URLConnection 对象创建 HTTP 请求,但接入生产时,如果能将底层通信组件更换为 Apache HttpClient、OKHttp 这样的专用通信组件,基于这些组件自带的连接池,可以更好地对 HTTP 连接对象进行重用与管理。

io.github.openfeignfeign-okhttpio.github.openfeignfeign-httpclient复制代码

然后在配置文件中加入如下:

feign:  okhttp:    enabled: true# 或者feign:  httpclient:    enabled: true复制代码

经过上面设置已经可以使用okhttp了,因为在 FeignAutoConfiguration 中已实现自动装配

数据压缩

在 OpenFeign 中,默认并没有开启数据压缩功能。但如果你在服务间单次传递数据超过 1K 字节,强烈推荐开启数据压缩功能。默认 OpenFeign 使用 Gzip 方式压缩数据,对于大文本通常压缩后尺寸只相当于原始数据的 10%~30%,这会极大提高带宽利用率。,在项目配置文件 application.yml 中添加以下配置:

feign:  compression:    request:      enabled: true  # 开启请求数据的压缩功能      mime-types: text/xml,application/xml, application/json  # 压缩类型      min-request-size: 1024  # 最小压缩值标准,当数据大于 1024 才会进行压缩    response:      enabled: true  # 开启响应数据压缩功能复制代码

Tip提醒: 如果应用属于计算密集型,CPU 负载长期超过 70%,因数据压缩、解压缩都需要 CPU 运算,开启数据压缩功能反而会给 CPU 增加额外负担,导致系统性能降低,这是不可取的。 这种情况 建议不要开启数据的压缩功能

负载均衡

OpenFeign 使用时默认引用 Ribbon 实现客户端负载均衡,它默认的负载均衡策略是轮询策略。那如何设置 Ribbon 默认的负载均衡策略呢?

只需在 application.yml 中调整微服务通信时使用的负载均衡类即可。

warehouse-service: #服务提供者的微服务ID  ribbon:    #设置对应的负载均衡类    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule复制代码

Tip提醒: 出于性能方面的考虑,我们可以选择用 权重策略或区域敏感策略来替代轮询策略 ,因为这样的执行效率最高。

一种封装OpenFegin的实践

在了解这种实践前,我们需要先知道Feign对接口继承的写法支持

Feign 继承支持

参考:docs.spring.io/spring-clou…

Feign通过单继承接口支持模板api。这允许将常见操作分组到方便的基本接口中。

Feign 支持继承和多重继承,这样我们可以将API暴露到接口当中,服务提供者实现该接口来提供服务,服务消费者只需要引入该接口所在的API模块即可。

但早先时候官方并不倡导这么做,相关思考可见:

why-is-it-such-a-bad-idea-to-share-an-interface-between-server-and-client should-i-use-a-single-interface-for-feign-and-spring-mvc
public interface UserService {    @RequestMapping(method = RequestMethod.GET, value =\"/users/{id}\")    User getUser(@PathVariable(\"id\") long id);}复制代码
@RestControllerpublic class UserResource implements UserService {    @Override    public User getUsers() {        ...    }}复制代码
@FeignClient(\"users\")public interface UserClient extends UserService {}复制代码

tips: @FeignClient接口不应该在服务器和客户端之间共享,并且不支持在类级别上用@RequestMapping注释@FeignClient接口。

实践

将 Feign 的 Client 抽取为独立模块,并且把接口有关的 POJO、默认的 Feign 配置都放到这个模块中,提供给所有消费者使用。

例如:将 UserClients、User、Feign 的默认配置都抽取到一个 feign-api 包中,所有微服务引用该依赖包,即可直接使用。

服务提供方开发实践

RPC 接口和实现各自放在独立的模块中,方便服务调用方重用服务接口 ,服务接口模块只能包含最基本的模块依赖(过多会导致依赖传递)。

服务的接口及相关模型必须放在同一模块中,该模块仅包含这些内容, 服务提供方的接口中不能指定 FeignClient 注解(在消费方指定),在方法中可以使用 @RequestMapping 等注解 。

例如:

account - account-api - account-service复制代码

account-api 模块中放消费方需要用到的东西, api接口,vo,入参等...

public interface AccountApi {    @RequestMapping(method = RequestMethod.GET, value =\"/users/{id}\")    User getUser(@PathVariable(\"id\") long id);}复制代码

account-service 实现 account-api 提供的接口

@RestController@Api(tags = \"用户接口\")public class AccountController implements AccountApi {    ...}复制代码

服务消费方开发实践

引用 RPC 接口模块,在消费方编写 feign 客户端类继承相关接口并处理熔断,在客户端接口上增加 FeignClient 注解即可。

@Component@FeignClient(name = \"account-service\",fallbackFactory = AccountClientFallbackFactory.class)public interface AccountClient extends AccountApi {    ...}@Componentpublic class AccountClientFallbackFactory implements FallbackFactory {    @Override    public AccountClient create(Throwable throwable) {        AccountClientFallback accountClientFallback = new AccountClientFallback();        accountClientFallback.setCause(throwable);        return accountClientFallback;    }}@Slf4jpublic class AccountClientFallback implements AccountClient {    @Setter    private Throwable cause;    @Override    public ResultData getByCode(String accountCode) {...    }}复制代码

扩展

Spring Cloud OpenFeign 特性完成公告

由于 Spring 现在提供了自己的 HTTP 接口客户端解决方案,比如在最新的 Spring Boot 3.0 中实现接口调用可以有以下两种解决方案:

RestTemplate WebClient

所以,从 Spring Cloud 2022.0.0 版本开始,Spring Cloud OpenFeign 模块已经视为功能完成状态了,这意味着 Spring Cloud 团队将不再向该模块添加新功能。

虽然 OpenFeign 不会再添加新功能,但还是会继续修复错误和安全问题,并且也还会考虑和审查来自社区的小规模的 pull requests 请求。

HTTP Interface

Spring 6 中带来了一个新的特性——HTTP Interface。这个新特性,可以让开发者将 HTTP 服务,定义成一个包含特定注解标记的方法的 Java 接口,然后通过对接口方法的调用,完成 HTTP 请求。看起来很像使用 Feign 来完成远程服务调用。

docs.spring.io/spring-fram…

原文链接:https://juejin.cn/post/7201316570315440184

相关新闻