Spring Cloud Feign 使用對象參數的操作
概述
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); }
加註釋的方法,就是我後添加進去的。encode 方法的最後一行稍微修改瞭一下,引用瞭我加的方法,其他都是直接借鑒過來的(本來我想更偷懶,直接繼承一下子,但是它用瞭私有的內部類導致我隻能全部復制粘貼瞭)。
解決方案
1.不用引入其他的 Feign 依賴,保證有下面這個就行(看網上其他方法還要引入特定依賴,要對應版本號,挺麻煩的)
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
2.編寫上面那樣的類,你可以直接復制過去改個包名就行,如果還需要除瞭 Date 以外的格式化,請看註釋和文章分析。其中我對日期的格式化,直接使用瞭 @DateTimeFormat 提供的模式,和 Spring 保持瞭一致。
3.編寫一個 Feign 配置類,將剛自定義的編碼器註冊進去。細節我就不多說瞭:
package com.example.billmanagerfront.config; import com.example.billmanagerfront.config.encoder.PowerfulQueryMapEncoder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import feign.Feign; import feign.Retryer; @Configuration public class FeignConfig { @Bean public Feign.Builder feignBuilder() { return Feign.builder() .queryMapEncoder(new PowerfulQueryMapEncoder()) .retryer(Retryer.NEVER_RETRY); } }
4.Feign 代理接口中聲明使用這個配置類,細節不談
package com.example.billmanagerfront.client; import java.util.List; import com.example.billmanagerfront.config.FeignConfig; import com.example.billmanagerfront.pojo.Bill; import com.example.billmanagerfront.pojo.BillType; import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.SpringQueryMap; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @FeignClient(name = "BILL-MANAGER", path = "bill", configuration = FeignConfig.class) public interface BillClient { @GetMapping("list") List<Bill> list(@SpringQueryMap(true) Bill b); @GetMapping("type") List<BillType> type(); @DeleteMapping("delete/{id}") public String delete(@PathVariable("id") Long id); }
到此這篇關於Spring Cloud Feign 如何使用對象參數的文章就介紹到這瞭,更多相關Spring Cloud Feign對象參數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- Spring Cloud Alibaba 使用 Feign+Sentinel 完成熔斷的示例
- JAVA入門教學之快速搭建基本的springboot(從spring boot到spring cloud)
- 如何自定義feign調用實現hystrix超時、異常熔斷
- 使用feign配置網絡ip代理
- SpringBoot + openFeign實現遠程接口調用的過程