Skip to content
标签
服务调用
工具
字数
1947 字
阅读时间
9 分钟

一、概述

Feign是Netflix开发的声明式,模板化的HTTP客户端,其灵感来自Retrofit,JAXRS-2.0以及WebSocket.

Feign可帮助我们更加便捷,优雅的调用HTTP API。

在SpringCloud中,使用Feign非常简单——创建一个接口,并在接口上添加一些注解,代码就完成了。

Feign支持多种注解,例如Feign自带的注解或者JAX-RS注解等。

SpringCloud对Feign进行了增强,使Feign支持了SpringMVC注解,并整合了Ribbon和Eureka,从而让Feign的使用更加方便。

1.1 工作原理

1、 启动类添加@EnableFeignClients注解,Spring会扫描标记了@FeignClient注解的接口,并生成此接口的代理对象

2、 @FeignClient(value = )即指定了服务名称,Feign会从注册中心获取服务列表,并通过负载均衡算法进行服务调用。

3、在接口方法 中使用注解@GetMapping("/cms/page/get/{id}"),指定调用的url,Feign将根据url进行远程调用。

1.2 注意事项

1、feignClient接口 有参数在参数必须加@PathVariable("XXX")@RequestParam("XXX")

2、feignClient返回值为复杂对象时其类型必须有无参构造函数。

二、使用Demo

2.1 基本使用

依赖

xml
<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring‐cloud‐starter‐openfeign</artifactId>
</dependency>
<dependency>
	<groupId>com.netflix.feign</groupId>
	<artifactId>feign‐okhttp</artifactId>
</dependency>

配置

yml
feign: 
  hystrix: # sentinel 
    enabled: true # 开启Feign的熔断功能
hystrix:
  command:
    default:
      execution:
        timeout:
          #设置熔断超时时间
          enabled:false
        loggerLevel: full
        isolation:
          thread:
            timeoutInMilliseconds:30000
  threadpool:
    default:
      #并发执行的最大线程数,默认10个,这里改成15个
      coreSize:15
      #BlockingQueue的最大队列数,默认值为-1,这里改成50个
      maxQueueSize:100
      #即使maxQueueSize没有达到,达到queueSizeRejectionThreshold该值后,请求也会被拒绝,默认值5,这里改成100
      queueSizeRejectionThreshold:100
ribbon:
  ReadTimeout:60000
  ConnectTimeout:2000

应用

java
//指定需要调用的微服务名称 
@FeignClient(name="shop-service-product")
public interface ProductFeginClient { 
    //调用的请求路径 
    @RequestMapping(value = "/product/{id}",method = RequestMethod.GET)
    public Product findById(@PathVariable("id") Long id);
}

定义各参数绑定时,@PathVariable、@RequestParam、@RequestHeader等可以指定参数属性,在Feign中绑定参数必须通过value属性来指明具体的参数名,不然会抛出异常

@FeignClient:注解通过name指定需要调用的微服务的名称,用于创建Ribbon的负载均衡器。所以Ribbon会把 shop-service-product 解析为注册中心的服务。

启用

启动类添加@EnableFeignClients注解

2.2 自定义构建

java
import feign.Feign;
import feign.Logger;
import feign.codec.Decoder;
import feign.codec.Encoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Import;
import org.springframework.stereotype.Component;

/**
 * feign工具类
 * 可借助缓存优化feign工具类调用。
 * @author wangxp
 * @Date 2020/9/27
 */
@Component
//@Import(FastJsonConfigurer.class)
public class FeignHelper {

    //Feign 原生构造器
    private Feign.Builder builder;

    public FeignHelper(Decoder decoder, Encoder encoder) {
        this.builder = Feign.builder()
                .encoder(encoder)
                .decoder(decoder)
                .logLevel(Logger.Level.HEADERS)
                .contract(new SpringMvcContract());
    }

    /**
     * 自定义Feign Target<br/>
     * 与注解自动生成的Target区别:可以手动指定服务实例
     *
     * @param apiType
     * @param url
     * @param <T>
     * @return
     */
    public <T> T target(Class<T> apiType, String url) {
        return this.builder.target(apiType, url);
    }

    public Feign.Builder getBuilder() {
        return this.builder;
    }
}

三、配置

3.1 feign组件配置

Feign可以支持很多的自定义配置,如下表所示:

类型作用说明
feign.Logger.Level修改日志级别包含四种不同的级别:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder响应结果的解析器http远程调用的结果做解析,例如解析json字符串为java对象
feign.codec.Encoder请求参数编码将请求参数编码,便于通过http请求发送
feign. Contract支持的注解格式默认是SpringMVC的注解
feign. Retryer失败重试机制请求失败的重试机制,默认是没有,不过会使用Ribbon的重试

一般情况下,默认值就能满足我们使用,如果要自定义时,只需要创建自定义的@Bean覆盖默认Bean即可。

自定义配置

配置文件方式

yml
feign:  
  client:
    config: 
      default/demoservice: # 针对某个微服务的配置  使用default为全局
        loggerLevel: FULL #  日志级别 相当于代码配置方式中的Logger Fegin的日志级别
        connectTimeout: 5000 # 相当于Request.Options  建立链接的超时时长
        readTimeout: 5000 # 相当于Request.Options   读取超时时长
        # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder Feign的错误解码器
        errorDecoder: com.example.SimpleErrorDecoder 
        # 配置重试,相当于代码配置方式中的Retryer 
        retryer: com.example.SimpleRetryer
        # 配置拦截器,相当于代码配置方式中的RequestInterceptor 
        requestInterceptors: 
        - com.example.FooRequestInterceptor 
        - com.example.BarRequestInterceptor 
        decode404: false # 配置熔断不处理404异常

