Spring Cloud Feign 总结

时间:2022-07-25
本文章向大家介绍Spring Cloud Feign 总结,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

Spring Cloud中, 服务又该如何调用 ?

各个服务以HTTP接口形式暴露 , 各个服务底层以HTTP Client的方式进行互相访问。

SpringCloud开发中,Feign是最方便,最为优雅的服务调用实现方式。

Feign 是一个声明式,模板化的HTTP客户端,可以做到用HTTP请求访问远程服务就像调用本地方法一样。简单搭建步骤如下 :

1. 首先加入pom.xml

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

2. 主类上面添加注解@EnableFeignClients,该注解表示当程序启动时,会进行包扫描,默认扫描所有带@FeignClient注解的类进行处理

package name.ealen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.feign.EnableFeignClients;

/**
 * Created by EalenXie on 2018/10/12 18:24.
 */
@SpringBootApplication
@EnableFeignClients
@EnableDiscoveryClient
public class FeignOpenClientApplication {

    public static void main(String[] args) {
        SpringApplication.run(FeignOpenClientApplication.class,args);
    }
}

3. 简单配置appliation.yml 注册到Eureka Server。

server:
  port: 8090
spring:
  application:
    name: spring-cloud-feign-openClient
eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

4. 使用@FeignClient为本应用声明一个简单的能调用的客户端。为了方便,找个现成的开放接口,比如Github开放的api,GET /search/repositories。

GitHub接口文档 : https://developer.github.com/v3/search/#search-repositories

package name.ealen.client;

import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com")
public interface GitHubApiClient {

    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepositories(@RequestParam("q") String queryStr);
}

其中,@FeignClient 即是指定客户端信息注解,务必声明在接口上面,url手动指定了客户端的接口地址。

5. 为其写一个简单Controller进行一波测试 :

package name.ealen.web;

import name.ealen.client.GitHubApiClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * Created by EalenXie on 2018/10/12 18:32.
 */
@RestController
public class FeignOpenClientController {

    @Resource
    private GitHubApiClient gitHubApiClient;

    @RequestMapping("/search/github/repository")
    public String searchGithubRepositoryByName(@RequestParam("name") String repositoryName) {
        return gitHubApiClient.searchRepositories(repositoryName);
    }
}

6. 依次启动Eureka Server,和该应用。然后访问 : http://localhost:8090/search/github/repository?name=spring-cloud-dubbo

注 : 有时候在测试的时候,很容易报500 null的异常,可能是因为GitHub连接拒绝的原因,这里只是为了测试,所以可以忽略,多尝试几次即可。

关于Feign Client配置细节

1. 重点配置 @FeignClient 注解,我这里专门对源码属性做了说明 :

在上例中,我们只是简单的指定了name和url属性,如果需要专门针对该客户端进行属性按需调整,可以调整以下参数 属性值 :

package org.springframework.cloud.netflix.feign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.core.annotation.AliasFor;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface FeignClient {

	@AliasFor("name")
	String value() default "";  
	
	@Deprecated
	String serviceId() default "";
	
	@AliasFor("value")
	String name() default "";

	String qualifier() default "";

	String url() default "";

	boolean decode404() default false;

	Class<?>[] configuration() default {};
    
	Class<?> fallback() default void.class;
	
	Class<?> fallbackFactory() default void.class;

	String path() default "";
    
	boolean primary() default true;
}
name:               指定Feign Client的名称,如果项目使用了 Ribbon,name属性会作为微服务的名称,用于服务发现。
serviceId:          用serviceId做服务发现已经被废弃,所以不推荐使用该配置。
value:              指定Feign Client的serviceId,如果项目使用了 Ribbon,将使用serviceId用于服务发现,但上面可以看到serviceId做服务发现已经被废弃,所以也不推荐使用该配置。
qualifier:          为Feign Client 新增注解@Qualifier
url:                请求地址的绝对URL,或者解析的主机名
decode404:          调用该feign client发生了常见的404错误时,是否调用decoder进行解码异常信息返回,否则抛出FeignException。
fallback:           定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback 指定的类必须实现@FeignClient标记的接口。实现的法方法即对应接口的容错处理逻辑。
fallbackFactory:    工厂类,用于生成fallback 类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码。
path:               定义当前FeignClient的所有方法映射加统一前缀。
primary:            是否将此Feign代理标记为一个Primary Bean,默认为ture

