Spring Cloud-Feign 声明式REST客户端

Spring Cloud-Feign 声明式REST客户端

第一节 简介

1.1 Java如何进行接口调用

Java中通常如何进行接口调用?

  • HttpClient:Apache Jakarta Common下的子项目,用来提供高效的、最新的、功能丰富的支持Http协议的客户端编程工具包,并且支持Http最新版本和建议。相比传统JDK自带的URLConnection,提升了易用性和灵活性,使客户端发送Http请求变得更容易,提高了开发的效率。
  • Okhttp:处理网络请求的开源项目,安卓端最火的轻量级框架,由Square公司贡献,用于替代HttpURLConnection和Apache HttpClient。拥有简洁的API、高效的性能,并支持多种协议(HTTP/2和SPDY)。
  • HttpURLConnection:Java的标准类,继承自URLConnection,可用于向指定的网站发送GET请求和POST请求。使用比较复杂,不如HttpClient易用。
  • RestTemplate:Spring提供的用于访问Rest服务的客户端,提供了多种便捷访问远程HTTP服务的方法,能够大大提高客户端的编写效率。

1.2 Feign

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

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


第二节 实战练习

2.1 在Spring Cloud中集成Feign

  首先引入依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

  启动类上添加@EnableFeignClients注解,若Feign接口定义和启动类不在同一个包名下,需要额外指定扫描的包名@EnableFeignClients(basePackages = “xxx.xxx.xxx”)。

1
2
3
4
5
6
7
8
@EnableDiscoveryClient
@EnableFeignClients(basePackages = "xxx.xxx.xxx")
@SpringBootApplication
public class FeignDemoApplication {
public static void main(String[] args) {
SpringApplication.run(FeignDemoApplication.class, args);
}
}

2.2 使用Feign调用接口

  可以通过接口定义一个Feign客户端。

1
2
3
4
5
6
//定义一个Feign客户端
@FeignClient(value = "eureka-client-user-service")//标识当前是一个Feign客户端,value标识要调用的服务接口
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}

  另一种做法是将接口单独抽出来定义,然后在Controller中实现接口。在客户端也实现了接口,从而达到接口共用的目的。此处没有共用,而是单独创建一个API Client的公共项目,基于约定的模式,每写一个接口就要对应写一个调用的Client,后面打成公共的jar,这样无论是哪个项目需要调用接口,只要引入公共的SDK jar即可。

  定义后可以直接通过注入UserRemoteClient来调用,和本地方法无差别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RestController
public class DemoController {
@Autowired
private UserRemoteClient userRemoteClient;

@GetMapping("/callHello")
public String callHello() {
// return restTemplate.getForObject("http://localhost:8083/house/hello",String.class);
// String result = restTemplate.getForObject("http://eureka-client-user-service/user/hello",String.class);
String result = userRemoteClient.hello();
System.out.println(" 调用结果:" + result);
return result;
}
}

  可以发现我们调用服务接口越来越简单了,从最开始要指定地址,到后面通过Eureka注册的服务名称调用,再到现在直接通过定义接口来调用。


第三节 自定义Feign配置

3.1 日志配置

  当遇到BUG(如接口调用失败、参数异常等)需要定位问题时,或是检查一下调用性能,可能想要看一下Feign的输出信息,这时需要配置Feign日志。

  首先定义一个配置类。

1
2
3
4
5
6
7
@Configuration
public class FeignConfiguration {
@Bean
Logger.Level feignLoggerLevel() {
return Logger.Level.FULL;
}
}

  查看源码中日志等级如下。

1
2
3
4
5
6
7
8
9
public static enum Level {
NONE, //不输出日志
BASIC, //只输出请求方法的URL和响应的状态码以及接口执行的时间
HEADERS, //将BASIC信息和请求头信息输出
FULL; //输出完整信息

private Level() {
}
}

  接下来需要在@FeignClient中指定使用的配置类。

1
2
3
4
5
6
@FeignClient(value = "eureka-client-user-service", configuration = FeignConfiguration.class)
public interface UserRemoteClient {
@GetMapping("/user/hello")
String hello();
}

  然后还需要在配置文件中指定Client的日志级别才可以正常输出日志。

1
2
# Feign日志级别:格式-logging.level.client地址=级别
logging.level.com.xxx.feigndemo.UserRemoteClient=DEBUG

  启动并访问接口,结果如下。

