1. 使用Feign调用服务接口

在Java中,进行项目开发基本都是模块划分,划分模块之后,就需要进行接口的调用。那么该如何进行接口的调用呢??

  • HttpClient

    HttpClient提供了高效的、最新的、功能丰富的支持Http协议的客户端编程工具包。

    HttpClient相比传统JDK自带的URLConnection,提升了易用性和灵活性,使客户端发送HTTP请求变得容易,提高了开发的效率。

  • OkHttp

    一个处理网络请求的开源项目,是安卓端最火的轻量级框架,OkHttp拥有简洁的API、高效的性能,并支持多种协议(Http和SPDY)。

  • HttpURLConnection

    HttpURLConnection 是Java的标准类,它继承自URLConnection,可用于向网站发送GET请求、POST请求。HttpURLConnection使用比较复杂,不像HttpClient那样容易使用。

  • RestTemplate

    RestTemplate 是Spring 提供的用于访问Rest 服务的客户端,RestTemplate提供了多种便捷访问远程Http 服务的方法,能够大大提高客户端的编写效率。

上面介绍了最常见的几种接口调用方法,下面要介绍的Feign更加简单、方便。

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

Feign会完全代理HTTP请求,我们只需要像调用方法一样调用它就可以完成服务请求及相关处理。

并且,Feign可以与Eureka 和Ribbon组合使用以支持负载均衡。

1.1 在 Spring Cloud 中集成Feign

下面通过一个简单的案例去描述Feign接口调用的一个简单过程:

需求:

将模块feign-interface和feign-invoke注册到注册中心,在feign-invoke模块中调用feign-interface中的接口。

首先需要创建一个模块eureka-server作为注册中心,并配置上security的安全框架。因为在我博客上已经写有Eureka服务注册中心的相关搭建,这里就简单将位置列出来即可,若感兴趣可以进入博客详细学习:小曾博客

eureka-server:

pom.xml:

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
28
29
30
31
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
</dependencies>

application.properties配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 服务器端口
server.port=8010

# 服务应用名称
spring.application.name=eureka-server

# 此应用为注册中心,false:不向注册中心注册自己。
eureka.client.register-with-eureka = false

# 注册中心职责是维护服务实例,false:不检索服务。
eureka.client.fetch-registry=false

eureka.client.service-url.defaultZone=http://localhost:8010/eureka/

# spring-security的配置信息:
spring.security.user.name=xiaozeng
spring.security.user.password=123456

#eureka.instance.prefer-ip-address=true

# eureka.instance.status-page-url=www.baidu.com

eureka.server.enableSelfPreservation=false

因为会集成Spring Security,所以在配置文件中必须添加下面两行代码,不添加的话会报401的权限错误:

1
2
3
# spring-security的配置信息:
spring.security.user.name=xiaozeng
spring.security.user.password=123456

主启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.self.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer
@SpringBootApplication
public class EurekaServerApplication {

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

下面是Security的相关配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.self.eurekaserver.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
//关闭csrf
http.csrf().disable();
//支持httpBasic
http.authorizeRequests().anyRequest().authenticated().and().httpBasic();
}
}

至此,Eureka-server注册中心就搭建成功了,启动项目,访问http://localhost:8010即可登录Eureka的Web页面。

下面就是进入feign的正式学习:

在Spring Cloud 中集成Feign 的步骤相当简单,首先还是创建项目feign-interface,并加入相关的依赖,如下:

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
28
29
30
31
32
33
34
35
36
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>

编写application.propeties配置文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 服务器端口
server.port=8088

# 服务应用名称
spring.application.name=feign-interface


# Eureka注册中心地址
eureka.client.service-url.defaultZone=http://xiaozeng:123456@localhost:8010/eureka/

# 使用IP注册
eureka.instance.prefer-ip-address=true

# 定义实例ID格式
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}

eureka.instance.status-page-url=http://www.baidu.com

spring.security.user.name=xiaozeng
spring.security.user.password=123456

启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.self.feign;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;

@SpringBootApplication(exclude = {org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class})
@EnableFeignClients
public class FeignInterfaceApplication {

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

在启动类上添加@EnableFeignClients注解,如果你的Feign接口定义跟你启动类不在同一包名下,还需要制定扫描的包名@EnableFeignClients(basePackages=”com.self.xxxx”)。在上面的@SpringBootApplication注解中排除了org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class ,是为了解决Security的一个权限问题,如果不排除这个自动配置,将会报401的权限问题。

因为是将这个模块注册到Eureka注册中心,然后客户端调用带模块的接口返回数据,所以必须在此模块中创建相应的接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.self.feign.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

@GetMapping("/user/hello")
public String hello(){

return "Hello Welcome to Study Feign~~~";
}
}

到了这里,启动Eureka注册中心,和feign-interface就可以将模块注册进注册中心。

