Feign 在默认情况下使用的是 JDK 原生的 URLConnection 发送 http 请求,没有连接池,但是每个地址会保持一个长连接,即利用 http 的 persistence Connection
我们可以使用 Apache 的 HttpClient 替换掉 Feign 原生的 HttpClient,通过设置连接池,超时时间等对服务之间的调用调优
/**
*@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";
}
}
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>org.apache.httpcomponentsgroupId>
<artifactId>httpclientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
@FeignClient(value = "feign-service")
public interface HelloClient {
@GetMapping("/hello")
String hello();
}
通过@FeignClient指定需要调用的服务
通过springmvc的注解绑定对应服务的对应映射
/**
*@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();
}
}
server:
port: 8010
spring:
application:
name: http-client
feign:
httpclient:
enabled: true
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
http 是目前比较通用的网络请求方式
有效的使用 http 可以使应用访问速度变得更快,更节省带宽
okhttp 是一个很棒的 http 客户端,目前我自己所在的公司项目也在用,具有以下特性支持 SPDY, 可以合并多个到同一个主机的请求
使用连接池技术减少请求的延迟【如果 SPDY 是可用的话】
使用 GZIP 压缩减少传输的数据量
缓存响应避免重复的网络请求
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-netflix-eureka-clientartifactId>
dependency>
<dependency>
<groupId>org.springframework.cloudgroupId>
<artifactId>spring-cloud-starter-openfeignartifactId>
dependency>
<dependency>
<groupId>io.github.openfeigngroupId>
<artifactId>feign-okhttpartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-testartifactId>
<scope>testscope>
dependency>
dependencies>
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();
}
}
在实际开发中,使用 feign 进行服务间调用,多参数传递是无法避免的
web 开发中,springmvc 是支持 GET 方法直接绑定 pojo 的,但是 Feign 的实现并未覆盖所有的springmvc功能,目前常见的解决方式有下面这些
这里我们介绍一种更加优雅的方式 , 即通过 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);
}
}
}
}
}
@GetMapping("/testGET")
public String hello(User user) {
return user.toString();
}
@PostMapping("/testPOST")
public String testPost(User user) {
return user.toString();
}
@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);
}
feign 早先不支持文件上传,后来虽然支持但是需要一次性完整读取到内存再编码发送
现在 feign 官方提供了子项目 feign-form , 其中实现了上传所需的 Encoder
下面会使用 feign-form 结合feign实现文件的上传
@PostMapping(value = "/upload",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String fileUploadServer(MultipartFile file){
return file.getOriginalFilename();
}
@PostMapping(value = "/upload",
consumes = MediaType.MULTIPART_FORM_DATA_VALUE,
produces = {MediaType.APPLICATION_JSON_UTF8_VALUE})
String fileUpload(@RequestPart(value = "file")MultipartFile file);
@PostMapping(value = "/upload_client",consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public String post(@RequestPart("file") MultipartFile file){
return client.fileUpload(file);
}
需要注意的几点
解决 Feign 首次请求失败的问题
当 Feign 和 Ribbon 整合了 Hystrix 之后,可能会出现首次调用失败的问题,原因如下
解决方式
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds:5000
hystrix.command.default.execution.timeout.enabled:false
feign.hystrix.enabled:false
Feign 返回图片流处理方式
在使用 Feign 的过程中 可以将流转换成 字节数组传递 , 但是因为Controller 层的返回不能直接返回 byte ,因此可以将 Feign 的返回值修改为
response
或者ResponseEntity
@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;
}
}
}