1
2
3
4
5
6
7
2020-03-26 17:42:17.274 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient       : [UserRemoteClient#hello] <--- HTTP/1.1 200 (431ms)
2020-03-26 17:42:17.274 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello] content-length: 5
2020-03-26 17:42:17.274 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello] content-type: text/plain;charset=UTF-8
2020-03-26 17:42:17.274 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello] date: Thu, 26 Mar 2020 09:42:17 GMT
2020-03-26 17:42:17.274 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello]
2020-03-26 17:42:17.276 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello] hello
2020-03-26 17:42:17.276 DEBUG 11976 --- [nio-8084-exec-1] com.xxx.feigndemo.UserRemoteClient : [UserRemoteClient#hello] <--- END HTTP (5-byte body)

3.2 契约配置

  Spring Cloud在Feign基础上进行了扩展,使Feign可以支持Spring MVC的注解来调用(原生并不支持)。如果想要使用原生的注解来定义客户端也可以,通过配置契约来实现,Spring Cloud默认是SpringMvcContract。

1
2
3
4
5
6
7
@Configuration
public class FeignConfiguration {
@Bean
public Contract feignContract() {
return new feign.Contract.Default();
}
}

  增加此配置后,@FeignClient注解就无法使用了。

3.3 Basic认证配置

  通常我们调用的接口是有权限控制的,我们需要提供认证的信息,通过参数或请求头传递,比如Basic认证。Feign可以直接配置Basic认证。

1
2
3
4
5
6
7
@Configuration
public class FeignConfiguration {
@Bean
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new BasicAuthRequestInterceptor("user","password");
}
}

  当然也可以自定义认证方式,其实就是自定义一个请求的拦截器。在请求之前先进行认证操作,然后往请求头中设置认证后的信息。通过实现RequestInterceptor接口来自定义认证。

1
2
3
4
5
6
7
8
9
10
public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {

public FeignBasicAuthRequestInterceptor() {
}

@Override
public void apply(RequestTemplate template) {
//业务逻辑
}
}

  然后修改配置类,引用我们的自定义认证。

1
2
3
4
5
6
7
@Configuration
public class FeignConfiguration {
@Bean
public FeignBasicAuthRequestInterceptor basicAuthRequestInterceptor() {
return new FeignBasicAuthRequestInterceptor();
}
}

3.4 超时时间配置

  通过Options可以设置连接超时时间和读取超时时间,分别对应第一和第二个参数,默认值分别为10 * 1000和60 * 1000。

1
2
3
4
@Bean
public Request.Options options(){
return new Request.Options(5000, 10000);
}

3.5 客户端组件配置

  Feign默认使用JDK原生的URLConnection发送HTTP请求,当然可以集成Apache HttpClient或OkHttp来替换。

  首先引入OkHttp依赖。

1
2
3
4
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>

  然后修改配置,禁用Feign的HttpClient,启用OkHttp。

1
2
3
# 禁用Feign的HttpClient,启用OkHttp
feign.httpclient.enabled=false
feign.okhttp.enabled=true

3.6 GZIP压缩配置

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

1
2
3
4
5
6
# 开启GZIP压缩配置
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

  但对于okhttp3压缩设置是无效的,可以参考源码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

@Configuration
@EnableConfigurationProperties({FeignClientEncodingProperties.class})
@ConditionalOnClass({Feign.class})
@ConditionalOnBean({Client.class})
@ConditionalOnProperty(
value = {"feign.compression.response.enabled"},
matchIfMissing = false
)
@ConditionalOnMissingBean(
type = {"okhttp3.OkHttpClient"}
) //只有不包含okhttp3.OkHttpClient时才启用
@AutoConfigureAfter({FeignAutoConfiguration.class})
public class FeignAcceptGzipEncodingAutoConfiguration {
public FeignAcceptGzipEncodingAutoConfiguration() {
}

@Bean
public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
return new FeignAcceptGzipEncodingInterceptor(properties);
}
}

3.7 编码器解码器配置

  Feign提供了自定义的编码解码器设置,也提供了多种编码器的实现,如Gson、Jaxb、Jackson。如果我们想传输XML格式的数据,可以自定义XML编码解码器来实现,或者使用官方提供的Jaxb。

  配置编码解码器只需在配置类中注册Decoder和Encoder这两个类。

1
2
3
4
5
6
7
8
9
10
11
// 设置解码器
@Bean
public Decoder decoder() {
return new MyDecoder();
}

// 设置编码器
@Bean
public Encoder encoder() {
return new MyEncoder();
}

  分别实现feign.codec.Decoder和feign.codec.Encoder接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyDecoder implements Decoder {
@Override
public Object decode(Response response, Type type) throws IOException, FeignException {
return null;
}
}

public class MyEncoder implements Encoder {
@Override
public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException {

}
}

3.8 使用配置自定义Feign的配置

  上述的一些配置类也可以直接在配置文件中进行配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 连接超时时间
feign.client.config.feignName.connect-timeout=5000
# 读取超时时间
feign.client.config.feignName.read-timeout=5000
# 日志等级
feign.client.config.feignName.logger-level=full
# 重试
feign.client.config.feignName.retryer=com.xxx.feigndemo.MyRetryer
# 拦截器
feign.client.config.feignName.request-interceptors[0]=com.xxx.feigndemo.FooRequestInterceptor
feign.client.config.feignName.request-interceptors[1]=com.xxx.feigndemo.BarRequestInterceptor
# 编码器
feign.client.config.feignName.encoder=com.xxx.feigndemo.MyEncoder
# 解码器
feign.client.config.feignName.decoder=com.xxx.feigndemo.MyDecoder
# 契约
feign.client.config.feignName.contract=com.xxx.feigndemo.MyContract