上面的步骤就是创建一个注册中心,并且将服务接口提供者已经注册到了注册中心,那么,既然已经有了服务的提供者,那么必须要有人去使用把??所以下面咱就来创建feign-invoke模块调用服务提供者提供的接口:

pom.xml :

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
28
29
30
31
32
33
34
35
36
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.SR2</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>2.2.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
<version>2.2.0.RELEASE</version>
</dependency>
</dependencies>

application.properties配置文件 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 服务器端口
server.port=8099

# 服务应用名称
spring.application.name=feign-invoke


# Eureka注册中心地址
eureka.client.service-url.defaultZone=http://xiaozeng:123456@localhost:8010/eureka/

# 使用IP注册
eureka.instance.prefer-ip-address=true

# 定义实例ID格式
eureka.instance.instance-id=${spring.application.name}:${spring.cloud.client.ip-address}:${server.port}

eureka.instance.status-page-url=http://www.baidu.com

spring.security.user.name=xiaozeng
spring.security.user.password=123456

启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.self.feign;

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

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class FeignApplication {

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

当我们写到这,服务调用者的基本配置已经完成。启动项目就可以将模块注册进注册中心:

如果需要调用服务提供者提供的接口,那么在调用端这里必须要获取提供者暴露出来的接口信息,因此需要在调用端创建一个DemoRemoteClient接口,标识一下服务提供者暴露的接口路径等信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.self.feign.client;

import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "feign-interface")
@Component
public interface DemoRemoteClient {

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

最后我们可以测试一下,使用调用端的DemoRemoteClient对象调用服务提供者提供的hello接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.self.feign.controller;

import com.self.feign.client.DemoRemoteClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class TestController {

@Autowired
private DemoRemoteClient demoRemoteClient;

@GetMapping("/callHello")
public String callHello() {

String result = demoRemoteClient.hello();
System.out.println("远程调用结果:" + result);
return result;
}
}

当模块都启动后,访问http://localhost:8099/callHello时,由于配置了Spring Security的原因,所以需要将配置文件的用户名密码填写登录才可以访问:

登录后可以发现页面返回数据:

控制台也会有对应的输出:

【注意:在客户端(服务调用者)接口上加@FeignClient注解,就相当于标识这是一个Feign客户端,value的属性就是对应的服务名称,也就是你需要调用那个服务的接口。】

定义方法时直接复制接口的定义即可,这样做的好处就是,每写一个接口就要对应写一个调用的Client,后面打成公共的jar,这样无论是哪个项目需要调用接口,只要引入公共的接口SDK jar 即可,不用重新定义一遍了。

2. Feign自定义配置

Feign提供了很多的的扩展机制,让用户可以更加灵活的使用,下面就一起来学习Feign的一些自定义配置。

2.1 日志配置

在进行接口调用时,比如接口调用失败,参数没收到等问题,或者想看看调用的性能,就需要配置Feign的日志,让Feign把请求信息输出出来。

首先定义一个配置类,获取日志级别,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.self.feign.config;

import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}

Feign 的Level日志级别有4种,分别是:

  • NONE:不输出日志。
  • BASIC:只输出请求方法的URL和响应的状态码以及接口执行的时间。
  • HEADERS:将BASIC信息和请求头信息输出。
  • FULL:输出完整的请求信息。

配置类建好后,我们需要在Feign Client(服务调用方)中的@FeignClient注解中指定使用的配置类,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.self.feign.client;

