开启springcloud全家桶4:极简API调用方式 Spring Cloud Feign 总结
Posted 黄小斜
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了开启springcloud全家桶4:极简API调用方式 Spring Cloud Feign 总结相关的知识,希望对你有一定的参考价值。
变得更简单。使用Feign,只需要创建一个接口并注解。它具有可插拔的注解特性,可使用Feign 注解和JAX-RS注解。Feign支持可插拔的编码器和解码器。Feign默认集成了Ribbon,并和Eureka结合,默认实现了负载均衡的效果。
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);
}
}
}
}
以上是关于开启springcloud全家桶4:极简API调用方式 Spring Cloud Feign 总结的主要内容,如果未能解决你的问题,请参考以下文章
开启springcloud全家桶9:springcloud的分布式链路追踪利器 Sleuth
开启springcloud全家桶6:Spring Cloud 服务网关 Zuul 快速入门
开启springcloud全家桶5:探索负载均衡组件 Ribbon实现与原理
开启springcloud全家桶:springcloud常见面试题