例如我们要为其添加一个fallback的容错,和覆盖掉默认的configuration。

1. 首先为其添加一个fallback容错的处理类.
package name.ealen.client;

/**
 * Created by EalenXie on 2018/11/11 19:19.
 */
public class GitHubApiClientFallBack implements GitHubApiClient {

    @Override
    public String searchRepositories(String queryStr) {
        return "call github api fail";
    }
}
2. 然后为其添加一个默认配置类,为了方便了解,我这里只是写了一下默认的配置。
package name.ealen.config;

import feign.Contract;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class GitHubFeignConfiguration {
    /**
     * Feign 客户端的日志记录,默认级别为NONE
     * Logger.Level 的具体级别如下:
     * NONE:不记录任何信息
     * BASIC:仅记录请求方法、URL以及响应状态码和执行时间
     * HEADERS:除了记录 BASIC级别的信息外,还会记录请求和响应的头信息
     * FULL:记录所有请求与响应的明细,包括头信息、请求体、元数据
     */
    @Bean
    Logger.Level gitHubFeignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

注意 : 编码器,解码器,重试器,调用解析器请谨慎配置,一般来说默认就行。笔者对这些配置研究得很浅,所以没有写自定义的配置。

3. 此时我们修改我们的GitHubApiClient。指定上面两个类即可
package name.ealen.client;
import name.ealen.config.GitHubFeignConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", fallback = GitHubApiClientFallBack.class, decode404 = false, configuration = GitHubFeignConfiguration.class)
public interface GitHubApiClient {
    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    String searchRepositories(@RequestParam("q") String queryStr);
}
4. Feign也支持属性文件对上面属性的配置,比如下面的配置和GitHubFeignConfiguration的配置是等价的 :
feign:
  client:
    config:
      ##对名字为 github-client 的feign client做配置
      github-client:                                # 对应GitHubApiClient类的@FeignClient的name属性值
        decoder404: false                           # 是否解码404
        loggerLevel: full                           # 日志记录级别

2. 重点配置 @EnableFeignClients 注解,我这里专门对源码属性做了说明 :

package org.springframework.cloud.netflix.feign;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.context.annotation.Import;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {

	String[] value() default {};

	String[] basePackages() default {};

	Class<?>[] basePackageClasses() default {};

	Class<?>[] defaultConfiguration() default {};

	Class<?>[] clients() default {};
}
value:                  等价于basePackages属性,更简洁的方式
basePackages:           指定多个包名进行扫描
basePackageClasses:     指定多个类或接口的class,扫描时会在这些指定的类和接口所属的包进行扫描。
defaultConfiguration:   为所有的Feign Client设置默认配置类
clients:                指定用@FeignClient注释的类列表。如果该项配置不为空,则不会进行类路径扫描。

同样的,为所有Feign Client 也支持文件属性的配置,如下 :

feign:
  client:
    config:                                         
    # 默认为所有的feign client做配置(注意和上例github-client是同级的)
      default:                                      
        connectTimeout: 5000                        # 连接超时时间
        readTimeout: 5000                           # 读超时时间设置  

注 : 如果通过Java代码进行了配置,又通过配置文件进行了配置,则配置文件的中的Feign配置会覆盖Java代码的配置。

但也可以设置feign.client.defalult-to-properties=false,禁用掉feign配置文件的方式让Java配置生效。

3. Feign 请求和响应开启GZIP压缩,提高通讯效率

1. 配置如下:
feign:
  compression:
    request:
      enable: true  #配置请求支持GZIP压缩,默认为false
      mime-types: text/xml, application/xml, application/json  #配置压缩支持的Mime Type
      min-request-size: 2048 #配置压缩数据大小的上下限
    reponse:
      enable: true #配置响应支持GZIP压缩,默认为false

对应配置源码可以看看 :

2. 由于开启GZIP压缩之后,Feign之间的调用通过二进制协议进行传输,返回的值需要修改为ResponseEntity<byte[]>才可以正常显示,否则会导致服务之间的调用结果乱码。
3. 例如此时修改GitHubApiClient类的Feign client的配置 :
import name.ealen.config.GitHubFeignConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
/**
 * Created by EalenXie on 2019/1/9 11:28.
 */
@FeignClient(name = "github-client", url = "https://api.github.com", path = "", serviceId = "", qualifier = "", decode404 = false, configuration = GitHubFeignConfiguration.class)
public interface GitHubApiClient {

//    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
//    Object searchRepositories(@RequestParam("q") String queryStr);

    @RequestMapping(value = "/search/repositories", method = RequestMethod.GET)
    ResponseEntity<byte[]> searchRepositories(@RequestParam("q") String queryStr);
}

4. Feign超时配置

feign的调用分为两层。ribbon和hystrix(默认集成),默认情况下,hystrix是关闭的,所以当ribbon发生超时异常时,可以如下配置调整ribbon超时时间 :

#ribbon的超时时间
ribbon:
  ReadTimeout: 60000           # 请求处理的超时时间
  ConnectTimeout: 30000        # 请求连接的超时时间

至于为什么这个配置会生效,我们可以大概看一下源码里面相关 键值对 的描述 :

DefaultClientConfigImpl中 有许多的属性键配置 :

CommonClientConfigKey :

AbstractRibbonCommand 中的 ribbon 和 hystrix都用到的 getRibbonTimeout()方法 :

默认情况下,feign中的hystrix是关闭的。

如果开启了hystrix。此时的ribbon的超时时间和Hystrix的超时时间的结合就是Feign的超时时间,当hystrix发生了超时异常时,可以如下配置调整hystrix的超时时间 :

feign:
  hystrix:
    enable: true
hystrix:
  shareSecurityContext: true    # 设置这个值会自动配置一个Hystrix并发策略会把securityContext从主线程传输到你使用的Hystrix command
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMillisecond: 10000   # hystrix超时时间调整 默认为1s
      circuitBreaker:
        sleepWindowInMilliseconds: 10000     # 短路多久以后开始尝试是否恢复,默认5s
        forceClosed: false # 是否允许熔断器忽略错误,默认false, 不开启

关于HystrixCommandProperties类的以上配置说明,详细可以参阅 : https://www.jianshu.com/p/b9af028efebb

注 : 当开启了Ribbon之后,可能会出现首次调用失败的情况。

原因 : 因为hystrix的默认超时时间是1s,而feign首次的请求都会比较慢,如果feign的响应时间(ribbon响应时间)大于了1s,就会出现调用失败的问题。

解决方法 :

1. 将Hystrix的超时时间尽量修改得长一点。(有时候feign进行文件上传的时候,如果时间太短,可能文件还没有上传完就超时异常了,这个配置很有必要)
2. 禁用Hystirx的超时时间 : hystrix.command.default.execution.timeout.enabled=false
3. Feign直接禁用Hystrix(不推荐) : feign.hystrix.enabled=false

Feign 的HTTP请求相关

1. Feign 默认的请求 Client 替换

feign在默认情况下使用JDK原生的URLConnection 发送HTTP请求。(没有连接池,保持长连接)

1. 使用HTTP Client替换默认的Feign Client

引入pom.xml :

<!--Apache HttpClient 替换Feign原生的httpclient-->
<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
</dependency>

配置application.yml支持httpclient :

feign: 
  httpclient:
    enable: true
2. 使用okhttp替换Feign默认的Client

引入pom.xml :

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>

配置application.yml支持okhttp :

feign: 
  httpclient:
    enable: false
  okhttp:
    enable: true

配置okhttp :

import feign.Feign;
import okhttp3.ConnectionPool;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.netflix.feign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkHttpConfig {
    @Bean
    public okhttp3.OkHttpClient okHttpClient() {
        return new okhttp3.OkHttpClient.Builder()
                .connectTimeout(60, TimeUnit.SECONDS)   //设置连接超时
                .readTimeout(60, TimeUnit.SECONDS)      //设置读超时
                .writeTimeout(60, TimeUnit.SECONDS)     //设置写超时
                .retryOnConnectionFailure(true)                 //是否自动重连
                .connectionPool(new ConnectionPool())           //构建OkHttpClient对象
                .build();
    }
}

2. Feign的Get多参数传递

Feign 默认不支持GET方法直接绑定POJO的,目前解决方式如下 :

1. 把POJO拆散成一个个单独的属性放在方法参数里面;
2. 把方法的参数变成Map传递;
3. GET传递@RequestBody。(此方式违反了Restful规范,而且我们一般不会这样写)

《重新定义Spring Cloud实战》一书中介绍了一种最佳实践方式,通过Feign的拦截器的方式进行处理。实现原理是通过Feign的RequestInterceptor中的apply方法,统一拦截转换处理Feign中的GET方法多参数。处理如下 :

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;

@Component
public class FeignRequestInterceptor implements RequestInterceptor {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void apply(RequestTemplate template) {
        //feign 不支持GET方法传POJO,json body 转query
        if (template.method().equals("GET") && template.body() != null) {
            try {
                JsonNode jsonNode = objectMapper.readTree(template.body());
                template.body(null);
                Map<String, Collection<String>> queries = new HashMap<>();
                buildQuery(jsonNode, "", queries);
                template.queries(queries);
            } catch (IOException e) {
                //提示:根据实践项目情况处理此处异常,这里不做扩展。
                e.printStackTrace();
            }
        }
    }

    private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
        if (!jsonNode.isContainerNode()) {   // 叶子节点
            if (jsonNode.isNull()) return;
            Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
            values.add(jsonNode.asText());
            return;
        }
        if (jsonNode.isArray()) {   // 数组节点
            Iterator<JsonNode> it = jsonNode.elements();
            while (it.hasNext()) {
                buildQuery(it.next(), path, queries);
            }
        } else {
            Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
            while (it.hasNext()) {
                Map.Entry<String, JsonNode> entry = it.next();
                if (StringUtils.hasText(path))
                    buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
                else   // 根节点
                    buildQuery(entry.getValue(), entry.getKey(), queries);
            }
        }
    }
}

