FeignClient : Get请求参数是复杂对象
0.问题
使用openFeign进行REST调用时,正常的Get请求和Post请求都能正常使用;唯有当使用Get请求且请求参数是复杂对象时,直接进入降级处理方法。
FeignClient代码
import com.itheima.pinda.authority.dto.auth.ResourceQueryDTO;
import com.itheima.pinda.authority.entity.auth.Resource;
import com.itheima.pinda.base.R;
import com.itheima.pinda.feign.fallback.ResourceApiFallback;
import com.itheima.pinda.feign.config.FeignConfiguration;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import java.util.List;
@FeignClient(name = "${pinda.feign.authority-server:pd-auth-server}",
configuration = FeignConfiguration.class,
fallback = ResourceApiFallback.class)
public interface ResourceApi {
//查询当前登录用户拥有的资源权限
@GetMapping("/resource")
public R<List<Resource>> visible(ResourceQueryDTO resource);
}
服务方代码
@GetMapping
public R<List<Resource>> visible(ResourceQueryDTO resource) {
if (resource == null) {
resource = new ResourceQueryDTO();
}
if (resource.getUserId() == null) {
resource.setUserId(getUserId());
}
return success(resourceService.findVisibleResource(resource));
}
查阅资料究其原因后应该是Feign将复杂请求参数(如复杂对象)的Get请求,转化为Post请求并将参数放在请求体传递了,导致FeignClient的实际请求与服务费提供的请求不一致
1.解决
组装Feign接口请求时,将body里面的参数取出来,转换为GET方式请求的参数
import com.alibaba.fastjson.JSON;
import feign.Request;
import feign.RequestInterceptor;
import feign.RequestTemplate;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Field;
import java.util.*;
@Configuration
public class FeignConfiguration implements RequestInterceptor {
@Override
public void apply(RequestTemplate requestTemplate) {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
//填充get中的body数据转化成query数据
if (requestTemplate.method().equals(HttpMethod.GET.name()) && Objects.nonNull(requestTemplate.body())) {
String json = requestTemplate.requestBody().asString();
Map<String, Object> map = JSON.parseObject(json);
Set<String> set = map.keySet();
Iterator<String> it = set.iterator();
while (it.hasNext()) {
String key = it.next();
Object values = map.get(key);
if (Objects.nonNull(values)) {
// 将body的参数写入queries
requestTemplate.query(key, values.toString());
}
}
try{
Class requestClass = requestTemplate.getClass();
Field field = requestClass.getDeclaredField("body");
field.setAccessible(true);
//修改body为空。
field.set(requestTemplate, Request.Body.empty());
} catch (Exception ex) {
System.out.println(ex.fillInStackTrace());
}
}
Enumeration<String> headerNames = request.getHeaderNames();
if (headerNames != null) {
while (headerNames.hasMoreElements()) {
String name = headerNames.nextElement();
String values = request.getHeader(name);
// 跳过 content-length
if (name.equals("content-length")){
continue;
}
requestTemplate.header(name, values);
}
} else {
System.out.println(String.format("feign interceptor error header:%s", requestTemplate));
}
}
}
写完以上的代码调试的时候又发现了新的问题
RequestContextHolder.getRequestAttributes()返回为null
问题分析
RequestContextHolder获取信息,是基于ThreadLocal存储的信息(RequestAttributes),主线程能够通过RequestContextHolder获取信息;那么原因肯定跟Feign的线程策略有关,进而想到了hystrix。
通过调试也发现了线程进行了切换,线程名确实是从 main 转化到了 hystrix
了解了一下才知道hystrix的线程隔离策略
线程池隔离 | 信号量隔离 | |
---|---|---|
是否支持超时 | 支持,超时直接返回 | 不支持,如果阻塞,只能通过调 用协议(如:socket超时才能返回) |
是否支持熔断 | 支持,当线程池到达maxSize后,再 请求会触发fallback接口进行熔断 | 支持,当信号量达到maxConcurrentRequests 后,再请求会触发fallback |
隔离原理 | 每个服务单独用线程池 | 通过信号量的计数器 |
是否是异步调用 | 可以是异步,也可以是同步。看调用的方法 | 同步调用,不支持异步 |
资源消耗情况 | 大,大量线程的上下文切换,容易造成机器负载高 | 小,只是个计数器 |
FeignClient会被熔断器(以Hystrix为例)包裹,其中熔断器(以Hystrix为例)的隔离策略一般设置为线程池(也就是说会以一条新的线程来进行服务调用);
此时新的线程取不到旧ThreadLocal中的数据。
针对上述问题提出两个解决方案
方案一
将熔断器隔离策略调整为信号量(信号量隔离方式使用的是工作线程进行请求调用,因此不存在跨线程问题)
hystrix.command.default.execution.isolation.strategy=SEMAPHORE
方案二
请求执行前覆写
Hystrix在将任务添加到内部线程池前会调用HystrixConcurrencyStrategy 抽象对象的wrapCallable方法,复写该方法并将想传递的数据传递进去即可。
为了解决这个问题就继承了HystrixConcurrencyStrategy(Hystrix的并发策略)重写了wrapCallable,将RequestAttributes传递。
import java.util.concurrent.Callable;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
/**
* 解决feign获取上下文失败
* @author xzx
* @data 2022/12/27
*/
public class RequestContextHystrixConcurrencyStrategy extends HystrixConcurrencyStrategy {
@Override
public <T> Callable<T> wrapCallable(Callable<T> callable) {
return new RequestAttributeAwareCallable<>(callable, RequestContextHolder.getRequestAttributes());
}
static class RequestAttributeAwareCallable<T> implements Callable<T> {
private final Callable<T> delegate;
private final RequestAttributes requestAttributes;
public RequestAttributeAwareCallable(Callable<T> callable, RequestAttributes requestAttributes) {
this.delegate = callable;
this.requestAttributes = requestAttributes;
}
@Override
public T call() throws Exception {
try {
RequestContextHolder.setRequestAttributes(requestAttributes);
return delegate.call();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}
然后在resource目录下添加配置文件hystrix-plugins.properties使其生效
# 类的权限的名
hystrix.plugin.HystrixConcurrencyStrategy.implementation=xxx.RequestContextHystrixConcurrencyStrategy