Feign调用传递对象参数,@SpringQueryMap

2021-09-29  本文已影响0人  子宇楚歌

环境:SpringCloud
Feign调用的方法定义和spring的controller方法定义很相似,但是有一些细节上的差异,如

  1. query参数需要显式加@RequestParam注解,并指定参数名
  2. 只有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

这样所有引用该公共模块的微服务都不用重新定义啦!

上一篇下一篇

猜你喜欢

热点阅读