Spring Cloud Feign 用于微服务的封装,通过接口代理的实现方式让微服务调用变得简单,让微服务的使用上如同本地服务。但是它在传参方面不是很完美。在使用 Feign 代理 GET 请求时,对于简单参数(基本类型、包装器、字符串)的使用上没有困难,但是在使用对象传参时却无法自动的将对象包含的字段解析出来。
如果你没耐心看完,直接跳到最后一个标题跟着操作就行了。
@RequestBody对象传参是很常见的操作,虽然可以通过一个个参数传递来替代,但是那样就太麻烦了,所以必须解决这个问题。
我在网上看到有人用 @RequestBody 来注解对象参数,我在尝试后发现确实可用。这个方案实际使用 body 体装了参数(使用的是 GET 请求),但是这个方案有些问题:
注解需要在 consumer 和 provider 两边都有,这造成了麻烦
使用接口测试工具 Postman 无法跑通微服务,后来发现是因为 body 体的格式选择不正确,这个格式不是通常的表单或者路径拼接,而是 GraphQL。我没有研究过这种格式应该如何填写参数,但是 Postman 上并没有给出像表单那样方便的格式,这对于测试是很不利的。
@SpringQueryMap于是我继续寻找答案,发现可以使用 @SpringQueryMap 仅添加在 consumer 的参数上就能自动对 Map 类型参数编码再拼接到 URL 上。而我用的高版本的 Feign,可以直接把对象编码。
可是正当我以为得到正解时,却发现还是有问题:
我明明在 Date 类型的字段上加上了 @DateTimeFormat(pattern = "yyyy-MM-dd"),却没有生效,他用自己的方式进行了编码(或者说序列化),而且官方确实没有提供这种格式化方式。
又一番找寻后发现了一位大佬自己实现了一个注解转换替代 @SpringQueryMap,并实现了丰富的格式化功能 ORZ(原文链接:Spring Cloud Feign实现自定义复杂对象传参),只能说佩服佩服。但是我没有那样的技术,又不太想复制粘贴他那一大堆的代码,因为出了问题也不好改,所以我还是想坚持最大限度地使用框架,最小限度的给框架填坑。
QueryMapEncoder终于功夫不费有心人,我发现了 Feign 预留的自定义编码器接口 QueryMapEncoder,框架提供了两个实现:
FieldQueryMapEncoder
BeanQueryMapEncoder
虽然这两个实现不能满足我的要求,但是只要稍加修改写一个自己的实现类就行了,于是我在 FieldQueryMapEncoder 的基础上修改,仅仅添加了一个方法,小改了一个方法就实现了功能。
原理:Feign 其实还是用 Map<String, Object> 进行的编码,编码方式也很简单,String 是 key,Object 是 value。最开始的方式就是用 Object 的 toString() 方法把参数编码,这也是为什么 Date 字段会变成一个默认的时间格式,因为 toString() 根本和 @DateTimeFormat 没有关系。而高版本使用编码器实现了对象传参,实际实际上是通过简单的反射获取对象的元数据,再放到 Map 中。
上面的原理都能从 @DateTimeFormat 的注释和编码器的源码中得到答案。
我们要做的就是自定义一个编码器,实现在元数据放入 Map 之前根据需要把字段变成我们想要的字符串。下面是我实现的代码,供参考:
package com.example.billmanagerfront.config.encoder; import java.lang.reflect.Field; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.TimeZone; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; import org.springframework.format.annotation.DateTimeFormat; import feign.Param; import feign.QueryMapEncoder; import feign.codec.EncodeException; public class PowerfulQueryMapEncoder implements QueryMapEncoder { private final Map<Class<?>, ObjectParamMetadata> classToMetadata = new ConcurrentHashMap<>(); @Override public Map<String, Object> encode(Object object) throws EncodeException { ObjectParamMetadata metadata = classToMetadata.computeIfAbsent(object.getClass(), ObjectParamMetadata::parseObjectType); return metadata.objectFields.stream() .map(field -> this.FieldValuePair(object, field)) .filter(fieldObjectPair -> fieldObjectPair.right.isPresent()) .collect(Collectors.toMap(this::fieldName, this::fieldObject)); } private String fieldName(Pair<Field, Optional<Object>> pair) { Param alias = pair.left.getAnnotation(Param.class); return alias != null ? alias.value() : pair.left.getName(); } // 可扩展为策略模式,支持更多的格式转换 private Object fieldObject(Pair<Field, Optional<Object>> pair) { Object fieldObject = pair.right.get(); DateTimeFormat dateTimeFormat = pair.left.getAnnotation(DateTimeFormat.class); if (dateTimeFormat != null) { DateFormat format = new SimpleDateFormat(dateTimeFormat.pattern()); format.setTimeZone(TimeZone.getTimeZone("GMT+8")); // TODO: 最好不要写死时区 fieldObject = format.format(fieldObject); } else { } return fieldObject; } private Pair<Field, Optional<Object>> FieldValuePair(Object object, Field field) { try { return Pair.pair(field, Optional.ofNullable(field.get(object))); } catch (IllegalAccessException e) { throw new EncodeException("Failure encoding object into query map", e); } } private static class ObjectParamMetadata { private final List<Field> objectFields; private ObjectParamMetadata(List<Field> objectFields) { this.objectFields = Collections.unmodifiableList(objectFields); } private static ObjectParamMetadata parseObjectType(Class<?> type) { List<Field> allFields = new ArrayList<Field>(); for (Class<?> currentClass = type; currentClass != null; currentClass = currentClass.getSuperclass()) { Collections.addAll(allFields, currentClass.getDeclaredFields()); } return new ObjectParamMetadata(allFields.stream() .filter(field -> !field.isSynthetic()) .peek(field -> field.setAccessible(true)) .collect(Collectors.toList())); } } private static class Pair<T, U> { private Pair(T left, U right) { this.right = right; this.left = left; } public final T left; public final U right; public static <T, U> Pair<T, U> pair(T left, U right) { return new Pair<>(left, right); } } }