OpenFeign是Netflix开源的一款声明式HTTP客户端,它能简化HTTP接口调用的代码逻辑,在微服务的服务间通信方面应用十分广泛。此外,SpringCloud对OpenFeign进行了集成,如果使用SpringCloud建议直接使用spring-cloud-starter-openfeign
这个项目,但如果不希望引入SpringCloud也可以直接使用OpenFeign库。
这里我们简单介绍OpenFeign库(非SpringCloud环境)的使用。
OpenFeign这个库在早期的groupId
为com.netflix.feign
,2016年改为了io.github.openfeign
,同时项目也改名为OpenFeign,前者也早就停止更新了,我们使用新的包即可。
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>12.1</version>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-jackson</artifactId>
<version>12.1</version>
</dependency>
feign-core
是OpenFeign的核心功能,feign-jackson
是和JSON序列化相关的库,这里其实也可以换成Gson
等其它的库,但因为SpringWeb默认的JSON序列化器就是Jackson(工程中也存在Jackson的依赖),所以我们直接选择feign-jackson
即可。
除此之外,OpenFeign默认基于HTTPURLConnection客户端进行HTTP请求,如果使用这个默认客户端就不需要再额外引入其他依赖了,但如果想使用OkHttpClient等其它HTTP客户端,OpenFeign也是支持的,但也要引入对应客户端库的依赖。
这里简单起见,我们就直接在SpringBoot环境中使用OpenFeign。如果非Spring工程也是类似的,这里就不多介绍了。
例子中,假设我们要调用一个GET的HTTP接口:GET http://127.0.0.1:8081/api/v1/order/queryOrderById?orderId={orderId}
。
package com.gacfox.web.client;
import com.gacfox.orderapi.OrderDto;
import feign.Param;
import feign.RequestLine;
public interface OrderServiceClient {
@RequestLine("GET /api/v1/order/queryOrderById?orderId={orderId}")
OrderDto queryOrderById(@Param("orderId") String orderId);
}
上面代码我们声明了一个接口,其中使用了几个OpenFeign提供的注解:
@RequestLine
:声明具体的调用地址,其中可以包含占位符@Param
:具体的参数,可以填充@RequestLine
中声明的占位符注意:对于GET请求我们在参数中一定要使用@Param
将参数拼到URL的形式,OpenFeign默认使用JDK的HTTPURLConnection,它不支持GET请求带Body(当然这确实也不合理)。OpenFeign接口参数如果不指定任何注解默认是放到HTTP请求的Body中的,HTTPURLConnection此时又会将GET请求转为POST,调用就可能会报错405 Method Not Allowed
。
package com.gacfox.web.config;
import com.gacfox.web.client.OrderServiceClient;
import feign.Feign;
import feign.jackson.JacksonDecoder;
import feign.jackson.JacksonEncoder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ClientConfig {
@Value("${demoboot.order.url}")
private String orderUrl;
@Bean
public OrderServiceClient getOrderServiceClient() {
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(OrderServiceClient.class, orderUrl);
}
}
上面代码我们具体创建了OrderServiceClient
对象,我们虽然没有具体编写该接口的实现类,但OpenFeign通过代理帮我们生成了,在具体实现的对象中会有发起HTTP请求的相关逻辑。创建时我们还传了几个参数,比如Jackson的序列化和反序列化器,以及接口类对象,和具体的调用地址(协议、IP和端口)。在实际开发中,我们其实也可以使用注解和反射的形式封装这部分逻辑,实现接口代理的自动注册,这里就不多介绍了。
注:这里${demoboot.order.url}
实际值为http://127.0.0.1:8081
。
package com.gacfox.web.comtroller;
import com.gacfox.orderapi.OrderDto;
import com.gacfox.web.client.OrderServiceClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
@RequestMapping(value = "/")
public class IndexPageController {
@Autowired
private OrderServiceClient orderServiceClient;
@GetMapping(value = "/orderPage")
public String orderPage(@RequestParam String orderId, Model model) {
OrderDto orderDto = orderServiceClient.queryOrderById(orderId);
model.addAttribute("order", orderDto);
return "order";
}
}
最终,我们在一个Controller中注入了OrderServiceClient
对象,并在具体的代码中对其进行了调用。
发起POST请求也是类似的,下面例子我们写了一个POST请求的接口方法:
@Headers("Content-Type: application/json")
@RequestLine("POST /api/v1/order/addOrder")
void addOrder(OrderDto orderDto);
这里我们的orderDto
参数没有带任何注解,表示该参数使用指定的默认序列化器写入请求Body。不过这里我们还用到了@Headers
注解,这是因为我们发起的是一个POST请求,其Body为JSON字符串,如果不加Content-Type: application/json
,上游的服务是拒绝识别解析的。
之前我们已经使用过了几个OpenFeign的注解,这些注解的具体用法可以参考以下表格。
注解 | 目标 | 说明 |
---|---|---|
@RequestLine | 方法 | 指定接口的URL,可以带有{} 格式的参数占位符 |
@Param | 参数 | 指定参数填充@RequestLine ,@Headers 或@Body 中的占位符 |
@Headers | 类,方法 | 指定请求头,可以带有{} 格式的参数占位符 |
@QueryMap | 参数 | 可以指定Map 类型或POJO的一组封装的参数。用于填充@RequestLine 中的占位符 |
@HeaderMap | 参数 | 可以指定Map 类型或POJO的一组封装的参数。用于填充@Headers 中的占位符 |
@Body | 方法 | 请求Body模板,支持{} 占位符,用于Body为非默认序列化器格式,需要手动指定的情况 |
OpenFeign支持指定请求的拦截器,我们可以在拦截器中指定一些通用的处理逻辑。
package com.gacfox.web.client;
import feign.RequestInterceptor;
import feign.RequestTemplate;
public class ApiRequestInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
requestTemplate.header("Content-Type", "application/json");
}
}
上面代码我们创建了一个ApiRequestInterceptor
类,其中逻辑非常简单,给所有请求增加一个Header。
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.requestInterceptor(new ApiRequestInterceptor())
.target(OrderServiceClient.class, orderUrl);
创建客户端代理对象时,我们直接用requestInterceptor()
方法指定拦截器对象即可。
下面例子我们对客户端请求进行了一些配置。
return Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.options(new Request.Options(1, TimeUnit.SECONDS, 5, TimeUnit.SECONDS, true))
.target(OrderServiceClient.class, orderUrl);
Request.Options()
方法中,前两个数字参数指定请求的connectTimeout
和readTimeout
,其默认值分别为10
秒和60
秒,最后一个参数指定是否followRedirects
。