Spring Cloud Feign實(shí)現(xiàn)自定義復(fù)雜對(duì)象傳參

歡迎關(guān)注我的github,以后所有文章源碼都會(huì)陸續(xù)更新上去

遇到的困境

現(xiàn)我們服務(wù)提供端有如下的根據(jù)用戶查詢條件獲取滿足條件的用戶列表controller接口

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

我們?cè)谑褂肍eign構(gòu)建遠(yuǎn)程服務(wù)請(qǐng)求客戶端的時(shí)候,會(huì)發(fā)現(xiàn)Feign官方版本是不支持GET請(qǐng)求傳遞自定義的對(duì)象,當(dāng)我們的請(qǐng)求參數(shù)很多的時(shí)候,我們只能選擇以下兩種方式:

  • @RequestParam注解方式,這種方式缺點(diǎn)很明顯,查詢條件越多,feign方法參數(shù)越多,而且我們是要求每一個(gè)微服務(wù)必須提供一個(gè)API jar包給其他小組使用的,這樣的話User對(duì)象完全沒(méi)法復(fù)用,而且純手寫(xiě)@RequestParam增加了多余的開(kāi)發(fā)量和出錯(cuò)的風(fēng)險(xiǎn)
@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傳遞參數(shù),雖然解決了參數(shù)過(guò)多的問(wèn)題,但是一般我們都不建議直接使用Map傳遞參數(shù),因?yàn)闆](méi)有了強(qiáng)類(lèi)型約束,編譯無(wú)法幫你保證程序的正確性和健壯,寫(xiě)錯(cuò)的風(fēng)險(xiǎn)依然存在,更致命的是服務(wù)消費(fèi)端根本無(wú)法從這個(gè)API看出我到底可以傳遞哪些參數(shù)
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);
}

如何支持直接傳遞自定義對(duì)象

那么我們希望能有一種方式保持跟controller完全一致只需要傳遞自定義的對(duì)象,既讓服務(wù)提供端開(kāi)發(fā)人員爽,也讓服務(wù)消費(fèi)端開(kāi)發(fā)人員爽,兩全其美。既然Feign官方不支持,那我們就自己動(dòng)手?jǐn)]源碼,自己來(lái)實(shí)現(xiàn)。

  • AnnotatedParameterProcessor feign方法參數(shù)注解處理器,總兩個(gè)方法:1.獲取當(dāng)前參數(shù)注解類(lèi)型;2.處理當(dāng)前參數(shù)
    image.png

    除開(kāi)第三個(gè)是我們自己的實(shí)現(xiàn)類(lèi)外,其余三個(gè)很明顯是分別處理@PathVariable,@Header以及@RequestParam注解的,那么我們就可以依葫蘆畫(huà)瓢,再實(shí)現(xiàn)一個(gè)自己注解處理器
    image.png
  • @RequestObject 首先我們自定義這樣一個(gè)注解,用于在feign方法上標(biāo)記自定義對(duì)象
    image.png
  • RequestObjectParameterProcessor 自定義識(shí)別@RequestObject注解的處理器。這里其實(shí)只做了一件事情,告訴context可以作為復(fù)雜查詢參數(shù)對(duì)象(可以是Map,@QueryMap,當(dāng)然這里是我們自定義的@RequestObject)的參數(shù)下標(biāo),后面讀取參數(shù)值的時(shí)候會(huì)用到。標(biāo)紅的1是為了排除基本類(lèi)型和包裝類(lèi)型參數(shù),它們是不可以作為復(fù)雜參數(shù)的
    image.png
  • QueryMapEncoder 就只有一個(gè)方法把參數(shù)對(duì)象轉(zhuǎn)換為Map
    image.png
  • RequestObjectQueryMapEncoder 自定義的map轉(zhuǎn)換器。具體實(shí)現(xiàn)里面做了很多細(xì)節(jié)優(yōu)化:
    1.支持camel轉(zhuǎn)snake
    2.支持Jackson的JsonProperty注解
    3.支持枚舉序列化
    4.支持JAVA8時(shí)間日期格式化
    5.支持基本類(lèi)型以及包裝類(lèi)型數(shù)組
    6.甚至還把分頁(yè)參數(shù)也兼容進(jìn)來(lái)
    以上細(xì)節(jié)可以根據(jù)自己的實(shí)際使用場(chǎng)景取舍,執(zhí)行完這些動(dòng)作后,放入Map中返回,等待feign構(gòu)建request的時(shí)候直接使用
