分享、成長,拒絕淺藏輒止。關注公眾號【BAT的烏托邦】,回覆關鍵字
專欄
有Spring技術棧、中介軟體等小而美的原創專欄供以免費學習。本文已被 https://www.yourbatman.cn 收錄。
✍前言
你好,我是YourBatman。
上篇文章 大篇幅把Spring全新一代型別轉換器介紹完了,已經至少能夠考個及格分。在介紹Spring眾多內建的轉換器裡,我故意留下一個尾巴,放在本文專門撰文講解。
為了讓自己能在“擁擠的人潮中”顯得不(更)一(突)樣(出),A哥特意準備了這幾個特殊的轉換器助你破局,穿越擁擠的人潮,踏上Spring已為你製作好的高階賽道。
版本約定
- Spring Framework:5.3.1
- Spring Boot:2.4.0
✍正文
本文的焦點將集中在上文留下的4個型別轉換器上。
- StreamConverter:將Stream流與集合/陣列之間的轉換,必要時轉換元素型別
這三個比較特殊,屬於“最後的”“兜底類”型別轉換器:
- ObjectToObjectConverter:通用的將原物件轉換為目標物件(通過工廠方法or構造器)
IdToEntityConverter
:本文重點。給個ID自動幫你兌換成一個Entity物件- FallbackObjectToStringConverter:將任何物件呼叫
toString()
轉化為String型別。當匹配不到任何轉換器時,它用於兜底
預設轉換器註冊情況
Spring新一代型別轉換內建了非常多的實現,這些在初始化階段大都被預設註冊進去。註冊點在DefaultConversionService
提供的一個static靜態工具方法裡:
static靜態方法具有與例項無關性,我個人覺得把該static方法放在一個xxxUtils裡統一管理會更好,放在具體某個元件類裡反倒容易產生語義上的誤導性
DefaultConversionService:
public static void addDefaultConverters(ConverterRegistry converterRegistry) {
// 1、新增標量轉換器(和數字相關)
addScalarConverters(converterRegistry);
// 2、新增處理集合的轉換器
addCollectionConverters(converterRegistry);
// 3、新增對JSR310時間型別支援的轉換器
converterRegistry.addConverter(new ByteBufferConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new StringToTimeZoneConverter());
converterRegistry.addConverter(new ZoneIdToTimeZoneConverter());
converterRegistry.addConverter(new ZonedDateTimeToCalendarConverter());
// 4、新增兜底轉換器(上面處理不了的全交給這幾個哥們處理)
converterRegistry.addConverter(new ObjectToObjectConverter());
converterRegistry.addConverter(new IdToEntityConverter((ConversionService) converterRegistry));
converterRegistry.addConverter(new FallbackObjectToStringConverter());
converterRegistry.addConverter(new ObjectToOptionalConverter((ConversionService) converterRegistry));
}
}
該靜態方法用於註冊全域性的、預設的轉換器們,從而讓Spring有了基礎的轉換能力,進而完成絕大部分轉換工作。為了方便記憶這個註冊流程,我把它繪製成圖供以你儲存:
特別強調:轉換器的註冊順序非常重要,這決定了通用轉換器的匹配結果(誰在前,優先匹配誰)。
針對這幅圖,你可能還會有疑問:
- JSR310轉換器只看到TimeZone、ZoneId等轉換,怎麼沒看見更為常用的LocalDate、LocalDateTime等這些型別轉換呢?難道Spring預設是不支援的?
- 答:當然不是。 這麼常見的場景Spring怎能會不支援呢?不過與其說這是型別轉換,倒不如說是格式化更合適。所以會在後3篇文章格式化章節在作為重中之重講述
- 一般的Converter都見名之意,但StreamConverter有何作用呢?什麼場景下會生效
- 答:本文講述
- 對於兜底的轉換器,有何含義?這種極具通用性的轉換器作用為何
- 答:本文講述
StreamConverter
用於實現集合/陣列型別到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個通用的轉換器用於兜底,你可能平時並不關注它,但它實時就在發揮著它的作用。
ObjectToObjectConverter
將源物件轉換為目標型別,非常的通用: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來看:
- part1:從快取中拿到Member,直接判斷Member的可用性,可用的話迅速返回
- part2:若part1沒有返回,就執行三部曲,嘗試找到一個合適的Member,然後放進快取內(若沒有就返回null)
part1:快速返回流程
當不是首次進入處理時,會走快速返回流程。也就是第0步isApplicable
判斷邏輯,有這幾個關注點:
- Member包括Method或者Constructor
- Method:若是static靜態方法,要求方法的第1個入參型別必須是源型別sourceType;若不是static方法,則要求源型別sourceType必須是
method.getDeclaringClass()
的子型別/相同型別 - Constructor:要求構造器的第1個入參型別必須是源型別sourceType
建立目標物件的例項,此轉換器支援兩種方式:
- 通過工廠方法/例項方法建立例項(
method.invoke(source)
) - 通過構造器建立例項(
ctor.newInstance(source)
)
以上case,在下面均會給出程式碼示例。
part2:三部曲流程
對於首次處理的轉換,就會進入到詳細的三部曲邏輯:通過反射嘗試找到合適的Member用於建立目標例項,也就是上圖的1、2、3步。
step1:determineToMethod,從sourceClass
裡找例項方法,對方法有如下要求:
- 方法名必須叫
"to" + targetClass.getSimpleName()
,如toPerson()
- 方法的訪問許可權必須是public
- 該方法的返回值必須是目標型別或其子型別
step2:determineFactoryMethod,找靜態工廠方法,對方法有如下要求:
- 方法名必須為
valueOf(sourceClass)
或者of(sourceClass)
或者from(sourceClass)
- 方法的訪問許可權必須是public
step3:determineFactoryConstructor,找構造器,對構造器有如下要求:
- 存在一個引數,且引數型別是sourceClass型別的構造器
- 構造器的訪問許可權必須是public
特別值得注意的是:此轉換器不支援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>
,然後註冊到註冊中心。至於到底選哪種合適,這就看具體應用場景嘍,本文只是多給你一種選擇
IdToEntityConverter
Id(S) --> Entity(T)。通過呼叫靜態查詢方法將實體ID兌換為實體物件。Entity裡的該查詢方法需要滿足如下條件find[EntityName]([IdType])
:
- 必須是static靜態方法
- 方法名必須為
find + entityName
。如Person類的話,那麼方法名叫findPerson
- 方法引數列表必須為1個
- 返回值型別必須是Entity型別
說明:此方法可以不必是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的。用型別轉換的領域設計思想代替了自上而下的過程程式設計思想。
FallbackObjectToStringConverter
通過簡單的呼叫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型別的轉換,原來都是交給它了
程式碼示例
略。
ObjectToOptionalConverter
將任意型別轉換為一個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會註冊大量的內建轉換器,從而支援String/數字型別轉換、集合型別轉換,這能解決協議層面的大部分轉換問題。
- 如Controller層,輸入的是JSON字串,可用自動被封裝為數字型別、集合型別等等
- 如@Value注入的是String型別,但也可以用數字、集合型別接收
對於複雜的物件 -> 物件型別的轉換,一般需要你自定義轉換器,或者參照本文的標準寫法完成轉換。總之:Spring提供的ConversionService
專注於型別轉換服務,是一個非常非常實用的API,特別是你正在做基於Spring二次開發的情況下。
當然嘍,關於ConversionService
這套機制還並未詳細介紹,如何使用?如何執行?如何擴充套件?帶著這三個問題,我們們下篇見。
✔✔✔推薦閱讀✔✔✔
【Spring型別轉換】系列:
- 1. 揭祕Spring型別轉換 - 框架設計的基石
- 2. Spring早期型別轉換,基於PropertyEditor實現
- 3. 搞定收工,PropertyEditor就到這
- 4. 上新了Spring,全新一代型別轉換機制
【Jackson】系列:
- 1. 初識Jackson -- 世界上最好的JSON庫
- 2. 媽呀,Jackson原來是這樣寫JSON的
- 3. 懂了這些,方敢在簡歷上說會用Jackson寫JSON
- 4. JSON字串是如何被解析的?JsonParser瞭解一下
- 5. JsonFactory工廠而已,還蠻有料,這是我沒想到的
- 6. 二十不惑,ObjectMapper使用也不再迷惑
- 7. Jackson用樹模型處理JSON是必備技能,不信你看
【資料校驗Bean Validation】系列:
- 1. 不吹不擂,第一篇就能提升你對Bean Validation資料校驗的認知
- 2. Bean Validation宣告式校驗方法的引數、返回值
- 3. 站在使用層面,Bean Validation這些標準介面你需要爛熟於胸
- 4. Validator校驗器的五大核心元件,一個都不能少
- 5. Bean Validation宣告式驗證四大級別:欄位、屬性、容器元素、類
- 6. 自定義容器型別元素驗證,類級別驗證(多欄位聯合驗證)
【新特性】系列:
- IntelliJ IDEA 2020.3正式釋出,年度最後一個版本很講武德
- IntelliJ IDEA 2020.2正式釋出,諸多亮點總有幾款能助你提效
- IntelliJ IDEA 2020.1正式釋出,你要的Almost都在這!
- Spring Framework 5.3.0正式釋出,在雲原生路上繼續發力
- Spring Boot 2.4.0正式釋出,全新的配置檔案載入機制(不向下相容)
- Spring改變版本號命名規則:此舉對非英語國家很友好
- JDK15正式釋出,劃時代的ZGC同時宣佈轉正
【程式人生】系列:
還有諸如【Spring配置類】【Spring-static】【Spring資料繫結】【Spring Cloud Netflix】【Feign】【Ribbon】【Hystrix】...更多原創專欄,關注BAT的烏托邦
回覆專欄
二字即可全部獲取,也可加我fsx1056342982
,交個朋友。
有些已完結,有些連載中。我是A哥(YourBatman),我們們下期再見