feign: 
  compression: 
    request: 
      enabled: true # 开启请求压缩 
      mime-types: text/html,application/xml,application/json # 设置压缩的数据类型 
      min-request-size: 2048 # 设置触发压缩的大小下限
    response: 
      enabled: true # 开启响应压缩

java类配置

java
public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日志级别为BASIC
    }
}

如果要全局生效,将其放到启动类的@EnableFeignClients这个注解中:

java
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class)

如果是局部生效,则把它放到对应的@FeignClient这个注解中:

java
@FeignClient(value = "demoservice", configuration = DefaultFeignConfiguration .class)

3.2 配置连接池

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

•URLConnection:默认实现,不支持连接池

•Apache HttpClient :支持连接池

•OKHttp:支持连接池

以HttpClient为例:

依赖

xml
<!--httpClient的依赖 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

配置

yml
feign:
  client:
    config:
      default: # default全局的配置
        loggerLevel: BASIC # 日志级别,BASIC就是基本的请求和响应信息  日志级别尽量用basic
  httpclient:
    enabled: true # 开启feign对HttpClient的支持
    max-connections: 200 # 最大的连接数
    max-connections-per-route: 50 # 每个路径的最大连接数

3.3 feign接口抽取

feign服务提供方和服务消费方都有相关重复代码,可以将方法抽取为接口,提供方实现,消费方继承。实现代码共享。

缺点:

  • 务提供方、服务消费方紧耦合

  • 参数列表中的注解映射并不会继承,因此Controller中必须再次声明方法、参数列表、注解

抽取后问题:

可能会找不到具体的类。因为添加@EnableFeignClients注解的包可能与api的包路径不一致。

java
// 扫描包路径
@EnableFeignClients(basePackages = "cn.itcast.feign.clients")

// 指定需要加载的接口
@EnableFeignClients(clients = {UserClient.class})

四、拓展配置

4.1 编码解码转换为fastjson

fastjson的编码解码器参考fastjsonhelper工具类

java
package com.lly.core.feign;

import com.lly.core.fastjson.FastJsonHelper;
import com.lly.core.feign.config.FeignRequestInterceptor;
import feign.RequestInterceptor;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;

/**
 * @Classname FeignAutoConfiguration
 * @Description feign的公用配置
 * @Date 2021/08/14 17:06
 * @Created by wxp
 */
@Configuration
public class FeignAutoConfiguration {

//    @Bean
//    public RequestInterceptor requestInterceptor() {
//        return new FeignRequestInterceptor();
//    }

    @Bean
    public ResponseEntityDecoder feignDecoder() {
        HttpMessageConverter fastJsonConverter = FastJsonHelper.getFastJsonConverter();
        ObjectFactory<HttpMessageConverters> objectFactory = () -> new HttpMessageConverters(fastJsonConverter);
        return new ResponseEntityDecoder(new SpringDecoder(objectFactory));
    }

    @Bean
    public Encoder feignEncoder() {
        return new SpringFormEncoder(new SpringEncoder(() -> new HttpMessageConverters(FastJsonHelper.getFastJsonConverter())));
//        return new SpringEncoder(() -> new HttpMessageConverters(getFastJsonConverter()));
    }

}

4.2 请求拦截器

java
package com.lly.core.feign.config;

import com.lly.common.utils.StringUtils;
import com.lly.core.constant.CacheConstants;
import com.lly.core.utils.IpUtils;
import com.lly.core.utils.ServletUtils;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Map;

/**
 * feign 请求拦截器
 * 
 * @author ruoyi
 */
@Component
public class FeignRequestInterceptor implements RequestInterceptor
{
    @Override
    public void apply(RequestTemplate requestTemplate)
    {
        HttpServletRequest httpServletRequest = ServletUtils.getRequest();
        if (StringUtils.isNotNull(httpServletRequest))
        {
            Map<String, String> headers = ServletUtils.getHeaders(httpServletRequest);
            // 传递用户信息请求头,防止丢失
            String userId = headers.get(CacheConstants.DETAILS_USER_ID.getValue());
            if (StringUtils.isNotEmpty(userId))
            {
                requestTemplate.header(CacheConstants.DETAILS_USER_ID.getValue(), userId);
            }
            String userName = headers.get(CacheConstants.DETAILS_USERNAME.getValue());
            if (StringUtils.isNotEmpty(userName))
            {
                requestTemplate.header(CacheConstants.DETAILS_USERNAME.getValue(), userName);
            }
            String authentication = headers.get(CacheConstants.AUTHORIZATION_HEADER.getValue());
            if (StringUtils.isNotEmpty(authentication))
            {
                requestTemplate.header(CacheConstants.AUTHORIZATION_HEADER.getValue(), authentication);
            }

            // 配置客户端IP
            requestTemplate.header("X-Forwarded-For", IpUtils.getIpAddr(ServletUtils.getRequest()));
        }
    }
}

4.3 日期格式序列化

java
package com.lly.core.feign;

import com.lly.common.utils.DateUtils;
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
import org.springframework.core.convert.converter.Converter;
import org.springframework.format.FormatterRegistry;
import org.springframework.stereotype.Component;

import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * feign日期格式序列化
 * @ClassName FeignDateFormatRegister
 * @Author wuzm
 * @Date 2020/12/1 19:04
 * @Version 1.0
 */
@Component
public class FeignDateFormatRegister implements FeignFormatterRegistrar {
    @Override
    public void registerFormatters(FormatterRegistry registry) {
        registry.addConverter(Date.class, String.class, new Date2StringConverter());
    }

    private class Date2StringConverter implements Converter<Date, String> {
        @Override
        public String convert(Date source) {
            SimpleDateFormat sdf = new SimpleDateFormat(DateUtils.ISO_DATE_TIME);
            return sdf.format(source);
        }
    }

}