SpringCloudAlibaba-feign(生产环境最佳实践)

3,687 阅读6分钟

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.BuilderFeign的入口Feign.Builder
ClientFeign底层用什么取样请求和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.BuilderFeign的入口
ClientFeign底层用什么去请求
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配置

方式粒度RibbonFeign
代码方式局部@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。

角度RestTemplateFeign
可读性、可维护性一般极佳
开发体验欠佳极佳
性能很好中等(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