美文网首页
Feign调用传递对象参数,@SpringQueryMap

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

作者: 子宇楚歌 | 来源:发表于2021-09-29 17:15 被阅读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
    

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

    相关文章

      网友评论

          本文标题:Feign调用传递对象参数,@SpringQueryMap

          本文链接:https://www.haomeiwen.com/subject/qqpenltx.html