5. 穿過擁擠的人潮,Spring已為你製作好高級賽道
- 2020 年 12 月 22 日
- 筆記
- A哥學類型轉換, Converter, Spring Framework, 類型轉換
分享、成長,拒絕淺藏輒止。關注公眾號【BAT的烏托邦】,回復關鍵字
專欄
有Spring技術棧、中間件等小而美的原創專欄供以免費學習。本文已被 //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),咱們下期再見