3.9 继承特性

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

  创建一个新的Maven项目feign-inherit-api,用来存放API接口的定义,增加如下依赖。

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

  定义接口,指定服务名称。

1
2
3
4
5
@FeignClient(value = "feign-inherit-provide")
public interface UserRemoteClient {
@GetMapping("/user/name")
String getName();
}

  创建一个新的Maven项目feign-inherit-provide作为服务提供者,引入feign-inherit-api。

1
2
3
4
5
<dependency>
<groupId>com.xxx</groupId>
<artifactId>feign-inherit-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

  实现UserRemoteClient接口。

1
2
3
4
5
6
7
@RestController
public class DemoController implements UserRemoteClient {
@Override
public String getName() {
return "Tom";
}
}

  创建一个新的Maven项目feign-inherit-provide作为服务消费者,引入feign-inherit-api。

1
2
3
4
5
<dependency>
<groupId>com.xxx</groupId>
<artifactId>feign-inherit-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>

  通过feign-inherit-api调用/user/name接口。

1
2
3
4
5
6
7
8
9
10
11
12
@RestController
public class DemoController {
@Autowired
private UserRemoteClient userRemoteClient;

@GetMapping("/call")
public String callName() {
String result = userRemoteClient.getName();
System.out.println(" 调用结果:" + result);
return result;
}
}

  通过将接口的定义单独抽离出来,由服务提供者实现接口,而服务消费者只需要引用定义好的接口调用即可。

3.10 多参数请求构造

  多参数请求构造分为Get请求Post请求两种方式。

  Get请求可以用固定参数的方式,或Map传参的方式(Map无法约束参数)。

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

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

  Post请求可以定义一个参数类,通过@RequestBody实现。

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

第四节 脱离Spring Cloud使用Feign

4.1 原生注解方式

  原生的Feign不支持Spring MVC注解,需要用@RequestLine注解。

1
2
3
4
5
6
7
8
9
10
11
12
// GET请求
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Parm("owner") String owner,
@Parm("repo") String repo);
}

// POST请求
interface Bank {
@RequestLine("POST /account/{id}")
Account getAccountInfo(@Parm("id") String id);
}

  需要使用哪种请求方式,直接在注解中写明即可,然后跟上请求的URL,Path参数可以通过大括号包起来,参数前加上@Parm进行配对。

  通过@Headers可以添加请求头信息,通过@Body直接添加请求体信息。

1
2
3
4
5
6
7
8
9
@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Parm("key") String key);

@RequestLine("POST /")
@Headers("Content-Type: application/json")
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"password\"%7D")
void json(@Parm("user_name") String user_name,
@Parm("password") String password);

4.2 构建Feign对象

  Feign通过Builder模式来构建接口代理对象,可以设置解码编码器、设置日志等信息。我们可以实现一个工具类来构建对象,只要传入一个定义好的接口和URL,就可以获取这个接口的代理对象,通过代理对象调用接口中的方法即可实现远程调用。

  首先引入依赖。

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

  构建工具类。

1
2
3
4
5
6
7
8
9
10
11
public class RestApiCallUtils {
/**
* 获取 API 接口代理对象
*@Param apiType 接口类
*@Param uri API地址
*@return
*/
public static <T> T getRestCkuebt(Class<T> apiType, String uri) {
return Feign.builder().target(apiType, uri);
}
}

  通过原生的调用方式重写callHello接口,定义一个调用的接口。

1
2
3
4
interface HelloRemote {
@RequestLine("GET /user/hello")
String hello();
}

  改写callHello接口。

1
2
3
4
5
6
7
@GetMapping("/callHello")
public String callHello() {
HelloRemote helloRemote = Feign.builder().target(HelloRemote.class, "http://localhost:8081");
String result = helloRemote.hello();
System.out.println(" 调用结果:" + result);
return result;
}

4.3 其他配置

  Feign的其他配置也是通过Feign.builder()之后的对象进行设置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 编码解码器设置
Feign.builder().encoder(new JacksonEncoder()).decoder(new JacksonDecoder());
// 日志设置
Feign.builder().logger(new Logger.JavaLogger()
.appendToFile(System.getProperty("logpath") + "/http.log"))
.logLevel(Logger.Level.FULL);
// 超时时间设置
Feign.builder().options(new Options(1000, 1000));
// 请求拦截器设置
Feign.builder().requestInterceptor(requestInterceptor);
// 客户端组件设置
Feign.builder().client(client);
// 重试机制设置
Feign.builder().retryer(retryer);

参考博客和文章书籍等:

《Spring Cloud微服务-入门、实战和进阶》

因博客主等未标明不可引用,若部分内容涉及侵权请及时告知,我会尽快修改和删除相关内容