声明式REST客户端Feign

引言

​ Feign是一个声明式REST客户端,它能让REST调用更加简单。Feign提供了HTTP请求的模板,通过编写简单的接口和插入注解,就可以定义好HTTP请求的参数、格式、地址等信息。

而Feign则会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。 Spring Cloud对 Feign进行了封装,使其支持 Springmvc标准注解和Httpmessage Converters Feign可以与 Eureka和 Ribbon组合使用以支持负载均衡。

——《spring cloud 微服务 入门、进阶与实战》第70页

在spring cloud 中集成Feign

  • 添加Feign starter
1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  • 定义Feign客户端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
/**
* @ClassName UserRemotClient
* @Author wuzhiyong
* @Date 2020/5/2 23:16
* @Version 1.0
**/
@FeignClient(value = "eureka-client-service")
public interface UserRemoteClient {

@GetMapping("/user/hello")
String hello();

}
  • 启动类开启Feign客户端并扫包
1
2
3
4
5
6
7
8
@EnableFeignClients(basePackages = "com.study.feignInfo")
@SpringBootApplication
public class SpringcloudFeignApplication {

public static void main(String[] args) {
SpringApplication.run(SpringcloudFeignApplication.class, args);
}
}
  • 测试Feign客户端的controller
1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController {
@Autowired
private UserRemoteClient userRemoteClient;

@GetMapping("/call/hello")
public String cellHello(){
return userRemoteClient.hello();
}
}

启动项目访问 /call/hello 成功返回了 eureka-client-service 服务的数据

自定义Feign配置

日志配置

  • 增加配置类 并配置日志级别
1
2
3
4
5
6
7
8
9
10
11
12
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfiguration {

@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}
  • 在 UserRemoteClient @FeignClient注解加入配置
1
@FeignClient(value = "eureka-client-service" ,configuration = FeignConfiguration.class)
  • properties中增加
1
2
#给某个 Feign客户端类 设定日志级别。(这里配置后才能输出日志)
logging.level.com.study.feignInfo.UserRemoteClient=DEBUG

再次调用测试 /call/hello 接口,控制台日志输出:

