SpringCloudAlibaba-feign(生产环境最

Feign

Feign是Netflix开源一种负载均衡的声明式Http客户端,使用Feign调用APi就像调用本地方法一样,避免了调用目标服务时,需要不断的解析/封装JSON数据的麻烦。Feign致力于编写Java的http客户端更加简便。

为什么要使用Feign

在我们微服务环境中,服务发现使用nacos实现,负载均衡使用ribbon实现,但是现有技术体系下的服务间调用存在以下问题,也是为什么我们需要使用Feign的原因:

  1. 代码可读性差
  2. 复杂的URL难以维护
  3. 难以响应需求的变化,在快速迭代的过程中很痛苦
  4. 编程体验不统一

Feign实现http调用

  1. 加依赖

    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    复制代码
  2. 写注解

    在启动类添加 @EnableFeignClients(basePackages = "com.samir.contentcenter.feignclient") 注解(basePackages为feign接口所在包路径)。

  3. 写配置

  4. 改写代码

    1. 编写feign接口

      package com.samir.contentcenter.feignclient;
      
      import com.samir.contentcenter.domian.dto.user.UserDTO;
      import org.springframework.cloud.openfeign.FeignClient;
      import org.springframework.web.bind.annotation.GetMapping;
      import org.springframework.web.bind.annotation.RequestParam;
      
      @FeignClient(name = "user-center", path = "/users")
      public interface UserCenterClient {
      
          /**
           * http://user-center/users/{id}
           * @param id
           * @return
           */
          @GetMapping(value = "/{id}")
          UserDTO findById(@RequestParam("id") Integer id);
      }
      复制代码
    2. 修改原有代码调用

      package com.samir.contentcenter.service.content.impl;
      
      import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
      import com.samir.contentcenter.domian.dto.content.ShareDTO;
      import com.samir.contentcenter.domian.dto.user.UserDTO;
      import com.samir.contentcenter.domian.entity.content.Share;
      import com.samir.contentcenter.dao.content.ShareDao;
      import com.samir.contentcenter.feignclient.UserCenterClient;
      import com.samir.contentcenter.service.content.ShareService;
      import org.springframework.beans.BeanUtils;
      import org.springframework.beans.factory.annotation.Autowired;
      import org.springframework.cloud.client.ServiceInstance;
      import org.springframework.cloud.client.discovery.DiscoveryClient;
      import org.springframework.stereotype.Service;
      import org.springframework.web.client.RestTemplate;
      
      import java.util.List;
      import java.util.concurrent.ThreadLocalRandom;
      import java.util.stream.Collectors;
      
      /**
       * Auto created by codeAppend plugin
       */
      @Service
      public class ShareServiceImpl extends ServiceImpl<ShareDao, Share> implements ShareService {
      
          @Autowired
          private RestTemplate restTemplate;
      
          @Autowired
          private DiscoveryClient discoveryClient;
      
          @Autowired
          private UserCenterClient userCenterClient;
      
          @Override
          public ShareDTO findById(Integer id) {
              // 获取分享详情
              Share share = baseMapper.selectById(id);
              // 获取发布人id
              Integer userId = share.getUserId();
      
      //        List<ServiceInstance> instances = discoveryClient.getInstances("user-center");
      //        List<String> urls = instances.stream().map(instance -> instance.getUri().toString() + "/users/{id}").collect(Collectors.toList());
      //
      //        // 随机算法
      //        int i = ThreadLocalRandom.current().nextInt(urls.size());
      //        // 远程调用用户中心服务接口
      //        UserDTO userDTO = restTemplate.getForObject(urls.get(i), UserDTO.class, userId);
      
              // 使用feign调用用户中心接口
              UserDTO userDTO = userCenterClient.findById(userId);
      
              // 消息的装配
              ShareDTO shareDTO = ShareDTO.builder()
                      .wxNickname(userDTO.getWxNickname())
                      .build();
              BeanUtils.copyProperties(share, shareDTO);
              return shareDTO;
          }
      }
      复制代码

Feign的组成

接口 作用 默认值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底层用什么取样请求 和Ribbo配合时:LoadBalancerFeignClient;不配合时:feign.Client.Default
Contract 契约,注解支持 SpringMvcContract
Encoder 编码器,将对象转换成http请求消息体 SpringEncoder
Decoder 解码器,将响应消息体转换成对象 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于为每个请求添加通用逻辑

自定义Feign日志级别

级别 打印内容
NONE(默认值) 不记录任何日志
BASIC 仅记录请求方法、URL、响应状态代码以及执行时间
HEADERS 记录BASIC级别的基础上,记录请求和响应的header
FULL 记录请求和响应的header、body和元数据(适用于开发环境

细粒度配置(日志级别)

Java代码方式

  1. 编写Feign配置类

    package com.samir.contentcenter.configuration;
    
    import feign.Logger;
    import org.springframework.context.annotation.Bean;
    // 注意,如果这里加上了@Configuration注解,就得避免父子上文的问题,不然就是全局生效;不写就是最佳实现
    public class UserCenterFeignConfigurantion {
        @Bean
        public Logger.Level level() {
            return Logger.Level.FULL;
        }
    }
    复制代码
  2. 在Feign客户端引入配置类

    package com.samir.contentcenter.feignclient;
    
    import com.samir.contentcenter.configuration.UserCenterFeignConfigurantion;
    import com.samir.contentcenter.domian.dto.user.UserDTO;
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(name = "user-center", path = "/users", configuration = UserCenterFeignConfigurantion.class)
    public interface UserCenterClient {
    
        /**
         * http://user-center/users/{id}
         * @param id
         * @return
         */
        @GetMapping(value = "/{id}")
        UserDTO findById(@RequestParam("id") Integer id);
    
    }
    复制代码
  3. 在配置文件配置Feign客户端的日志级别为debug

    logging:
      level:
        com.samir.contentcenter.feignclient.UserCenterClient: debug
    复制代码

配置属性方式

​ feign.client.config..loggerLevel: 日志级别

feign:
  client:
    config:
      user-center:
        loggerLevel: full
复制代码

全局配置

Java代码方式

  • 方式一:让父子上下文ComponentSacn重叠(强烈不建议使用)

  • 方式二:@EnableFeignClients(defaultConfiguration=xxx.class)

    package com.samir.contentcenter;
    
    import com.samir.contentcenter.configuration.GlobalFeignConfiguration;
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
    import org.springframework.cloud.client.loadbalancer.LoadBalanced;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    import org.springframework.context.annotation.Bean;
    import org.springframework.web.client.RestTemplate;
    
    @SpringBootApplication
    @MapperScan("com.samir.contentcenter.dao")
    @EnableDiscoveryClient
    // 只需要在启动类这里设置feign的配置就行了
    @EnableFeignClients(basePackages = "com.samir.contentcenter.feignclient", defaultConfiguration = GlobalFeignConfiguration.class)
    public class ContentCenterApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(ContentCenterApplication.class, args);
        }
    
        @Bean
        @LoadBalanced
        RestTemplate restTemplate() {
            return new RestTemplate();
        }
    }
    复制代码

