0
点赞
收藏
分享

微信扫一扫

Spring Cloud Feign实现自定义复杂对象传参

boomwu 2021-09-21 阅读 85

遇到的困境

现我们服务提供端有如下的根据用户查询条件获取满足条件的用户列表controller接口

@RestController
@RequestMapping("user")
public class UserController {
  @GetMaping("search")
  public List<User> search(User user) {
    // ...
    return list;
  }
}

我们在使用Feign构建远程服务请求客户端的时候,会发现Feign官方版本是不支持GET请求传递自定义的对象,当我们的请求参数很多的时候,我们只能选择以下两种方式:

  • @RequestParam注解方式,这种方式缺点很明显,查询条件越多,feign方法参数越多,而且我们是要求每一个微服务必须提供一个API jar包给其他小组使用的,这样的话User对象完全没法复用,而且纯手写@RequestParam增加了多余的开发量和出错的风险
@FeignClient("user", path = "user")
public interface UserFeign {
  @GetMapping("search")
  public List<User> search(@RequestParam("user_id") int userId, @RequestParam("user_name") String userName, @RequestParam("gender") boolean gender);
}
  • 使用Map传递参数,虽然解决了参数过多的问题,但是一般我们都不建议直接使用Map传递参数,因为没有了强类型约束,编译无法帮你保证程序的正确性和健壮,写错的风险依然存在,更致命的是服务消费端根本无法从这个API看出我到底可以传递哪些参数
Map<String, Object> userMap = new LinkedMultiValueMap();
userMap.put("user_id", 123);
userMap.put("user_name", "codingman1990");

@FeignClient("user", path = "user")
public interface UserFeign {
  @GetMapping("search")
  public List<User> search(Map<String, Object> userMap);
}

如何支持直接传递自定义对象

那么我们希望能有一种方式保持跟controller完全一致只需要传递自定义的对象,既让服务提供端开发人员爽,也让服务消费端开发人员爽,两全其美。既然Feign官方不支持,那我们就自己动手撸源码,自己来实现。

  • AnnotatedParameterProcessor feign方法参数注解处理器,总两个方法:1.获取当前参数注解类型;2.处理当前参数


    除开第三个是我们自己的实现类外,其余三个很明显是分别处理@PathVariable,@Header以及@RequestParam注解的,那么我们就可以依葫芦画瓢,再实现一个自己注解处理器

  • @RequestObject 首先我们自定义这样一个注解,用于在feign方法上标记自定义对象

  • RequestObjectParameterProcessor 自定义识别@RequestObject注解的处理器。这里其实只做了一件事情,告诉context可以作为复杂查询参数对象(可以是Map,@QueryMap,当然这里是我们自定义的@RequestObject)的参数下标,后面读取参数值的时候会用到。标红的1是为了排除基本类型和包装类型参数,它们是不可以作为复杂参数的

  • QueryMapEncoder 就只有一个方法把参数对象转换为Map

  • RequestObjectQueryMapEncoder 自定义的map转换器。具体实现里面做了很多细节优化:
    1.支持camel转snake
    2.支持Jackson的JsonProperty注解
    3.支持枚举序列化
    4.支持JAVA8时间日期格式化
    5.支持基本类型以及包装类型数组
    6.甚至还把分页参数也兼容进来
    以上细节可以根据自己的实际使用场景取舍,执行完这些动作后,放入Map中返回,等待feign构建request的时候直接使用
/**
 * 把@RequestObject对象编码为查询参数Map对象(MethodMetadata.queryMapIndex是唯一可以自定义对象编码的契机了)
 *
 * @author ty
 */
public class RequestObjectQueryMapEncoder implements QueryMapEncoder {
    private final ConcurrentHashMap<Class<?>, List<Field>> fieldMap = new ConcurrentHashMap<>();
    private final DateTimeFormatter LOCAL_DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private final DateTimeFormatter LOCAL_DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    /**
     * 专门应对{@link com.epet.microservices.common.web.Page}仅需要输出的属性
     */
    private static final String[] PRESENT_FIELD_NAME = new String[]{"pageSize", "curPage"};
    private static boolean JACKSON_PRESENT;

    static {
        try {
            Class.forName("com.fasterxml.jackson.annotation.JsonProperty");
            JACKSON_PRESENT = true;
        } catch (ClassNotFoundException e) {
            JACKSON_PRESENT = false;
        }
    }