1
2
3
4
5
6
7
8
9
[UserRemoteClient#hello] <--- HTTP/1.1 200 (33ms)
[UserRemoteClient#hello] connection: keep-alive
[UserRemoteClient#hello] content-length: 5
[UserRemoteClient#hello] content-type: text/plain;charset=UTF-8
[UserRemoteClient#hello] date: Sat, 02 May 2020 15:41:22 GMT
[UserRemoteClient#hello] keep-alive: timeout=60
[UserRemoteClient#hello]
[UserRemoteClient#hello] hello
[UserRemoteClient#hello] <--- END HTTP (5-byte body)

契约配置

spring cloud 在 Feign 基础上做了扩展,使其能够支持 spring mvc 的注解。原生的 Feign 是不支持 spring mvc 的注解的。如果想用原生的方式来定义 Feign 客户端,可通过如下配置:

1
2
3
4
5
6
7
8
@Configuration
public class FeignConfiguration {

@Bean
public Contract feignContract(){
return new feign.Contract.Default();
}
}

当这样配置后,如果前面定义客户端的代码不改变,项目重启时会报错。

1
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'userController': Unsatisfied dependency expressed through field 'userRemoteClient'; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.study.feignInfo.UserRemoteClient': FactoryBean threw exception on object creation; nested exception is java.lang.IllegalStateException: Method UserRemoteClient#hello() not annotated with HTTP method type (ex. GET, POST)

Basic 认证配置

如果服务端开启了 Basic 认证。我们可以通过下面方式配置登录名和密码

1
2
3
4
5
6
7
8
@Configuration
public class FeignConfiguration {

@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("WuZhiYong", "123456");
}
}

如果服务端使用的其它的认证方式,我们可以配置自定义请求拦截器。把相应的认证信息存入head 中

1
2
3
4
5
6
7
8
9
10
11
import feign.RequestInterceptor;
import feign.RequestTemplate;

public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
//业务逻辑 比如存放 token 。。。
requestTemplate.header("Authorization","Basic V3VaaGlZb25nOjEyMzQ1Ng==");

}
}

再配置上自定义的拦截器

1
2
3
4
5
6
7
8
@Configuration
public class FeignConfiguration {

@Bean
public FeignBasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new FeignBasicAuthRequestInterceptor();
}
}

超时时间配置

链接超时 读取超时

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
public class FeignConfiguration {

@Bean
public Request.Options options(){
/**
* @param connectTimeout value.
* @param connectTimeoutUnit with the TimeUnit for the timeout value.
* @param readTimeout value.
* @param readTimeoutUnit with the TimeUnit for the timeout value.
* @param followRedirects if the request should follow 3xx redirections.
*/
return new Request.Options(50000L, TimeUnit.MILLISECONDS, 10000,TimeUnit.MILLISECONDS,true);
}
}

客户端组件配置

Feign 中默认使用 JDK 原生的 URLConnection 发送 HTTP 请求,我们可以集成别的组件来替换它。比如Apache HttpClient.OkHttp.

配置OkHttp :

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
1
2
3
#Feign 使用 OkHttp
feign.httpclient.enabled=false
feign.okhttp.enabled=true
1
2
3
4
//关于配置可参看源码
org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration

org.springframework.cloud.openfeign.FeignAutoConfiguration.OkHttpFeignConfiguration

GZIP压缩配置

配置GZIP来压缩数据可以有效的节约网络资源,提升接口性能。

1
2
3
4
5
6
7
#配置   开启压缩
feign.compression.request.enabled=true
feign.compression.response.enabled=true
#配置压缩的类型
feign.compression.request.mime-types=text/xml,application/xml,application/json
#最小压缩值标准
feign.compression.request.min-request-size=2048

注意:只有当Feign的httpClient 不是okhttp3的时候,压缩才会生效。(参看下面源代码)

1
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration

配置编码器解码器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import feign.codec.Decoder;
import feign.codec.Encoder;

@Configuration
public class FeignConfiguration {

@Bean
public Decoder decoder(){
return new MyDecoder();//自定义实现 Decoder
}

@Bean
public Encoder encoder(){
return MyEncoder();//自定义实现 Encoder
}
}

使用自定义Feign的配置

在配置文件中自定义配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 链接超时时间
feign.client.config.feignName.connectTimeout=5000
# 读取超时时间
feign.client.config.feignName.readTimeout=5000
# 日志等级
feign.client.config.feignName.loggerLevel=full
# 重试
feign.client.config.feignName.retryer=com.example.SimpleRetryer
# 拦截器
feign.client.config.feignName.requestInterceptors[0]=com.example.FooRequestInterceptor
feign.client.config.feignName.requestInterceptors[1]=com.example.BarRequestInterceptor
# 编码器
feign.client.config.feignName.encoder=com.example.SimpleEncoder
# 解码器
feign.client.config.feignName.decoder=com.example.SimpleDecoder
# 契约
feign.client.config.feignName.contract=com.example.SimpleContract

其中 feignName 表示我们定义的 Feign 客户端里配置的服务名,即下面 @FeignClient 注解的 value 值。

1
2
3
4
5
6
7
@FeignClient(value = "eureka-client-service" ,configuration = FeignConfiguration.class)
public interface UserRemoteClient {

@GetMapping("/user/hello")
String hello();

}

继承特性

Feign 的继承特性可以让服务的接口定义单独抽出来,作为公共的依赖,以方便使用。

  • 创建一个 maven quickstart 项目 命名为 feign-api

    我们可看到 pom 中这个项目的坐标信息

    1
    2
    3
    <groupId>com.study</groupId>
    <artifactId>springcloud-feign-api</artifactId>
    <version>1.0-SNAPSHOT</version>

    加入 feign 和 spring cloud 相关依赖

    再建立一个fegin 客户端类 (完了后运行编译,打包,install 看看有没有什么问题)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.web.bind.annotation.GetMapping;

    @FeignClient(value = "feign-provider")
    public interface UserRemoteClient {

    @GetMapping("/user/name")
    String getName();

    }
  • 再建立一个spring cloud 项目 命名为 feign-provider。

    引入 Eureka 的依赖 并把服务实例名配置为 feign-provider

    引入 feign-api 项目的坐标作为依赖

    建立一个类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import org.springframework.web.bind.annotation.RestController;

    @RestController
    public class UserController implements UserRemoteClient{
    @Override
    public String getName() {
    return "my name is wuzhiyong ";
    }
    }
  • 再建立一个spring cloud 项目 命名为 feign-consumer。(如果注册到 Eureka 就引入相关依赖并配置)

    引入 feign-api 项目的坐标作为依赖

    建立一个类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @RestController
    public class UserController {

    @Resource
    UserRemoteClient userRemoteClient;

    @GetMapping("/call")
    public String call(){
    return userRemoteClient.getName();
    }

    }

    在启动类上配置 feign client 扫包

    1
    2
    3
    4
    5
    6
    7
    8
    @SpringBootApplication
    @EnableFeignClients(basePackages = "com.study")
    public class SpringcloudFeignConsumerApplication {

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

分别启动 注册中心,feign-provider ,feign-consumer 访问 feign-consumer 的 call 接口可以得到 feign-provider 返回的数据。

多参数请求构造

多参数请求构造分为 GET 请求和 POST 请求两种方式

feign-api 添加如下。

1
2
3
4
5
6
7
8
@GetMapping("/user/info")
User getUserInfo(@RequestParam("name")String name, @RequestParam("age")int age);

@GetMapping("/user/detail")
String getUserDetail(@RequestParam Map<String, Object> param);

@PostMapping("/user/add")
String addUser(@RequestBody User user);

书中说 addUser 实现类 也要加上 @RequestBody 注解,我试了下没加上也可以。然后传map 参数我这边后台接收不到,一直解析不成功。(可能是没有加第三方解析json的依赖的缘故吧)

脱离 spring cloud 使用 Feign

原生注解方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import feign.Body;
import feign.Headers;
import feign.Param;
import feign.RequestLine;

public interface UserRemoteClient {

@RequestLine(value = "GET user/hello")
String hello();

@RequestLine("GET /repos/{id}/{name}/xxx")
public String getRepos(@Param("id") int id,@Param("name") String name);

@RequestLine("POST /user/{name}")
public String addUser(@Param("name") String name);

@RequestLine("POST /user")
@Headers("Content-Type:application/json")
@Body("%7B\"username\":\"{user_name}\",\"password\":{pwd}\"%7D")
String postUser(@Param("user_name") String name,@Param("pwd") String pwd);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@RestController
public class UserController {
private UserRemoteClient userRemoteClient = Feign.builder().target(UserRemoteClient.class,"http://localhost:8081");

@GetMapping("/call/hello")
public String cellHello(){
System.out.println("8888888888888888888888888888");
return userRemoteClient.hello();
}

@GetMapping("/repos/{id}/{name}/xxx")
public String getRepos(@PathVariable("id") int id, @PathVariable("name") String name){

return userRemoteClient.getRepos(id,name);
}

@PostMapping("/user/{name}")
public String addUser(@PathVariable("name") String name){

return userRemoteClient.addUser(name);
}

@PostMapping("/user")
String postUser(@RequestParam("user_name") String name, @RequestParam("pwd") String pwd){
return userRemoteClient.postUser(name,pwd);
}
}

依赖可以用 springcloud feign 的依赖。也可以用 feign 本身的依赖

1
2
3
4
5
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>10.10.1</version>
</dependency>

构建 Feign 对象

feign 是通过 builder 模式来构建代理对象的。我们可以写一个通用的工具类来构建对象。

1
2
3
4
5
6
7
import feign.Feign;

public class RestApiCallUtils {
public static <T> T getRestClient(Class<T> apiType,String url){
return Feign.builder().target(apiType,url);
}
}

控制器中这样调用:

1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController {
private UserRemoteClient userRemoteClient = RestApiCallUtils.getRestClient(UserRemoteClient.class,"http://localhost:8081");

@GetMapping("/call/hello")
public String cellHello(){
System.out.println("8888888888888888888888888888");
return userRemoteClient.hello();
}
}

注意:在调用Feign.builder().target()方法的时候传的 url 是包括 http:// 前缀的。否则控制台会报错:values must be absolute.

其它配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class RestApiCallUtils {
public static <T> T getRestClient(Class<T> apiType,String url){
return Feign.builder()
//编码解码
.encoder( new MyEncoder())
.decoder( new MyDecoder())
//日志
.logger( new Logger.JavaLogger().appendToFile(System.getProperties().getProperty("logpath")+"/http.log")).logLevel(Logger.Level.BASIC)
//超时时间
.options( new Request.Options(10000,10000))
//拦截器
.requestInterceptor( new MyInterceptor())
//客户端组件
.client( new OkHttpClient())
//重试
.retryer(new Retryer.Default())
.target(apiType,url);
}
}

参考:

《spring cloud 微服务 入门、进阶与实战》

书中代码:

https://github.com/yinjihuan/spring-cloud/tree/master/Spring-Cloud-Book-Code-2/ch-5