Spring Cloud FeignClient 自定义复杂对象参数解析

教程分享 > Java教程 > Spring Cloud (139) 2024-08-07 11:09:10

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

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

 

AnnotatedParameterProcessor.java 接口feign方法参数注解处理器

Spring Cloud FeignClient 自定义复杂对象参数解析_图示-0990f34322724ecab0eb3303200b0cc9.png

总两个方法:

  1. 获取当前参数注解类型;
  2. 处理当前参数;
Spring Cloud FeignClient 自定义复杂对象参数解析_图示-8ebef426682c4004a820bc73b8d28dbe.png

默认已经实现了 

  1. @CookieValue 
  2. @MatrixVariable
  3. @PathVariable
  4. @SpringQueryMap @QueryMap
  5. @Header 
  6. @RequestParam 
  7. @RequestPartParam 

那么我们就可以依葫芦画瓢,再实现一个自己注解处理器
 

 

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

@RequestObject

Spring Cloud FeignClient 自定义复杂对象参数解析_图示-9714d7278fe84ff2a613d73d46d6e751.png

在定义一个注解参数的处理器 RequestObjectParameterProcessor 来识别和处理@RequestObject 注解。

这里其实只做了一件事情,告诉context可以作为复杂查询参数对象(可以是Map,@QueryMap,当然这里是我们自定义的@RequestObject)的参数下标,后面读取参数值的时候会用到。标红的1是为了排除基本类型和包装类型参数,它们是不可以作为复杂参数的
Spring Cloud FeignClient 自定义复杂对象参数解析_图示-47af234b7b314937a26ced2c82147ee2.png
public class RequestObjectParameterProcessor implements AnnotatedParameterProcessor {
private final static Class<RequestObject> ANNOTATION = RequestObject.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}

@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex();
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata methodMetadata = context.getMethodMetadata();
//非基本类型或包装类型
if (!ClassUtils.isPrimitiveOrWrapper(parameterType)){
//一个接口只能由一个(@QueryMap Map @RequestObject)
Assert.isNull(methodMetadata.queryMapIndex(),"Query map can only be present once.");
methodMetadata.queryMapIndex(parameterIndex);
}
return true;
}
}


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

Spring Cloud FeignClient 自定义复杂对象参数解析_图示-fdb26a332ab049ccb1325f99deaf42bc.png

自定义RequestObjectQueryMapEncoder接口,实现了以下数据转换和功能支持

  1. 支持camel转snake
  2. 支持Jackson的JsonProperty注解
  3. 支持枚举序列化
  4. 支持JAVA8时间日期格式化
  5. 支持基本类型以及包装类型数组
  6. 甚至还把分页参数也兼容进来
以上功能可以根据自己的实际使用场景取舍,执行完这些动作后,放入Map中返回,等待feign构建request的时候直接使用
/**
 * 把@RequestObject对象编码为查询参数Map对象(MethodMetadata.queryMapIndex是唯一可以自定义对象编码的契机了)
 *
 * 
 */
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

/**
 * 为支持复杂对象类型查询参数自动配置类
 *
 */
@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 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;
        }
    }
}

feignContract 方法中其他几个一定要写完哈。

spring.factories 开启自动配置

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.your.package.to.FeignRequestObjectAutoConfiguration

spring.factories文件路径:/src/main/resources/META-INF/spring.factories

如果没相应的目录和文件创建一个,文件就是文本

 

使用

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

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

 

对比@SpringQueryMap 自定义的解析器在某些时候能用上,简单使用则直接用@SpringQueryMap即可。

 

https://www.leftso.com/article/2408071018542626.html

相关文章
Spring Cloud FeignClient fallbackFactory配置详解一般FeignClient需要指定一个fallbackFactory或者fallback,一个一个接口的实...
前言最近在学习springcloud,在进行springboot拆分成springcloud项目的时候,我使用feign来进行微服务的调用,遇到了一些坑,特此总
@FeignClient 配置 fallback无效 解决检查配置文件中是否有以下配置片段feign: hystrix: enabled: true该配置不依赖
如何支持直接传递自定义对象那么我们希望能有一种方式保持跟controller完全一致只需要传递自定义的对象,既让服务提供端开发人员爽,也让服务消费端开发人员爽,
1. 什么是 spring cloud?spring cloud 是一系列框架的有序集合
随着Spring Cloud 的越来越流行,国内很多公司也在开始使用该框架了
feign-client在第一次调用微服务会出现Read timed out异常,提示的报错信息:java.net.SocketTimeoutException: Read timed out ...
环境说明spring cloud 2021.04 spring cloud alibaba 2021.0.4 spring boot 2.6.13 nacos 2.2.3 问题排查...
项目源码下载:(访问密码:9987)Spring-Cloud-Circuit-Breaker.zip学习在调用底层微服务的同时利用调用的Spring Cloud Netflix堆栈组件之一Hys...
演示项目源码下载:(访问密码:9987)spring-cloud-zipkin.zipZipkin是非常有效的工具分布追踪在微服务生态系统
演示项目源码下载:(访问密码:9987)spring-cloud-config-server-git.zip微服务方法现在已经成为任何新 API 开发的行业标准,几乎所有组织都在推广它
演示项目源码下载:(访问密码:9987)Spring-Cloud-discovery-server.zip 了解如何创建微服务的基础上,Spring Cloud,对Netflix的Eureka注...
我们知道spring boot可以通过文件名来区分配置,如下:application.ymlapplication-dev.yml #开发环境application-test.yml #测试环境...
在这个 Spring cloud 教程中,学习在 spring boot/cloud 项目中使用 Netflix Ribbon 使用客户端负载平衡
演示项目源码下载:(访问密码:9987)Spring-Cloud-Consoul.zip了解如何创建微服务的基础上Spring cloud,对登记HashiCorp Consul注册服务器,以及...