    @Override
    public Map<String, Object> encode(Object object) {
        if (ClassUtils.isPrimitiveOrWrapper(object.getClass())) {
            throw new EncodeException("@ParamObject can't be primitive or wrapper type");
        }
        Class<?> clazz = object.getClass();
        List<Field> fieldList = fieldMap.computeIfAbsent(clazz, this::fieldList);
        /*List<Field> fieldList = fieldMap.get(clazz);
        if (fieldList == null) {
            fieldList = fieldList(clazz);
            fieldMap.put(clazz, fieldList);
        }*/
        Map<String, Object> map = new HashMap<>(fieldList.size());
        try {
            for (Field field : fieldList) {
                Object fieldObj = field.get(object);
                if (fieldObj == null) {
                    continue;
                }
                Class<?> fieldClazz = field.getType();
                String name;
                // 支持@JsonProperty
                if (JACKSON_PRESENT && field.getDeclaredAnnotation(JsonProperty.class) != null) {
                    name = field.getDeclaredAnnotation(JsonProperty.class).value();
                } else {
                    // 默认camel转snake
                    name = StringUtil.camel2Snake(field.getName());
                }

                // DeserializableEnum特殊处理
                if (DeserializableEnum.class.isAssignableFrom(fieldClazz)) {
                    DeserializableEnum deserializableEnum = (DeserializableEnum) fieldObj;
                    map.put(name, deserializableEnum.getValue());
                }
                // LocalDate
                else if (LocalDate.class.isAssignableFrom(fieldClazz)) {
                    String localDate = LOCAL_DATE_FORMATTER.format((LocalDate) fieldObj);
                    map.put(name, localDate);
                }
                // LocalDateTime
                else if (LocalDateTime.class.isAssignableFrom(fieldClazz)) {
                    String localDateTime = LOCAL_DATE_TIME_FORMATTER.format((LocalDateTime) fieldObj);
                    map.put(name, localDateTime);
                }
                // 基本类型数组
                else if (ClassUtil.isPrimitiveArray(fieldClazz)) {
                    // byte[]
                    if (ClassUtil.isByteArray(fieldClazz)) {
                        map.put(name, StringUtil.join((byte[]) fieldObj, ","));
                    }
                    // char[]
                    else if (ClassUtil.isCharArray(fieldClazz)) {
                        map.put(name, StringUtil.join((char[]) fieldObj, ","));
                    }
                    // short[]
                    else if (ClassUtil.isShortArray(fieldClazz)) {
                        map.put(name, StringUtil.join((short[]) fieldObj, ","));
                    }
                    // int[]
                    else if (ClassUtil.isIntArray(fieldClazz)) {
                        map.put(name, StringUtil.join((int[]) fieldObj, ","));
                    }
                    // float[]
                    else if (ClassUtil.isFloatArray(fieldClazz)) {
                        map.put(name, StringUtil.join((float[]) fieldObj, ","));
                    }
                    // long[]
                    else if (ClassUtil.isLongArray(fieldClazz)) {
                        map.put(name, StringUtil.join((long[]) fieldObj, ","));
                    }
                    // double[]
                    else if (ClassUtil.isDoubleArray(fieldClazz)) {
                        map.put(name, StringUtil.join((double[]) fieldObj, ","));
                    }
                }
                // 基本包装类型数组
                else if (ClassUtil.isPrimitiveWrapperArray(fieldClazz)) {
                    map.put(name, StringUtil.join((Object[]) fieldObj, ","));
                }
                // String[]
                else if (String[].class.isAssignableFrom(fieldClazz)) {
                    map.put(name, StringUtil.join((String[]) fieldObj, ","));
                } else {
                    map.put(name, fieldObj);
                }
            }
            return map;
        } catch (IllegalAccessException e) {
            throw new EncodeException("Fail encode ParamObject into query Map", e);
        }
    }

    private List<Field> fieldList(Class<?> clazz) {
        List<Field> fields = new ArrayList<>();
        for (Field field : clazz.getDeclaredFields()) {
            if (illegalField(field)) {
                fields.add(field);
            }
        }
        // 支持继承的父类属性
        for (Class<?> superClazz : ClassUtils.getAllSuperclasses(clazz)) {
            if (!Object.class.equals(superClazz)) {
                // Page class
                boolean isPage = superClazz.equals(Page.class);
                Arrays.stream(superClazz.getDeclaredFields())
                        .filter(field -> !isPage || (isPage && Arrays.stream(PRESENT_FIELD_NAME).anyMatch(s -> s.equalsIgnoreCase(field.getName()))))
                        .forEach(field -> {
                            if (illegalField(field)) {
                                fields.add(field);
                            }
                        });
                /*for (Field field : superClazz.getDeclaredFields()) {
                    if (illegalField(field)) {
                        fields.add(field);
                    }
                }*/
            }
        }
        return fields;
    }

