在Spring Cloud体系中,Feign的作用就是可以与Eureka组合在一起简化各服务之间的http调用,在Spring Cloud Netflix体系中,Feign和Ribbon都是用于调用其他服务的,它们之间的关系是,Feign是在Ribbon的基础上发展而来的,Ribbon的特性在Feign里面同样有体现,比如要修改Feign的负载均衡策略,其实就是修改Ribbon的负载均衡策略,按照Ribbon的配置进行即可(这大概是现有版本Spring Cloud Netflix默认集成了Riibon的缘由,就是要使用Feign,不要直接使用Ribbon,我瞎猜的)。Feign存在的意义就是简化http调用,让开发者可以像调用方法一样访问http接口,在非Sping Cloud体系项目中,遇到需要调用第三方接口的需求,也可以使用它去简化一些http的调用。
Feign是一个声明式的Rest客户端,它能让http调用更加简单。Feign提供了http请求的模板,通过编写简单的接口和插入注解,就可以定义好http请求的参数、格式、地址等信息,这样,就可以像调用方法一样,完成各服务之间请求处理。
加入依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
脱离Spring Cloud使用Feign
1、编写一个提供接口服务的项目(这里暂时命名为cloud-server0项目,使用9000端口,服务名也定义为cloud-server0),提供的接口如下:
@Controller
public class Server0Controller {
@GetMapping("/server0/api0")
@ResponseBody
public String methodA() {
return "第1个服务提供者:第一个接口";
}
}
2、创建另外一个项目(这里暂时命名为cloud-server3-Feign项目,使用9003端口,服务名也定义为cloud-server3-Feign),在启动类中开启对Feign的支持
@SpringBootApplication
// 开启Feign支持。注意:如果Feign相关代码不在当前类所在包或子包路径下,需要指定扫描的路径
@EnableFeignClients
public class Server3FeignApp {
public static void main(String[] args) {
SpringApplication.run(Server3FeignApp.class, args);
}
}
3、在cloud-server3-Feign项目中编写Feign调用上面接口的代码
// 不在Spring Cloud中使用Feign服务
public interface FeignWithoutCloudService {
// 指明使用GET的请求方式调用目标接口,目标接口的uri:/server0/api0
@RequestLine("GET /server0/api0")
String methodA();
}
4、在cloud-server3-Feign项目中配置Feign(否则光凭上面一个光秃秃的interface,鬼知道该怎么执行)。然后就可以像使用其他Spring Bean一样,注入FeignWithoutCloudService后,就可以像调用方法一样访问其他http接口
@Configuration
public class FeignConfig {
@Bean
public FeignWithoutCloudService initFeign() {
// 简单配置Feign(Feign还有其他很多配置),指明FeignWithoutCloudService接口中所有方法的调用路径是127.0.0.1:9000
FeignWithoutCloudService feignWithoutCloudService = Feign.builder()
.target(FeignWithoutCloudService.class, "http://127.0.0.1:9000");
return feignWithoutCloudService;
}
}
5、启动cloud-server0项目(http接口提供者),然后在cloud-server3-Fign项目中编写下面的单元测试
@RunWith(SpringRunner.class)
@SpringBootTest
public class FeignWithoutCloudServiceTest {
// 像调用方法一样简单.注入被配置为Feign Client的实例
@Autowired
private FeignWithoutCloudService feignWithoutCloudService;
@Test
public void testA() {
String requestResult = feignWithoutCloudService.methodA();
System.out.println("调用结果 : " + requestResult);
}
}
运行结果如下:
可以看到,实现了像调用方法一样调用http接口。在这之前的项目中,一般调用http接口,可以使用HttpClient、Okhttp、RestTemplate,甚至是原生的网络编程Httpurlconnection,Feign确实简化了不少http的调用。
Feign的常用注解
在上面的FeignWithoutCloudService接口中,已经使用@RequestLine注解。
@RequestLine注解(包路径为:feign.RequestLine)
该注解用于Feign接口中的方法体上,用于定义调用目标http接口的请求动作,比如GET、POST等,以及定义目标http接口的资源定位符uri。请求动作和uri中间隔一个空格。@RequestLine是支持Rest风格的,比如这种uri:xxx/{yyy}/zzz。
注:在Spring Cloud体系中使用,因为锲约配置的原因,需要使用SpringMVC中的@GetMapping、@PostMapping等去代替@RequestLine
下述示例,表示用GET请求访问一个ip:port/server0/api0 的接口:
@RequestLine("GET /server0/api0")
String methodA();
@Param注解(包路径为:feign.Param)
在方法参数中使用该注解,用于定义目标http接口所需要的参数(目标接口的参数类型限制:multipart/form-data)。
下述示例,表示用POST请求访问一个ip:port/server0/api1的接口,目标接口需要一个id参数:
@RequestLine("POST /server0/api1")
String methodB(@Param("id") String id);
说明:@Param作为一个参数绑定注解,里面的值必须和目标http接口的参数名保持一致,Feign接口的参数名可以自定义(和Mybatis的@Param类似)。比如上面可以改为:@Param("id") String userId
@Headers注解(包路径为:feign.Param)
该注解用于Feign接口中的方法体上,因为请求头都是键值对形式,所有它的值需要用英文逗号隔开。作用是为当前http请求携带请求头信息。它支持的value值是一个String数组,即除了可以定义单个请求头,还可以用花括号"{}"定义多个请求头。
下述示例,表示为当请求携带token和Content-Type请起头:
@Headers({"token:zepal123456", "Content-Type:application/json"})
@QueryMap注解(包路径为:feign.Param)
该注解用于Feign接口的方法参数上。用于构建动态请求参数,标记一个Map,使该Map中的所有键值对都成为目标http接口的请求参数。这种方式灵活性较高,假如需要调用的http接口,一会需要3个请求参数,一会需要2个请求参数,@QueryMap就可以派上用场了,当然,目标http接口是固定的请求参数也可以使用,只是可读性没那么强。
下述示例,使用@QueryMap定义请求参数,调用该方法时,传入的Map参数中,所有的键值对都会作为当前http调用的请求参数:
// 参数params中所有的键值对,都会作为请求参数
@RequestLine("POST /server0/api5")
String methodG(@QueryMap Map<String, String> params);
@HeaderMap注解(包路径为:feign.Param)
该注解用于Feign接口的方法参数上。用于构建动态请求头,标记一个Map,使该Map中的所有键值对都成为请求头。这种方式定义请求头灵活性比较高,假如需要调用的http接口,一会需要3个请求头,一会需要2个请求头,@HeaderMap就可以派上用场了,当然,目标http接口是固定的请求头也可以使用,只是可读性没那么强。
下述示例,使用@HeaderMap定义请求头,调用该方法时,传入的Map参数中,所有的键值对都会作为当前http调用的请求头:
// 参数headers中所有的键值对,都会作为请求头
@RequestLine("POST /server0/api4")
String methodF(@HeaderMap Map<String, String> headers);
@Body注解(包路径为:feign.Param)
该注解用于Feign接口中的方法体上。用于标记请求体,比如传递json、xml等格式的请求参数,需要配合@Headers注解使用。
下述示例,表示使用json格式的请求参数:
@RequestLine("POST /server0/api2")
@Headers("Content-Type:application/json")
// json以转义符'%7B'开始,以转义符'%7D'结束
@Body("%7B\"user_name\": \"{user_name}\", \"password\": \"{password}\"%7D")
String methodD(@Param("user_name") String user_name, @Param("password") String password);
下述示例,表示使用xml格式的请求参数:
@RequestLine("POST /server0/api3")
@Headers("Content-Type: application/xml")
@Body("<login \"user_name\"=\"{user_name}\" \"password\"=\"{password}\"/>")
String methodE(@Param("user_name") String user_name, @Param("password") String password);
Feign的原生配置
在上面的FeignConfig编码中,只是简单的配置了Feign,Feign支持以Feign.builder()去构建相关设置。
更多详细说明参考:https://github.com/OpenFeign/feign
设置自定义编码解码器
Feign.builder()
.encoder(Encoder encoder)
.decoder(Encoder encoder);
该配置的作用是对http的请求信息和响应信息进行编码和解码, Feign默认提供了基于SpringMVC HttpMessageConverters支持的编码解码方式。支持所有实现了feign.codec.Encoder和feign.codec.Decoder接口的编码解码,常见支持Feign编码解码的有JacksonEncoder/JacksonDecoder、GsonEncoder/GsonDecoder、SaxEncoder/SaxDecoder(基于XML 格式的Sax 库持久化转换协议)、JAXBEncoder/JAXBDecoder(基于XML 格式的JAXB 库持久化转换协议)、ResponseEntityEncoder/ResponseEntityDecoder、SpringEncoder/SpringDecoder,以及自定义的编码解码,只要实现Encoder和Decoder接口即可。
设置日志信息
设置日志主要是用于生成Feign的http调用日志。
Feign默认提供的日志级别有:NONE(不输出日志)/BASIC(只输出请求方法的URL和响应的状态码以及接口的执行时间)/HEADERS(将BASIC信息和请求头信息输出)/FULL(输出完整的请求信息)
示例:
Feign.builder()
.logger((new Logger.JavaLogger()).appendToFile("D:/feign.log"))
.logLevel(Level.FULL);
然后会在目标文件路径下输出日志文件:feign.log,内容如下:
设置超时时间
示例:
Feign.builder()
.options(new Options(1000, 1000));
第一个参数是:连接超时时间,单位是毫秒,默认是10s;第二个参数是:读取超时时间,单位是毫秒,默认是60s。同时Options(包路径是:feign.Request.Options),提供另外一个构造方法,多一个boolean类型参数,表示告知Feign客户端是否遵循重定向,默认为true。
设置请求拦截器
作用是用于拦截Feign的所有请求,然后做一些其他逻辑,比如,为所有请求都加上一个请求头,做一些鉴权服务等。所有定义的请求拦截都需要去实现feign.RequestInterceptor接口,去重新它里面的apply(RequestTemplate template)方法。
示例:
public void test() {
Feign.builder()
.requestInterceptor(new ForwardedForInterceptor());
}
// 为当前项目中所用使用Feign的http请求添加一个请求头token
static class ForwardedForInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
template.header("token", "xxxx");
}
}
另外,Feign还支持了请求拦截器链的形式(即多个拦截器依次组成的一层一层拦截),拦截顺序按照拦截器链的遍历顺序执行,所以这里为了保证定义的拦截器链按照自己想要的顺序执行,需要使用List等存储有序的结构去创建。配置伪代码如下:
Feign.builder()
.requestInterceptors(Iterable<RequestInterceptor> requestInterceptors)
Feign提供了一个Basic认证组件(feign.auth.BasicAuthRequestInterceptor),用于认证Spring Security。即假如调用的目标http接口,开启了Spring Security认证,那http请求的鉴权信息就可以使用Feign提供的Basic认证组件。配置伪代码如下:
# username和password分别是目标http接口鉴权信息
Feign.builder()
.requestInterceptor(new BasicAuthRequestInterceptor("username", "password"))
设置调用客户端组件
Feign默认使用的HttpClient对http接口进行调用。Feign支持所有实现了feign.Client接口的http组件,除了默认HttpClient,还有OkHttpClient,还可以支持Ribbon提供的智能路由和弹性功能。
示例:
Feign.builder()
.client(Client client);
设置重试机制
该配置的作用是,当请求超时或异常后,对http请求进行重试。支持实现了feign.Retryer接口的所有重试。Feign提供了一个默认的重试类feign.Retryer.Default,提供的值:最大重试次数5次,重试间隔100ms,最大重试等待时间1s。
示例:
Feign.builder()
.retryer(Retryer retryer);
在Spring Cloud体系中使用Feign
这里就不演示负载均衡,如果需要进行负载均衡测试,多创建一个服务提供者项目,按照Ribbon的配置进行即可。
FBI WARNING:注释掉前面所有脱离Spring Cloud体系和Feign相关的代码及配置,否则可能会出现一些莫名其妙的异常。
1、创建一个Eureka项目(这里暂时命名为cloud-eureka项目,使用9999端口,服务名也定义为cloud-eureka),将上述的两个项目cloud-server0和cloud-server3都注册到Eureka
2、重新编写Feign的调用接口,如下:
// @FeignClient会将当前接口注册为Spring Bean,类似于前面定义的FeignConfig
// value:指明需要调用的服务名,也就是需要调用哪个服务的接口
// path:目标http接口中的统一uri前缀
@FeignClient(value = "cloud-server0", path = "/server0")
public interface FeignWithCloudService {
// 默认的情况下,这里将不再支持@RequestLine("GET /api0")的方式
// 这是Feign的契约组件提供的功能,可以更改,锲约配置仅支持在SPring Cloud体系中使用,即在非Spring Cloud体系中,还是要老老实实使用原生注解@RequestLine
@GetMapping("/api0")
String methodA();
}
3、运行测试,可以得到同样的响应信息
锲约配置
在Feign中默认提供的锲约配置是SpringMvcContract,即支持使用SpringMVC中@GetMapping、@PostMapping等去代替@RequestLine注解。
注意:在非Spring Cloud体系中使用Feign,只能使用原生的@RequestLine,而在Spring Cloud体系中使用Feign,默认配置下,需要强制使用锲约注解去代替@RequestLine。
配置示例:
// 当前版本中提供的默认契约配置类是:Contract.Default
Feign.builder()
.contract(new Contract.Default());
说明:这种配置Feign的方式,仅存在于非Spring Cloud体系中,在非Spring Cloud体系中本身又不支持SpringMVC的契约,所以这里仅仅只是为了说明它的原生契约配置在什么地方,没什么实际意义。
在Spring Cloud体系中配置Feign
上面展示了通过Feign.builder()去进行原生的配置Feign。那么在Spring Cloud体系中,这种方式不太好使。
1、配置文件(application.properties)形式
Feign提供了一个配置属性feign.client.config,配置的值是Map<String, FeignClientConfiguration>。
示例:
# 设置Feign连接超时和读超时都为5s
feign.client.config.FeignClientConfiguration.connect-timeout=5000
feign.client.config.FeignClientConfiguration.read-timeout=5000
# 将默认的锲约配置换成SpringMvcContract:以前旧版本中SpringMvcContract是默认的契约配置.注:这个契约同样强制使用@GetMapping等注解去替代@RequestLine
feign.client.config.FeignClientConfiguration.contract=org.springframework.cloud.openfeign.support.SpringMvcContract
# 像上面原生Feign配置一样,其他更多配置以FeignClientConfiguration进行即可
2、在@FeignClient(configuration = Class<?>[])指明相应的配置(configuration注解属性是一个Class数组,表示支持多个配置类)
创建一个或多个配置类
@Configuration
public class FeignConfig {
// 初始化重试机制(这里用了Feign默认的重试机制做示例)
@Bean
public Retryer initRetryer() {
return new Retryer.Default();
}
// 初始化Feign的连接超时和读超时
@Bean
public Options initOptions() {
return new Options(5000, 5000);
}
}
在@FeignClient注解中指向该配置类
@FeignClient(value = "", path = "", configuration = FeignConfig.class)
或者以数组的方式
// 如果有写了多个配置类,就用数组的表现形式
@FeignClient(value = "", path = "", configuration = {FeignConfig.class})
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