3. feign的文件上传

1. 首先我们编写一个简单文件上传服务的应用,并将其注册到Eureka Server上面

简单配置一下,application的name为feign-file-upload-application。为其写一个上传的接口 :

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;

@RestController
public class FeignUploadController {
    private static final Logger log = LoggerFactory.getLogger(FeignUploadController.class);
    @PostMapping(value = "/server/uploadFile", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUploadServer(MultipartFile file) throws Exception {
        log.info("upload file name : {}", file.getName());
        //上传文件放到 /usr/temp/uploadFile/ 目录下
        file.transferTo(new File("/usr/temp/uploadFile/" + file.getName()));
        return file.getOriginalFilename();
    }
}
2. 编写一个要使用上传功能的feign 客户端 :

feign客户端应用还需要加入依赖,pom.xml :

<!-- Feign文件上传依赖-->
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form</artifactId>
    <version>3.0.3</version>
</dependency>
<dependency>
    <groupId>io.github.openfeign.form</groupId>
    <artifactId>feign-form-spring</artifactId>
    <version>3.0.3</version>
</dependency>

客户端指定接口信息 :

import name.ealen.config.FeignMultipartSupportConfiguration;
import org.springframework.cloud.netflix.feign.FeignClient;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.multipart.MultipartFile;

@FeignClient(value = "feign-file-upload-application", configuration = FeignMultipartSupportConfiguration.class)
public interface FileUploadFeignService {
    /***
     * 1.produces,consumes必填
     * 2.注意区分@RequestPart和RequestParam,不要将
     * : @RequestPart(value = "file") 写成@RequestParam(value = "file")
     */
    @RequestMapping(method = RequestMethod.POST, value = "/uploadFile/server", produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
    public String fileUpload(@RequestPart(value = "file") MultipartFile file);
}
import feign.form.spring.SpringFormEncoder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.context.annotation.Scope;
import feign.codec.Encoder;
/**
 * Feign文件上传Configuration
 */
@Configuration
public class FeignMultipartSupportConfiguration {
    @Bean
    @Primary
    @Scope("prototype")
    public Encoder multipartFormEncoder() {
        return new SpringFormEncoder();
    }
}

注意 : 文件上传功能的feign client 与其他的feign client 配置要分开,因为用的是不同的Encoder和处理机制,以免互相干扰,导致请求抛Encoder不支持的异常。

4. feign的调用传递headers里面的信息内容

默认情况下,当通过Feign调用其他的服务时,Feign是不会带上当前请求的headers信息的。

如果我们需要调用其他服务进行鉴权的时候,可能会需要从headers中获取鉴权信息。则可以通过实现Feign的拦截RequestInterceptor接口,进行获取headers。然后手动配置到feign请求的headers中去。

import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

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

@Component
public class FeignHeadersInterceptor implements RequestInterceptor {
    @Override
    public void apply(RequestTemplate template) {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();
        Enumeration<String> headerNames = request.getHeaderNames();
        if (headerNames != null) {
            while (headerNames.hasMoreElements()) {
                String keys = headerNames.nextElement();
                String values = request.getHeader(keys);
                template.header(keys, values);
            }
        }
    }
}