环境:SpringCloud
Feign调用的方法定义和spring的controller方法定义很相似,但是有一些细节上的差异,如
- query参数需要显式加@RequestParam注解,并指定参数名
- 只有body参数可以直接使用包装对象传递,query参数不能
我们这次讨论的就是第二点。get方法参数太多,又不好把参数放body,怎么办呢?有一个注解@SpringQueryMap
@GetMapping(value = "/inner/orders")
Result<Page<OrderVO>> getOrderByPage(
@SpringQueryMap Page<Order> page,
OrderQueryDTO queryDTO,
@RequestHeader String from);
@SpringQueryMap可以实现用包装对象传递多个query参数,查看源码,关键代码如下
ReflectiveFeign.BuildTemplateByResolvingArgs
@Override
public RequestTemplate create(Object[] argv) {
...
// 这里可以看到,queryMapIndex只有一个,即被@SpringQueryMap注解的对象只能有一个
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
Object value = argv[metadata.queryMapIndex()];
Map<String, Object> queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}
...
}
被@SpringQueryMap注解的对象只能有一个,这也是个问题,要想修改成多个比较麻烦。我试了下,因为要修改内部数据结构,feign中许多类又是无法从外部访问的内部类,想要修改的话不能继承,只能完全重写,并且涉及到整条数据链路的很多类,虽然最后成功实现了目的,但是对框架的侵入太严重,弊大于利,放弃。
private RequestTemplate addQueryMapQueryParameters(Map<String, Object> queryMap,
RequestTemplate mutable) {
for (Entry<String, Object> currEntry : queryMap.entrySet()) {
Collection<String> values = new ArrayList<String>();
boolean encoded = metadata.queryMapEncoded();
Object currValue = currEntry.getValue();
if (currValue instanceof Iterable<?>) {
Iterator<?> iter = ((Iterable<?>) currValue).iterator();
while (iter.hasNext()) {
Object nextObject = iter.next();
values.add(nextObject == null ? null
: encoded ? nextObject.toString()
: UriUtils.encode(nextObject.toString()));
}
} else {
values.add(currValue == null ? null
: encoded ? currValue.toString() : UriUtils.encode(currValue.toString()));
}
mutable.query(encoded ? currEntry.getKey() : UriUtils.encode(currEntry.getKey()), values);
}
return mutable;
}
从上面的addQueryMapQueryParameters方法可以看出,它是把每个query参数的值调用toString()方法转成字符串后添加到url上,如果是List之类实现了Iterable接口的集合类,会遍历元素分别转换,最后得到的结果类似于
@Data
public class OrderDTO {
private int index = 3;
// List类型成员变量 {"a", "b"}
private List<String> names;
}
// 转换结果
?index=3&names=a&names=b
但这种转换方式有个问题,就是query参数的类型,除了Iterable类型,其他类型必须确保toString()方法是有意义的,如果是自定义类型,必须重写toString()方法。而且Iterable类型也只能有一层,List<List<String>>这种也是有问题的。这点要特别注意。
然后我们再来看上面的toQueryMap方法,用来将对象转成query参数的映射集合,类型是Map<String, Object>,我们来看看这个映射集合里query参数的类型由何而来呢?
private Map<String, Object> toQueryMap(Object value) {
if (value instanceof Map) {
return (Map<String, Object>) value;
}
try {
return queryMapEncoder.encode(value);
} catch (EncodeException e) {
throw new IllegalStateException(e);
}
}
这个方法很简单,就是调用queryMapEncoder成员变量的encode方法,这个成员变量的类型是QueryMapEncoder,这个类型在feign中提供了两种实现。
一种是默认的FieldQueryMapEncoder,它的encode方法就是使用反射获取对象类型的所有成员变量的Field,然后通过Field取值。使用FieldQueryMapEncoder,调用toQueryMap转换成的query参数类型就是成员变量的类型。
关键代码如下:
private static ObjectParamMetadata parseObjectType(Class<?> type) {
List<Field> fields = new ArrayList<Field>();
for (Field field : type.getDeclaredFields()) {
if (!field.isAccessible()) {
field.setAccessible(true);
}
fields.add(field);
}
return new ObjectParamMetadata(fields);
}
从这里可以看出,它只取了对象类型自身的Field,而没有取父类的Field,这和开发通常定义一个抽取一些公共字段为基类的做法是相悖的,基类中的字段在参数传递中会被忽略。
那我们来看看另一种QueryMapEncoder——BeanQueryMapEncoder,和FieldQueryMapEncoder使用Field取值不同,它使用的是getter方法:
private static ObjectParamMetadata parseObjectType(Class<?> type)
throws IntrospectionException {
List<PropertyDescriptor> properties = new ArrayList<PropertyDescriptor>();
for (PropertyDescriptor pd : Introspector.getBeanInfo(type).getPropertyDescriptors()) {
boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName());
if (isGetterMethod) {
properties.add(pd);
}
}
return new ObjectParamMetadata(properties);
}
使用BeanQueryMapEncoder的好处是可以自定义getter,对变量的值先做format再传递出去。
如果想修改使用的QueryMapEncoder类型怎么办呢?修改调用方微服务中feign配置对象:
@Configuration
public class FeignConfiguration {
@Bean
public Feign.Builder feignBuilder() {
return new Feign.Builder()
// 传入自己想要使用的QueryMapEncoder对象
.queryMapEncoder(new BeanQueryMapEncoder())
.retryer(Retryer.NEVER_RETRY);
}
}
然后在需要应用的Feign client中应用这个配置:
@FeignClient(value = "User", fallbackFactory = OrderFallbackFactory.class, configuration = FeignConfiguration.class)
public interface RemoteOrderService {
如果想使用Field取值,又想继承父类字段,我们也可以自定义一个QueryMapEncoder:
public static class InheritedFieldQueryMapEncoder implements QueryMapEncoder {
@Override
public Map<String, Object> encode(Object object) throws EncodeException {
Class<?> cls = object.getClass();
// 这里的ReflectUtil使用的是cn.hutool.core.util.ReflectUtil,会循环查找所有层级父类的字段
Field[] fields = ReflectUtil.getFields(cls);
Map<String, Object> fieldNameToValue = new HashMap<>(fields.length);
for (Field field : fields) {
Object value = ReflectUtil.getFieldValue(object, field);
if (value != null && value != object) {
fieldNameToValue.put(field.getName(), value);
}
}
return fieldNameToValue;
}
}
如果想在SpringCloud中所有微服务都使用上面的配置呢?
在一个公共模块中添加配置类,然后在resources/META-INF/下新建spring.factories文件:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.mc.common.core.config.FeignConfiguration
这样所有引用该公共模块的微服务都不用重新定义啦!
网友评论