分享、成長,拒絕淺藏輒止。關注公衆號【BAT的烏托邦】,回覆關鍵字
專欄
有Spring技術棧、中間件等小而美的原創專欄供以避免費學習。本文已被 https://www.yourbatman.cn 收錄。java
你好,我是YourBatman。程序員
上篇文章 大篇幅把Spring全新一代類型轉換器介紹完了,已經至少可以考個及格分。在介紹Spring衆多內建的轉換器裏,我故意留下一個尾巴,放在本文專門撰文講解。數據庫
爲了讓本身能在「擁擠的人潮中」顯得不(更)一(突)樣(出),A哥特地準備了這幾個特殊的轉換器助你破局,穿越擁擠的人潮,踏上Spring已爲你製做好的高級賽道。編程
本文的焦點將集中在上文留下的4個類型轉換器上。數組
這三個比較特殊,屬於「最後的」「兜底類」類型轉換器:緩存
IdToEntityConverter
:本文重點。給個ID自動幫你兌換成一個Entity對象toString()
轉化爲String類型。當匹配不到任何轉換器時,它用於兜底Spring新一代類型轉換內建了很是多的實現,這些在初始化階段大都被默認註冊進去。註冊點在DefaultConversionService
提供的一個static靜態工具方法裏:安全
static靜態方法具備與實例無關性,我我的以爲把該static方法放在一個xxxUtils裏統一管理會更好,放在具體某個組件類裏反倒容易產生語義上的誤導性app
DefaultConversionService: public static void addDefaultConverters(ConverterRegistry converterRegistry) { // 一、添加標量轉換器(和數字相關) addScalarConverters(converterRegistry); // 二、添加處理集合的轉換器 addCollectionConverters(converterRegistry); // 三、添加對JSR310時間類型支持的轉換器 converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new StringToTimeZoneConverter()); converterRegistry.addConverter(new ZoneIdToTimeZoneConverter()); converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter()); // 四、添加兜底轉換器(上面處理不了的全交給這幾個哥們處理) converterRegistry.addConverter(new ObjectToObjectConverter()); converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry)); converterRegistry.addConverter(new FallbackObjectToStringConverter()); converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry)); } }
該靜態方法用於註冊全局的、默認的轉換器們,從而讓Spring有了基礎的轉換能力,進而完成絕大部分轉換工做。爲了方便記憶這個註冊流程,我把它繪製成圖供以你保存:框架
特別強調:轉換器的註冊順序很是重要,這決定了通用轉換器的匹配結果(誰在前,優先匹配誰)。dom
針對這幅圖,你可能還會有疑問:
用於實現集合/數組類型到Stream類型的互轉,這從它支持的Set<ConvertiblePair>
集合也能看出來:
@Override public Set<ConvertiblePair> getConvertibleTypes() { Set<ConvertiblePair> convertiblePairs = new HashSet<ConvertiblePair>(); convertiblePairs.add(new ConvertiblePair(Stream.class, Collection.class)); convertiblePairs.add(new ConvertiblePair(Stream.class, Object[].class)); convertiblePairs.add(new ConvertiblePair(Collection.class, Stream.class)); convertiblePairs.add(new ConvertiblePair(Object[].class, Stream.class)); return convertiblePairs; }
它支持的是雙向的匹配規則:
/** * {@link StreamConverter} */ @Test public void test2() { System.out.println("----------------StreamConverter---------------"); ConditionalGenericConverter converter = new StreamConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(Set.class); TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Stream.class); boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp); System.out.println("是否可以轉換:" + matches); // 執行轉換 Object convert = converter.convert(Collections.singleton(1), sourceTypeDesp, targetTypeDesp); System.out.println(convert); System.out.println(Stream.class.isAssignableFrom(convert.getClass())); }
運行程序,輸出:
----------------StreamConverter--------------- 是否可以轉換:true java.util.stream.ReferencePipeline$Head@5a01ccaa true
關注點:底層依舊依賴DefaultConversionService
完成元素與元素之間的轉換。譬如本例Set -> Stream的實際步驟爲:
也就是說任何集合/數組類型是先轉換爲中間狀態的List,最終調用list.stream()
轉換爲Stream流的;如果逆向轉換先調用source.collect(Collectors.<Object>toList())
把Stream轉爲List後,再轉爲具體的集合or數組類型。
說明:若source是數組類型,那底層實際使用的就是ArrayToCollectionConverter,注意觸類旁通
StreamConverter它的訪問權限是default,咱們並不能直接使用到它。經過上面介紹可知Spring默認把它註冊進了註冊中內心,所以面向使用者咱們直接使用轉換服務接口ConversionService即可。
@Test public void test3() { System.out.println("----------------StreamConverter使用場景---------------"); ConversionService conversionService = new DefaultConversionService(); Stream<Integer> result = conversionService.convert(Collections.singleton(1), Stream.class); // 消費 result.forEach(System.out::println); // result.forEach(System.out::println); //stream has already been operated upon or closed }
運行程序,輸出:
----------------StreamConverter使用場景--------------- 1
再次特別強調:流只能被讀(消費)一次。
由於有了ConversionService
提供的強大能力,咱們就能夠在基於Spring/Spring Boot作二次開發時使用它,提升系統的通用性和容錯性。如:當方法入參是Stream類型時,你既能夠傳入Stream類型,也能夠是Collection類型、數組類型,是否是瞬間逼格高了起來。
按照添加轉換器的順序,Spring在最後添加了4個通用的轉換器用於兜底,你可能平時並不關注它,但它實時就在發揮着它的做用。
將源對象轉換爲目標類型,很是的通用:Object -> Object:
@Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); }
雖然它支持的是Object -> Object,看似沒有限制但實際上是有約定條件的:
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { return (sourceType.getType() != targetType.getType() && hasConversionMethodOrConstructor(targetType.getType(), sourceType.getType())); }
是否可以處理的判斷邏輯在於hasConversionMethodOrConstructor
方法,直譯爲:是否有轉換方法或者構造器。代碼詳細處理邏輯以下截圖:
此部分邏輯可分爲兩個part來看:
當不是首次進入處理時,會走快速返回流程。也就是第0步isApplicable
判斷邏輯,有這幾個關注點:
method.getDeclaringClass()
的子類型/相同類型建立目標對象的實例,此轉換器支持兩種方式:
method.invoke(source)
)ctor.newInstance(source)
)以上case,在下面均會給出代碼示例。
對於首次處理的轉換,就會進入到詳細的三部曲邏輯:經過反射嘗試找到合適的Member用於建立目標實例,也就是上圖的一、二、3步。
step1:determineToMethod,從sourceClass
裏找實例方法,對方法有以下要求:
"to" + targetClass.getSimpleName()
,如toPerson()
step2:determineFactoryMethod,找靜態工廠方法,對方法有以下要求:
valueOf(sourceClass)
或者 of(sourceClass)
或者from(sourceClass)
step3:determineFactoryConstructor,找構造器,對構造器有以下要求:
特別值得注意的是:此轉換器不支持Object.toString()方法將sourceType轉換爲java.lang.String。對於toString()支持,請使用下面介紹的更爲兜底的FallbackObjectToStringConverter
。
// sourceClass @Data public class Customer { private Long id; private String address; public Person toPerson() { Person person = new Person(); person.setId(getId()); person.setName("YourBatman-".concat(getAddress())); return person; } } // tartgetClass @Data public class Person { private Long id; private String name; }
書寫測試用例:
@Test public void test4() { System.out.println("----------------ObjectToObjectConverter---------------"); ConditionalGenericConverter converter = new ObjectToObjectConverter(); Customer customer = new Customer(); customer.setId(1L); customer.setAddress("Peking"); Object convert = converter.convert(customer, TypeDescriptor.forObject(customer), TypeDescriptor.valueOf(Person.class)); System.out.println(convert); // ConversionService方式(實際使用方式) ConversionService conversionService = new DefaultConversionService(); Person person = conversionService.convert(customer, Person.class); System.out.println(person); }
運行程序,輸出:
----------------ObjectToObjectConverter--------------- Person(id=1, name=YourBatman-Peking) Person(id=1, name=YourBatman-Peking)
// sourceClass @Data public class Customer { private Long id; private String address; } // targetClass @Data public class Person { private Long id; private String name; /** * 方法名稱能夠是:valueOf、of、from */ public static Person valueOf(Customer customer) { Person person = new Person(); person.setId(customer.getId()); person.setName("YourBatman-".concat(customer.getAddress())); return person; } }
測試用例徹底同上,再次運行輸出:
----------------ObjectToObjectConverter--------------- Person(id=1, name=YourBatman-Peking) Person(id=1, name=YourBatman-Peking)
方法名能夠爲valueOf、of、from
任意一種,這種命名方式幾乎是業界不成文的規矩,因此遵照起來也會比較容易。可是:建議仍是註釋寫好,防止別人重命名而致使轉換生效。
基本同靜態工廠方法示例,略
基於本轉換器能夠完成任意對象 -> 任意對象的轉換,只須要遵循方法名/構造器默認的一切約定便可,在咱們平時開發書寫轉換層時是很是有幫助的,藉助ConversionService
能夠解決這一類問題。
對於Object -> Object的轉換,另一種方式是自定義
Converter<S,T>
,而後註冊到註冊中心。至於到底選哪一種合適,這就看具體應用場景嘍,本文只是多給你一種選擇
Id(S) --> Entity(T)。經過調用靜態查找方法將實體ID兌換爲實體對象。Entity裏的該查找方法須要知足以下條件find[EntityName]([IdType])
:
find + entityName
。如Person類的話,那麼方法名叫findPerson
說明:此方法能夠沒必要是public,但建議用public。這樣即便JVM的Security安全級別開啓也可以正常訪問
支持的轉換Pair以下:ID和Entity均可以是任意類型,能轉換就成
@Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, Object.class)); }
判斷是否能執行準換的條件是:存在符合條件的find方法,且source能夠轉換爲ID類型(注意source能轉換成id類型就成,並不是目標類型哦)
@Override public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { Method finder = getFinder(targetType.getType()); return (finder != null && this.conversionService.canConvert(sourceType, TypeDescriptor.valueOf(finder.getParameterTypes()[0]))); }
根據ID定位到Entity實體對象簡直太太太經常使用了,運用好此轉換器的提供的能力,或許能讓你事半功倍,大大減小重複代碼,寫出更優雅、更簡潔、更易於維護的代碼。
Entity實體:準備好符合條件的findXXX方法
@Data public class Person { private Long id; private String name; /** * 根據ID定位一個Person實例 */ public static Person findPerson(Long id) { // 通常根據id從數據庫查,本處經過new來模擬 Person person = new Person(); person.setId(id); person.setName("YourBatman-byFindPerson"); return person; } }
應用IdToEntityConverter,書寫示例代碼:
@Test public void test() { System.out.println("----------------IdToEntityConverter---------------"); ConditionalGenericConverter converter = new IdToEntityConverter(new DefaultConversionService()); TypeDescriptor sourceTypeDesp = TypeDescriptor.valueOf(String.class); TypeDescriptor targetTypeDesp = TypeDescriptor.valueOf(Person.class); boolean matches = converter.matches(sourceTypeDesp, targetTypeDesp); System.out.println("是否可以轉換:" + matches); // 執行轉換 Object convert = converter.convert("1", sourceTypeDesp, targetTypeDesp); System.out.println(convert); }
運行程序,正常輸出:
----------------IdToEntityConverter--------------- 是否可以轉換:true Person(id=1, name=YourBatman-byFindPerson)
示例效果爲:傳入字符串類型的「1」,就能返回獲得一個Person實例。能夠看到,咱們傳入的是字符串類型的的1,而方法入參id類型實際爲Long類型,但由於它們能完成String -> Long轉換,所以最終仍是可以獲得一個Entity實例的。
這個使用場景就比較多了,須要使用到findById()
的地方均可以經過它來代替掉。如:
Controller層:
@GetMapping("/ids/{id}") public Object getById(@PathVariable Person id) { return id; } @GetMapping("/ids") public Object getById(@RequestParam Person id) { return id; }
Tips:在Controller層這麼寫我並不建議,由於語義上沒有對齊,勢必在代碼書寫過程當中帶來必定的麻煩。
Service層:
@Autowired private ConversionService conversionService; public Object findById(String id){ Person person = conversionService.convert(id, Person.class); return person; }
Tips:在Service層這麼寫,我我的以爲仍是OK的。用類型轉換的領域設計思想代替了自上而下的過程編程思想。
經過簡單的調用Object#toString()
方法將任何支持的類型轉換爲String類型,它做爲底層兜底。
@Override public Set<ConvertiblePair> getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(Object.class, String.class)); }
該轉換器支持CharSequence/StringWriter等類型,以及全部ObjectToObjectConverter.hasConversionMethodOrConstructor(sourceClass, String.class)
的類型。
說明:ObjectToObjectConverter不處理任何String類型的轉換,原來都是交給它了
略。
將任意類型轉換爲一個Optional<T>
類型,它做爲最最最最最底部的兜底,稍微瞭解下便可。
@Test public void test5() { System.out.println("----------------ObjectToOptionalConverter---------------"); ConversionService conversionService = new DefaultConversionService(); Optional<Integer> result = conversionService.convert(Arrays.asList(2), Optional.class); System.out.println(result); }
運行程序,輸出:
----------------ObjectToOptionalConverter--------------- Optional[[2]]
一個典型的應用場景:在Controller中可傳可不傳的參數中,咱們不只能夠經過@RequestParam(required = false) Long id
來作,仍是能夠這麼寫:@RequestParam Optional<Long> id
。
本文是對上文介紹Spring全新一代類型轉換機制的補充,由於關注得人較少,因此纔有機會突破。
針對於Spring註冊轉換器,須要特別注意以下幾點:
對於複雜的對象 -> 對象類型的轉換,通常須要你自定義轉換器,或者參照本文的標準寫法完成轉換。總之:Spring提供的ConversionService
專一於類型轉換服務,是一個很是很是實用的API,特別是你正在作基於Spring二次開發的狀況下。
固然嘍,關於ConversionService
這套機制還並未詳細介紹,如何使用?如何運行?如何擴展?帶着這三個問題,我們下篇見。
【Spring類型轉換】系列:
【Jackson】系列:
【數據校驗Bean Validation】系列:
【新特性】系列:
【程序人生】系列:
還有諸如【Spring配置類】【Spring-static】【Spring數據綁定】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏托邦
回覆專欄
二字便可所有獲取,也可加我fsx1056342982
,交個朋友。
有些已完結,有些連載中。我是A哥(YourBatman),我們下期再見