Feign使用中有一个小小的细节之处,在明明我们使用Get配置的时候,我们会发现Feign会将Get请求转成Post调用。
直接上示例:
接口提供者:
@GetMapping(value = "/provider")
public String test(@RequestParam(value = "name", defaultValue = "zcswl7961") String name) {
List services = discoveryClient.getServices();
/**
* test code
*/
for (String serviceId : services) {
List instances = discoveryClient.getInstances(serviceId);
for (ServiceInstance serviceInstance : instances) {
URI uri = serviceInstance.getUri();
}
}
return "this port is:" + port + " name is:"+name;
}
/**
* test feign Get Post
*/
@PostMapping(value = "provider")
public String postTest(@RequestBody String name) {
System.out.println(name);
return "this method is Post port is:" + port + " name is:" +name;
}
该 服务模块命名为service-hi,我们在服务中定义了两个接口,接口路径相同,都是/provider 只是一个为Get 一个为Post请求,
Feign接口定义:
@FeignClient(value = "service-hi",fallback = ScheduleServiceHiHystric.class)
public interface ScheduleServiceHi {
/**
* 接口调用
* @param name 名称
* @return 返回结果
*/
@RequestMapping(value = "/provider",method = RequestMethod.GET)
String sayProvider(String name);
}
Feign接口中,我们定义了对应Get请求的接口示例,
Feign接口调用:
@RestController
@AllArgsConstructor
public class HiController {
private final ScheduleServiceHi scheduleServiceHi;
@GetMapping(value = "/hi")
public String sayHi(@RequestParam String name) {
return scheduleServiceHi.sayProvider( name );
}
}
我们在服务模块:service-feign(端口:8766)中 定义了一个/hi接口,使用了Feign接口调用,通过调用测试/hi接口之后,我们会发现,feign调用了Post接口的请求数据
curl :
127.0.0.1:8766/hi?name=zhoucg
Response:
this method is Post port is:8763 name is:zhoucg
对于这个现象,我们需要声明参数请求的注解形式
例如,我们可以在Feign的接口定义的参数中加上@RequestParam(“name”)方式
@FeignClient(value = "service-hi",fallback = ScheduleServiceHiHystric.class)
public interface ScheduleServiceHi {
/**
* 接口调用
* @param name 名称
* @return 返回结果
*/
@RequestMapping(value = "/provider",method = RequestMethod.GET)
String sayProvider(@RequestParam("name") String name);
}
另一种方法是更换Apache的HttpClient
在Feign的配置项中加入
feign:
httpclient:
enabled: true
同时使用下面两个依赖:
org.apache.httpcomponents
httpclient
4.5.9
io.github.openfeign
feign-httpclient
10.2.3
这个问题的主要的原因就是Feign默认使用的连接工具实现类,发现只要你有对应的body体对象,就会强制把GET请求转换成POST请求
这句话说的很笼统,下面,就是带着源码细节,我们慢慢的分析,具体的Feign源码的实现细节
针对第一个解决方式:
我们只是简单的在Feign的接口中的调用参数中增加了一个@RequestParam(“name”) 注解就解决了问题,这主要是因为,Feign源码在解析含有@FeignClient注解的接口的时候,在创建代理对象的时候,代理对象在去解析含有@RequestParam注解的参数的时候,会将该参数增强到url上,而不是作为body传递
我们在使用Feign的时候,模块启动类会存在 @EnableFeignClients注解
该注解会通过@Import注解向Spring注入 FeignClientsRegistrar类
FeignClientsRegister是一个ImportBeanDefinitionRegistrar 类型的类 ,系统会默认调用registerBeanDefinitions(AnnotationMetadata metadata) 方法
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
registerDefaultConfiguration(metadata, registry);
registerFeignClients(metadata, registry);
}
该方法中,首先是会根据@EnableFeignClients 配置的Configuration配置环境实体类去向Spring容器中注入BeanDefinition,
BeanDefinition中的name为 default. + @EnableFeignClients注解的类的全名
BeanDefinition中的class为:FeignClientSpecification
然后会去解析指定包下的@FeignClient注解,并创建对应接口的代理类,注册到spring 容器中,
默认情况下,解析的包会从@EnableFeignClient注解的basePackages配置和basePackageClasses配置属性中查询,如果没有配置,会去解析当前包以及子包下含有的@FeignClient注解
然后在遍历每一个包路径,并且解析包中含有@FeignClient注解的BeanDefinition类,进行处理,处理步骤
在解析@FeignClient之后,会通过ObjectFacotry的getObject()方法获取到其代理对象
T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
builder.client(client);
}
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}
我们重点看一下loadBalance方法中的最终会调用ReflectiveFeign#newInstance(Target target) 方法进行获取
首先是通过ParseHandlersByName类apply去获取解析目标接口,并返回方法元数据对象
1,避免检测从Object类继承的方法,剔除检测@FeignClient注解的接口中的静态方法,和抽象方法,
2,进入parseAndValidateMetadata方法,解析对应存在方法中的feign的注解参数
protected MethodMetadata parseAndValidateMetadata(Class> targetType, Method method) {
MethodMetadata data = new MethodMetadata();
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)",
method.getName());
Class>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation) {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
if (data.headerMapIndex() != null) {
checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
genericParameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
}
}
return data;
}
1,该方法中,首先是获取对应feign配置方法中的returnType和configKey
2,解析配置的@RequestMapping注解以及对应的Param注解,最终我们会发现,会将含有@RequestParam注解存储到 indexToNameurl进行封装
protected MethodMetadata parseAndValidateMetadata(Class> targetType, Method method) {
MethodMetadata data = new MethodMetadata();
data.returnType(Types.resolve(targetType, targetType, method.getGenericReturnType()));
data.configKey(Feign.configKey(targetType, method));
if (targetType.getInterfaces().length == 1) {
processAnnotationOnClass(data, targetType.getInterfaces()[0]);
}
processAnnotationOnClass(data, targetType);
for (Annotation methodAnnotation : method.getAnnotations()) {
processAnnotationOnMethod(data, methodAnnotation, method);
}
checkState(data.template().method() != null,
"Method %s not annotated with HTTP method type (ex. GET, POST)",
method.getName());
Class>[] parameterTypes = method.getParameterTypes();
Type[] genericParameterTypes = method.getGenericParameterTypes();
Annotation[][] parameterAnnotations = method.getParameterAnnotations();
int count = parameterAnnotations.length;
for (int i = 0; i < count; i++) {
boolean isHttpAnnotation = false;
if (parameterAnnotations[i] != null) {
isHttpAnnotation = processAnnotationsOnParameter(data, parameterAnnotations[i], i);
}
if (parameterTypes[i] == URI.class) {
data.urlIndex(i);
} else if (!isHttpAnnotation) {
checkState(data.formParams().isEmpty(),
"Body parameters cannot be used with form parameters.");
checkState(data.bodyIndex() == null, "Method has too many Body parameters: %s", method);
data.bodyIndex(i);
data.bodyType(Types.resolve(targetType, targetType, genericParameterTypes[i]));
}
}
if (data.headerMapIndex() != null) {
checkMapString("HeaderMap", parameterTypes[data.headerMapIndex()],
genericParameterTypes[data.headerMapIndex()]);
}
if (data.queryMapIndex() != null) {
if (Map.class.isAssignableFrom(parameterTypes[data.queryMapIndex()])) {
checkMapKeys("QueryMap", genericParameterTypes[data.queryMapIndex()]);
}
}
return data;
}
这也是为什么增加了@RequestParam时,Feign会将其追加到url中,而不是作为body传递
针对第二个解决方法:
第二个解决方式是我们直接替换Feign中的HttpClient调用jar包,
Feign中,默认的Http连接工具是在feign-core.jar包中的Client方法
HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
final HttpURLConnection connection =
(HttpURLConnection) new URL(request.url()).openConnection();
if (connection instanceof HttpsURLConnection) {
HttpsURLConnection sslCon = (HttpsURLConnection) connection;
if (sslContextFactory != null) {
sslCon.setSSLSocketFactory(sslContextFactory);
}
if (hostnameVerifier != null) {
sslCon.setHostnameVerifier(hostnameVerifier);
}
}
connection.setConnectTimeout(options.connectTimeoutMillis());
connection.setReadTimeout(options.readTimeoutMillis());
connection.setAllowUserInteraction(false);
connection.setInstanceFollowRedirects(options.isFollowRedirects());
connection.setRequestMethod(request.httpMethod().name());
Collection contentEncodingValues = request.headers().get(CONTENT_ENCODING);
boolean gzipEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
boolean deflateEncodedRequest =
contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);
boolean hasAcceptHeader = false;
Integer contentLength = null;
for (String field : request.headers().keySet()) {
if (field.equalsIgnoreCase("Accept")) {
hasAcceptHeader = true;
}
for (String value : request.headers().get(field)) {
if (field.equals(CONTENT_LENGTH)) {
if (!gzipEncodedRequest && !deflateEncodedRequest) {
contentLength = Integer.valueOf(value);
connection.addRequestProperty(field, value);
}
} else {
connection.addRequestProperty(field, value);
}
}
}
// Some servers choke on the default accept string.
if (!hasAcceptHeader) {
connection.addRequestProperty("Accept", "*/*");
}
if (request.requestBody().asBytes() != null) {
if (contentLength != null) {
connection.setFixedLengthStreamingMode(contentLength);
} else {
connection.setChunkedStreamingMode(8196);
}
connection.setDoOutput(true);
OutputStream out = connection.getOutputStream();
if (gzipEncodedRequest) {
out = new GZIPOutputStream(out);
} else if (deflateEncodedRequest) {
out = new DeflaterOutputStream(out);
}
try {
out.write(request.requestBody().asBytes());
} finally {
try {
out.close();
} catch (IOException suppressed) { // NOPMD
}
}
}
return connection;
}
Feign原生的连接工具使用了jdk中的rt.jar包的HttpURLConnection 类 进行实现,
其中,对应HttpURLConnection 的连接对象,Feign默认的实现是设置了doOutput为true
connection.setDoOutput(true);
这个设置也正是解释了为什么Feign只要发现你存在body体对象,就会将Get请求转成Post
关于这个HttpURLConnection的配置的解释,我们可以参考strackoverflow这个链接:https://stackoverflow.com/questions/8587913/what-exactly-does-urlconnection-setdooutput-affect
因此,我们可以替换原始的feign中的httpclient的实现,来解决这个问题。
在Netflix的官方github上,同样是针对这个问题提出了一个issue,链接:
https://github.com/spring-cloud/spring-cloud-netflix/issues/1253
其中,有一位打个也同样给出了一个解决方案:通过实现RequestInterceptor 来自定义Feign配置的解析
代码如下:
public class YryzRequestInterceptor implements RequestInterceptor {
@Autowired
private ObjectMapper objectMapper;
@Override
public void apply(RequestTemplate template) {
// feign 不支持 GET 方法传 POJO, json body转query
if (template.method().equals("GET") && template.body() != null) {
try {
JsonNode jsonNode = objectMapper.readTree(template.body());
template.body(null);
Map> queries = new HashMap<>();
buildQuery(jsonNode, "", queries);
template.queries(queries);
} catch (IOException e) {
e.printStackTrace();
}
}
}
private void buildQuery(JsonNode jsonNode, String path, Map> queries) {
if (!jsonNode.isContainerNode()) { // 叶子节点
if (jsonNode.isNull()) {
return;
}
Collection values = queries.get(path);
if (null == values) {
values = new ArrayList<>();
queries.put(path, values);
}
values.add(jsonNode.asText());
return;
}
if (jsonNode.isArray()) { // 数组节点
Iterator it = jsonNode.elements();
while (it.hasNext()) {
buildQuery(it.next(), path, queries);
}
} else {
Iterator> it = jsonNode.fields();
while (it.hasNext()) {
Map.Entry entry = it.next();
if (StringUtils.hasText(path)) {
buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
} else { // 根节点
buildQuery(entry.getValue(), entry.getKey(), queries);
}
}
}
}
}
关于RequestInterceptor的解析,Feign的源码是在SynchronousMethodHandler类中的targetRequest(RequestTemplate template) 方法实现
Request targetRequest(RequestTemplate template) {
for (RequestInterceptor interceptor : requestInterceptors) {
interceptor.apply(template);
}
return target.apply(template);
}
可自行分析