配置属性方式

feign.client.config.default.loggerLevel: 日志级别

feign:
  client:
    config:
      default: # 这里是default就是全局的配置
        loggerLevel: full
复制代码

支持的配置项

代码方式

配置项 作用
Feign.Builder Feign的入口
Client Feign底层用什么去请求
Contract 契约,注解支持
Encoder 编码器,用于将对象转换成http请求消息体
Decoder 解码器,用于将响应消息体转换为对象
Logger 日志管理器

属性方式

使用 **feign.client.config..属性 ** 的方式。如下属性:

  • connectTimeout: 5000 # 连接超时时间
  • readTimeout: 5000 # 读取超时时间
  • loggerLevel: full # 日志级别
  • errorDecoder: com.example.SimpleErrorDecoder # 错误解码器
  • retryer: com.example.SimpleRetryer # 重试策略
  • requestInterceptors: com.example.FooRequestInterceptor # 拦截器
  • decode404: false # 是否对404错误码解码(处理逻辑见 feign.SynchronousMethodHandler#executeAndDecode)
  • encoder: com.example.SimpleEncoder # 编码器
  • decoder: com.example.SimpleDecoder # 解码器
    • contract: com.example.SimpleContract # 契约

配置最佳实践

Ribbon配置 vs Feign配置