    private boolean illegalField(Field field) {
        Class<?> fieldType = field.getType();
        // 暂时只能支持一层属性编码,所以必须是基础类型或者包装类型,基础类型或者包装类型数组,String,String[],DeserializableEnum类型
        // 2019-3-8 fix:新增JAVA8 LocalDate和LocalDateTime支持
        if (ClassUtils.isPrimitiveOrWrapper(fieldType)
                || ClassUtil.isPrimitiveOrWrapperArray(fieldType)
                || String.class.isAssignableFrom(fieldType) || String[].class.isAssignableFrom(fieldType)
                || DeserializableEnum.class.isAssignableFrom(fieldType)
                || LocalDateTime.class.isAssignableFrom(fieldType) || LocalDate.class.isAssignableFrom(fieldType)
                // 2019-4-15 fix:新增BigDecimal和BigInteger支持
                || BigDecimal.class.isAssignableFrom(fieldType) || BigInteger.class.isAssignableFrom(fieldType)) {
            if (!field.isAccessible()) {
                field.setAccessible(true);
            }
            return true;
        }
        return false;
    }
}
  • FeignRequestObjectAutoConfiguration 处理器和转换器都写好了,我们现在需要覆盖feign默认的配置(查看FeignClientsConfiguration源码即可理解),转而使用我们自定义的。两个目的:
    1.使用feign.request.object属性可以开启关闭,默认开启
    2.覆盖默认的SpringMvcContract,内部增加RequestObjectParameterProcessor
    3.覆盖默认Feign.Builder,使用我们自定义的RequestObjectQueryMapEncoder
/**
 * 为支持复杂对象类型查询参数自动配置类
 *
 * @author ty
 */
@Configuration
@ConditionalOnClass(Feign.class)
@ConditionalOnProperty(prefix = "feign.request", name = "object", havingValue = "true", matchIfMissing = true)
public class FeignRequestObjectAutoConfiguration {
    /**
     * 覆盖FeignClientsConfiguration默认
     */
    @Bean
    public Contract feignContract(ConversionService feignConversionService) {
        List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
        annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
        annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
        annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
        // 新增的处理复杂对象类型查询参数
        annotatedArgumentResolvers.add(new RequestObjectParameterProcessor());
        return new SpringMvcContract(annotatedArgumentResolvers, feignConversionService);
    }

    /**
     * 覆盖FeignClientsConfiguration默认
     */
    @Configuration
    @ConditionalOnClass({HystrixCommand.class, HystrixFeign.class})
    protected static class HystrixFeignConfiguration {
        @Bean
        @Scope("prototype")
        @ConditionalOnProperty(name = "feign.hystrix.enabled")
        public Feign.Builder feignHystrixBuilder() {
            HystrixFeign.Builder builder = HystrixFeign.builder();
            builder.queryMapEncoder(new RequestObjectQueryMapEncoder());
            return builder;
        }
    }
}
  • spring.factories 开启自动配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.epet.microservices.common.feign.FeignRequestObjectAutoConfiguration

使用

对比之前的@RequestParam和Map用法,方法参数变少了,User对象复用了,对服务提供端和消费端都更方便了

@FeignClient("user", path = "user")
public interface UserFeign {
  @GetMapping("search")
  public List<User> search(@RequestObject User user);
}

后续

最近在调研spring cloud版本升级,发现新版的Feign也支持了自定义对象传参,实现方式大同小异

  • @SpringQueryMap 等同于我们的@RequestObject

  • QueryMapParameterProcessor 等同于我们的RequestObjectParameterProcessor

  • FieldQueryMapEncoder和BeanQueryMapEncoder 等同于我们的RequestObjectQueryMapEncoder


    个人觉得新版虽然官方支持了,但是功能却是很弱,他只是简单的反射获取属性名称和值,像我们前面提到的枚举,日期,camel转snake等业务场景无法满足。只要能够理解实现原理,其实实现自己的方案搭配自己的内部框架使用起来会更方便和强大。
举报

相关推荐

0 条评论