Feign 的实战运用
Feign 默认Client 的替换
Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送 http 请求,没有连接池,但是每个地址会保持一个长连接,即利用 http 的 persistence Connection
我们可以使用 Apache 的 HttpClient 替换掉 Feign 原生的 HttpClient,通过设置连接池,超时时间等对服务之间的调用调优
使用 HTTPClient 替换 Feign 默认 Client
- 创建一个提供服务的 feign-service, 为了演示方便 , 这里就不由提供方维护接口了
/**
*@Description
*@author apdoer
*@CreateDate 2019/5/17-23:47
*@Version 1.0
*===============================
**/
@RestController
public class DemoController {
@GetMapping("/hello")
public String hello(){
return "hello-service-feign";
}
}
- 创建调用服务的 http-client
- 我们使用 apache 的 HTTPClient 来替换 feign 默认的 HTTPClient
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency> <!--使用Apache HTTPClient替换掉原生的HTTPClient--> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies>
- 定义一个 client 来调用 feign-service 提供的接口
@FeignClient(value = "feign-service") public interface HelloClient { @GetMapping("/hello") String hello(); }
通过@FeignClient指定需要调用的服务
通过springmvc的注解绑定对应服务的对应映射- 创建一个controller来用于访问,方便测试
/** *@Description *@author apdoer *@CreateDate 2019/5/17-23:37 *@Version 1.0 *=============================== **/ @RestController public class HelloController { @Autowired private HelloClient client; @GetMapping("/hello-client") public String getInfo(){ return client.hello(); } }
- 在 application.yml 中配置让 feign 启动时加载 HTTP client 替换默认的 client
server: port: 8010 spring: application: name: http-client feign: httpclient: enabled: true eureka: client: service-url: defaultZone: http://localhost:8761/eureka/
- 然后我们访问 http-client 的接口
localhost:8010/hello-client
可以看到也是调用成功,替换成功!
使用 okhttp 替换 feign 默认的 client
http 是目前比较通用的网络请求方式
有效的使用 http 可以使应用访问速度变得更快,更节省带宽
okhttp 是一个很棒的 http 客户端,目前我自己所在的公司项目也在用,具有以下特性支持 SPDY, 可以合并多个到同一个主机的请求
使用连接池技术减少请求的延迟【如果 SPDY 是可用的话】
使用 GZIP 压缩减少传输的数据量
缓存响应避免重复的网络请求
- 创建一个名为 okhttp 的 springboot 工程,引入 okhttp 的依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-okhttp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 在配置文件中开启okhttp 替换默认的client
server:
port: 8011
spring:
application:
name: okhttp
feign:
httpclient:
enabled: false
okhttp:
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
- 这里说一下
okhttpClient 是 okhttp 的核心功能的执行者,可以通过
OKHttpClient client = new OKHTTPClient();
来创建默认的 okHttpClient 对象
也可以使用如下的配置来自定义 okHttpClient 对象,托管给 spring 管理.这里只给了常用的配置项,其他请自行扩展
/**
*@Description
*@author apdoer
*@CreateDate 2019/5/18-16:19
*@Version 1.0
*===============================
**/
@Configuration
@ConditionalOnClass(Feign.class)
@AutoConfigureBefore(FeignAutoConfiguration.class)
public class FeignOkhttpConfig {
@Bean
public okhttp3.OkHttpClient okHttpClient(){
return new okhttp3.OkHttpClient.Builder()
//设置连接超时时间
.connectTimeout(60,TimeUnit.SECONDS)
//设置读超时
.readTimeout(60,TimeUnit.SECONDS)
//设置写超时
.writeTimeout(60,TimeUnit.SECONDS)
//是否自动重连
.retryOnConnectionFailure(true)
.connectionPool(new ConnectionPool())
.build();
}
}
- 启动项目访问
localhost:8011/hello-okhttp
可以看到使用成功
Feign 的 post 和 get 的多参数传递
在实际开发中,使用 feign 进行服务间调用,多参数传递是无法避免的
web 开发中,springmvc 是支持 GET 方法直接绑定 pojo 的,但是 Feign 的实现并未覆盖所有的springmvc功能,目前常见的解决方式有下面这些
- 把 pojo 拆散成一个个单独的属性作为方法参数
- 把方法参数变成 Map 传递
- 使用 GET 请求传递 @RequestBody , 但此方法违反 Restful 规范
这里我们介绍一种更加优雅的方式 , 即通过 Feign 拦截器的方式处理
通过实现 Feign 的RequestInterceptor 中的 apply 方法来进行统一拦截转换处理 Feign 中的 GET 方法多参数传递问题
Feign 进行 POST 多参数相比 GET 来说就简单的多
拦截器代码如下
@Component
public class FeignRequestInterceptor implements RequestInterceptor {
@Autowired
private ObjectMapper mapper;
@Override
public void apply(RequestTemplate requestTemplate) {
//Feign 不支持GET 请求传 pojo ,json body 转 query
if (requestTemplate.method().equals("GET") && requestTemplate.body() != null){
try {
JsonNode jsonNode = mapper.readTree(requestTemplate.body());
requestTemplate.body((Request.Body) null);
Map<String, Collection<String>> queries = new HashMap<>();
buildQuery(jsonNode,"",queries);
requestTemplate.queries(queries);
} catch (IOException e) {
//根据实际业务处理异常
e.printStackTrace();
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
if (!jsonNode.isContainerNode()) { // 叶子节点
if (jsonNode.isNull()) {
return;
}
Collection<String> values = queries.get(path);
if (null == values) {
values = new ArrayList<>();
queries.put(path, values);
}
values.add(jsonNode.asText());
return;
}
if (jsonNode.isArray()) { // 数组节点
Iterator<JsonNode> it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry<String, JsonNode> entry = it.next();
if (StringUtils.hasText(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else { // 根节点
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}
- 然后在 feign-sevice 另外再提供两个接口
@GetMapping("/testGET")
public String hello(User user) {
return user.toString();
}
@PostMapping("/testPOST")
public String testPost(User user) {
return user.toString();
}
- 在 服务消费方client端维护这两个接口,在controller中调用
@GetMapping("/okhttp_get")
public String get(User user){
System.out.println(user);
return client.get(user);
}
@PostMapping("okhttp_post")
public String post(User user){
return client.post(user);
}
- 访问localhost:8011/okhttp_get 和用postman访问localhost:8011/okhttp_post都是可以访问到的
Feign 的文件上传
feign 早先不支持文件上传,后来虽然支持但是需要一次性完整读取到内存再编码发送
现在 feign 官方提供了子项目 feign-form , 其中实现了上传所需的 Encoder
下面会使用 feign-form 结合feign实现文件的上传
- 创建 feign-file-server 作为文件服务器,服务提供者
- 该工程用于接受 feign 客户端发过来的文件,这里模拟获取文件名,controller 代码如下
@PostMapping(value = "/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String fileUploadServer(MultipartFile file){ return file.getOriginalFilename(); }
- 在调用上传服务的客户端 client 我们创建 feign client
@PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = {MediaType.APPLICATION_JSON_UTF8_VALUE}) String fileUpload(@RequestPart(value = "file")MultipartFile file);
- controller 代码如下
@PostMapping(value = "/upload_client",consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String post(@RequestPart("file") MultipartFile file){ return client.fileUpload(file); }
- 可以看到,我们用 postman 测试是可以拿到 文件的 originName 的.由此可以证明 service 端获取到了对应的文件
需要注意的几点
- 文件上传的客户端 produces 和 consumes 必填
- 注意区分 @RequestParam 和 @RequestPart 的区别,不要写错
解决 Feign 首次请求失败的问题
当 Feign 和 Ribbon 整合了 Hystrix 之后,可能会出现首次调用失败的问题,原因如下
- Hystrix 的默认超时时间是 1 秒, 如果超过 1秒未响应 , 将会进入 fallback 代码, 由于 bean 的装配以及懒加载机制等 , Feign首次请求都会比较慢,大于1秒, 就会出现请求失败的情况
解决方式
- 1 将Hystrix 的超时时间改为5秒
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:5000
- 2 禁用 hystrix 的超时时间
hystrix.command.default.execution.timeout.enabled:false
- 3 使用 Feign 的时候直接关闭 Hystrix , 一般不这么做
feign.hystrix.enabled:false
Feign 返回图片流处理方式
在使用 Feign 的过程中 可以将流转换成 字节数组传递 , 但是因为Controller 层的返回不能直接返回 byte ,因此可以将 Feign 的返回值修改为
response
或者ResponseEntity<byte>
@GetMapping("/createImage")
public byte[] createImage(@RequestParam("imageKey") String imageKey);
@GetMapping("/createImage")
public Response createImage(@RequestParam ("imageKey") String imageKey);
@GetMapping("/createImage")
public ResponseEntity<byte> createImage(@RequestParam ("imageKey") String imageKey);
Feign 调用传递 Token
在进行认证鉴权的时候, 不管是 jwt 还是security ,当使用 Feign 时就会发现外部请求到 A服务的时候,A服务是可以拿到 token 的, 然而当服务使用 Feign 调用 服务B服务时 , Token 就会丢失,从而认证失败,解决比较简单, 就是在Feign调用时,向请求头里添加需要传递的 Token
实现 Feign 提供的 一个接口 RequestInterceptor, 假设我们在验证权限时放在请求头里的key 为 oauthToken ,先获取当前请求的key为 oauthToken ,然后放到 Feign 的请求 Header上【像这种通用的代码确定传递的key后建议统一到通用的二方库使用】
/**
*@Description token 过滤器
*@author apdoer
*@CreateDate 2019/5/19-0:07
*@Version 1.0
*===============================
**/
public class FeignTokenInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
if (null == getHttpServletRequest()){
return;
}
//将获取的Token对应值向下传递
requestTemplate.header("oauthToken",getHttpServletRequest().getHeader("oauthToken"));
}
private HttpServletRequest getHttpServletRequest() {
try {
return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
}catch (Exception e){
return null;
}
}
}
总结
- 使用 apache 的HTTPClient 替换 Feign 自带的 HTTPClient
- 使用 okhttp 替换 Feign 自带的 HttpClient
- Feign 的多参数传递问题
- Feign 服务间文件上传问题
- Feign 的图片返回和 Token 传递问题
- 以上