方式 粒度 Ribbon Feign
代码方式 局部 @RibbonClient(name = "user-center", configuration = xxx.class);xxx.class必须使用@Configuration并且不能父子上下文重叠 @FeignClient(name = "user-center", path = "/users", configuration = xxx.class);xxx.class必须使用@Configuration,如果使用不能父子上下文重叠
代码方式 全局 @RibbonClients(defaultConfiguration) @EnableFeignClients(defaultConfiguration)
属性方式 局部 .ribbon.NFLoadBalancerRuleClassName = 规则的全路径 feign.client.config..loggerLevel: 日志级别
属性方式 全局 - feign.client.config.default.loggerLevel: 日志级别

Feign代码方式 vs 属性方式

配置方式 优点 缺点
代码配置 基于代码,更加灵活 注意父子上下文的问题;线上修改需要重新打包发布
属性配置 易上手;配置简洁直观;线上修改无需重新打包发布(配置配置中心);优先级更高 极端场景下没有代码配置方式灵活

优先级:全局代码配置 < 全局属性配置 < 细粒度代码配置 < 细粒度属性配置

最佳实现

  • 尽量使用属性配置,属性方式实现不了的时候再考虑代码配置。
  • 在同一个微服务中尽量保持单一性,不要两种方式混用,增加定位代码的复杂性。简单就是美

Feign的继承

​ 以user-center服务为例,user-center提供的接口与content-center调用user-center服务的Feign客户端接口基本上是一样的,那么我们就可以考虑将其接口独立出来,以一个maven独立api模块管理。但是官方不建议这样使用,而现在很多企业在使用,故需要根据自身情况决定是否适应继承。

多参数请求构造

  • GET

    • 方式一

      @GetMapping(value = "/find")
      UserDTO find(@RequestParam("id") Integer id, @RequestParam("name") String name);
      复制代码
    • 方式二

      @GetMapping(value = "/find")
      UserDTO find(@StringQueryMap User user);
      复制代码
    • 方式三(不建议使用)

      @GetMapping(value = "/find")
      UserDTO find(@RequestParam Map<String, Object> map);
      复制代码
  • POST

    • 方式一

      @PostMapping(value = "/find")
      UserDTO find(@RequestBody User user);
      复制代码
    • 方式二(推荐)

      @PostMapping(value = "/find", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
      UserDTO find(User user);
      复制代码

Feign脱离Ribbon使用

使用feign调用未在注册中心注册的服务,例:www.baidu.com

package com.samir.contentcenter.feignclient;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "baidu", url = "http://www.baidu.com") // 使用url属性完成
public interface TestBaiduFeignClient {
    @GetMapping("")
    public String index();
}
复制代码

RestTemplate vs Feign

原则:尽量使用Feign,杜绝使用RestTemplate;但是出现Feign真的解决不了的问题,再考虑RestTemplate。

角度 RestTemplate Feign
可读性、可维护性 一般 极佳
开发体验 欠佳 极佳
性能 很好 中等(RestTemplate的50%左右)
灵活性 极佳 中等(内置功能可满足绝大多数场景)

Feign性能优化

为feign配置连接池【性能提升15%左右】

​ feign的底层默认使用UrlConnection请求,是没有连接池的。而Feign支持Apache的httpclient和okhttp,这两种http请求是支持连接池的,所有我们需要集成集中一种到我们的项目,配置。

  • httpclient

    1. 加依赖

      <dependency>
          <groupId>io.github.openfeign</groupId>
          <artifactId>feign-httpclient</artifactId>
      </dependency>
      复制代码
    2. 写配置

      feign:
        httpclient:
          enabled: true # 让feign使用apache httpclient 做请求,而不是使用默认的urlconnection
          # 通过压测的结果配置最优的连接池大小
          max-connections: 200 # feign的最大连接数
          max-connections-per-route: 50 # feign单个路径的最大连接数
      复制代码
  • okhttp

    1. 加依赖

      <dependency>
          <groupId>io.github.openfeign</groupId>
          <artifactId>feign-okhttp</artifactId>
          <version>10.4.0</version>
      </dependency>
      复制代码
    2. 写配置

      feign:
        okhttp:
          enabled: true # 让feign使用okhttp 做请求,而不是使用默认的urlconnection
        httpclient:
        	# 通过压测的结果配置最优的连接池大小
          max-connections: 200 # feign的最大连接数
          max-connections-per-route: 50 # feign单个路径的最大连接数
      复制代码

日志级别

若生产环境需要日志,建议将生产环境日志级别设置为Basic