/**
 * 把@RequestObject對(duì)象編碼為查詢參數(shù)Map對(duì)象(MethodMetadata.queryMapIndex是唯一可以自定義對(duì)象編碼的契機(jī)了)
 *
 * @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");
    /**
     * 專(zhuān)門(mén)應(yīng)對(duì){@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 {
                    // 默認(rèn)camel轉(zhuǎn)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);
                }
                // 基本類(lèi)型數(shù)組
                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, ","));
                    }
                }
                // 基本包裝類(lèi)型數(shù)組
                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);
            }
        }
        // 支持繼承的父類(lèi)屬性
        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();
        // 暫時(shí)只能支持一層屬性編碼,所以必須是基礎(chǔ)類(lèi)型或者包裝類(lèi)型,基礎(chǔ)類(lèi)型或者包裝類(lèi)型數(shù)組,String,String[],DeserializableEnum類(lèi)型
        // 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 處理器和轉(zhuǎn)換器都寫(xiě)好了,我們現(xiàn)在需要覆蓋feign默認(rèn)的配置(查看FeignClientsConfiguration源碼即可理解),轉(zhuǎn)而使用我們自定義的。兩個(gè)目的:
    1.使用feign.request.object屬性可以開(kāi)啟關(guān)閉,默認(rèn)開(kāi)啟
    2.覆蓋默認(rèn)的SpringMvcContract,內(nèi)部增加RequestObjectParameterProcessor
    3.覆蓋默認(rèn)Feign.Builder,使用我們自定義的RequestObjectQueryMapEncoder
/**
 * 為支持復(fù)雜對(duì)象類(lèi)型查詢參數(shù)自動(dòng)配置類(lèi)
 *
 * @author ty
 */
@Configuration
@ConditionalOnClass(Feign.class)
@ConditionalOnProperty(prefix = "feign.request", name = "object", havingValue = "true", matchIfMissing = true)
public class FeignRequestObjectAutoConfiguration {
    /**
     * 覆蓋FeignClientsConfiguration默認(rèn)
     */
    @Bean
    public Contract feignContract(ConversionService feignConversionService) {
        List<AnnotatedParameterProcessor> annotatedArgumentResolvers = new ArrayList<>();
        annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
        annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
        annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
        // 新增的處理復(fù)雜對(duì)象類(lèi)型查詢參數(shù)
        annotatedArgumentResolvers.add(new RequestObjectParameterProcessor());
        return new SpringMvcContract(annotatedArgumentResolvers, feignConversionService);
    }

    /**
     * 覆蓋FeignClientsConfiguration默認(rèn)
     */
    @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 開(kāi)啟自動(dòng)配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.epet.microservices.common.feign.FeignRequestObjectAutoConfiguration

使用

對(duì)比之前的@RequestParam和Map用法,方法參數(shù)變少了,User對(duì)象復(fù)用了,對(duì)服務(wù)提供端和消費(fèi)端都更方便了

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

后續(xù)

最近在調(diào)研spring cloud版本升級(jí),發(fā)現(xiàn)新版的Feign也支持了自定義對(duì)象傳參,實(shí)現(xiàn)方式大同小異

  • @SpringQueryMap 等同于我們的@RequestObject
    image.png
  • QueryMapParameterProcessor 等同于我們的RequestObjectParameterProcessor
    image.png
  • FieldQueryMapEncoder和BeanQueryMapEncoder 等同于我們的RequestObjectQueryMapEncoder
    image.png

    個(gè)人覺(jué)得新版雖然官方支持了,但是功能卻是很弱,他只是簡(jiǎn)單的反射獲取屬性名稱和值,像我們前面提到的枚舉,日期,camel轉(zhuǎn)snake等業(yè)務(wù)場(chǎng)景無(wú)法滿足。只要能夠理解實(shí)現(xiàn)原理,其實(shí)實(shí)現(xiàn)自己的方案搭配自己的內(nèi)部框架使用起來(lái)會(huì)更方便和強(qiáng)大。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容