import com.self.feign.config.FeignConfig;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(value = "feign-interface",configuration = FeignConfig.class)
@Component
public interface DemoRemoteClient {

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

在配置文件中执行Client的日志级别才能正确的输出日志,格式是”logging.level.client类地址=级别”。

1
logging.level.com.self.feign.client.DemoRemoteClient=DEBUG

最后,通过Feign的客户端8099端口去调用/callHello,就可以看到控制台输出的调用信息,如下图:

2.2 契约配置

其实,原生的Feign是不支持Spring MVC注解的,只是因为Spring Cloud 在Feign的基础上做了扩展,才使Feign支持Spring MVC的注解来调用。

如果你想在Spring Cloud中使用原生的注解方式来定义客户端也是可以的,通过配置契约来改变这个配置,Spring Cloud中默认的是SpringMvcContract,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.self.feign.config;

import feign.Contract;
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FeignConfig {

@Bean
public Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}

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

当配置了默认契约之后,之前定义的Client就用不了了,因为之前上面的注解都是Spring MVC的注解。

效果如下图:

2.3 Basic 认证配置

通常我们调用的接口都是有权限控制的,很多时候可能认证的值是通过参数去传递的,还有就是通过请求头去传递认证信息,比如Basic认证方式。在Feign中我们可以直接配置Basic认证,代码如下:

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

或者你可以自定义属于自己的认证方式,其实就是自定义一个请求拦截器。在请求之前做认证操作,然后往请求头中设置认证之后的信息。通过实现RequestIntercepter接口来自定义认证方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.self.feign.interceptor;

import feign.RequestInterceptor;
import feign.RequestTemplate;

public class FeignBasicAuthRequestInterceptor implements RequestInterceptor {

public FeignBasicAuthRequestInterceptor(){

}

@Override
public void apply(RequestTemplate requestTemplate) {
System.out.println("进入请求拦截,用户名密码认证~~~");
}
}

然后将配置改成我们自定义的就可以了,这样当Feign去请求接口的时候,每次请求之前都会进入FeignBasicAuthRequestInterceptor的apply方法中,在在里面就可以做属于你的逻辑了,代码如下:

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

尝试去访问8099下的callHello接口后,控制台会输出以下结果:

2.4 超时时间配置

通过Options可以配置连接超时时间和读取超时时间(代码如下),Options的第一个参数是连接超时时间(ms),默认值是10 × 1000;第二个是读取超时时间(ms),默认值是60 × 1000。

1
2
3
4
5
6
7
@Configuration
public class FeignConfig {
@Bean
public Request.Options options() {
return new Request.Options(5000, 10000);
}
}

2.5 客户端组件配置

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

配置 OkHttp 只需要加入OkHttp 的依赖,代码如下:

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

然后修改配置,将Feign的 HttpClient 禁用,启用OkHttp,配置如下:

1
2
3
#feign 使用 okhttp
feign.httpclient.enabled=false
feign.okhttp.enabled=true

关于配置可参考源码org.springframework.cloud.openfeign.FeignAutoConfiguration。

HttpClient自动配置源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@ConditionalOnClass(ApacheHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration {
@Autowired(required = false)
private HttpClient httpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
if (this.httpClient != null) {
return new ApacheHttpClient(this.httpClient);
}
return new ApacheHttpClient();
}
}

OkHttp 自动配置源码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Configuration
@ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnProperty(value = "feign.okhttp.enabled", matchIfMissing = true)
protected static class OkHttpFeignConfiguration {
@Autowired(required = false)
private okhttp3.OkHttpClient okHttpClient;
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient() {
if (this.okHttpClient != null) {
return new OkHttpClient(this.okHttpClient);
}
return new OkHttpClient();
}
}

上面所示两段代码分别是配置 HttpClient 和 OkClient 的方法。其通过@ConditionalOnProperty中的值来决定启用哪种客户端(HTTPClient 和 OkHttp),@ConditionalOnClass 表示对应的类在 classpath 目录下存在时,才会去解析对应的配置文件。

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

除了使用代码的方式来对Feign进行配置,我们还可以通过配置文件的方式来指定 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.xxx.xxxRetryer
# 拦截器
feign.client.config.feignName.requestInterceptors[0]=com.xxx.xxxRequestInterceptor
feign.client.config.feignName.requestInterceptors[1]=com.xxx.xxxRequestInterceptor
# 编码器
feign.client.config.feignName.encoder=com.xxx.xxxEncoder
# 解码器
feign.client.config.feignName.decoder=com.xxx.xxxDecoder
# 契约
feign.client.config.feignName.contract=com.xxx.xxxContract

2.7 继承特性

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

创建一个 Maven 项目,feign-inherit-api,用于存放API接口的定义,增加Feign 的依赖,代码如下:

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

定义接口,指定服务名称,代码如下:

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

创建一个服务提供者feign-inherit-provider,引入feign-inherit-api,代码如下:

1
2
3
4
5
<dependency>
<groupId>net.biancheng</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 "xiaozeng";
}
}

创建一个服务消费者feign-inherit-consumer,同样需要引入feign-inherit-api,用于调用feign-inherit-provider提供的/user/name 接口,代码如下:

1
2
3
4
5
6
7
8
9
10
@RestController
public class UserController {
@Autowired
private UserRemoteClient userRemoteClient;
@GetMapping("/call")
public String callHello() {
String result = userRemoteClient.getName();
System.out.println("getName调用结果:" + result);
}
}

通过将接口的定义单独抽取出来,服务提供者去实现接口,服务消费者直接就可以引入定义好的接口进行调用,非常方便。

2.8 多参数请求构造

多参数请求构造分为GET 请求 和POST 请求两种方式,首先来看GET请求的多参数请求构造方式,代码如下:

1
2
@GetMapping("/user/info")
public String getUserInfo(@RequestParam("name")String name,@RequestParam("age")int age);

另一种是通过Map来传递参数,参数数量可以动态的改变,个人是不建议使用Map进行传参的(Map传递参数最大的问题就是可以随意传参)。代码如下:

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

POST 请求多参数就定义一个参数类,通过@RequestBody 注解的方式来实现,代码如下:

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

实现类中也需要加上@RequestBody 注解,代码如下:

1
2
3
4
5
6
7
@RestController
public class UserController implements UserRemoteClient {
@Override
public String addUser(@RequestBody User user) {
return user.getName();
}
}

【注意:使用继承特性的时候,实现类也需要加上@RequestBody 注解】。

至此,Feign的基本使用已经结束……