1 前言
在日常的開發中,我們需要使用到各種非空,非 Null 等條件判定以保證程序不出錯,因此避免不了寫出臃腫的代碼。盡管 JDK 8 提供了強大的 Stream 流,但它並不總是能滿足各種需求。
網絡上對於 PropertyMapper 類的研究甚少,寫這篇文章也是為了記下所學知識,同時也希望給大家提供一個另類的思路,以簡化日常開發。
2 Spring 與 PropertyMapper
PropertyMapper 是 Spring 框架中的工具類,廣泛應用於框架的各處,作為一個程序猿,我們不應該局限於使用框架本身,更應從里邊學到各種程序設計。
3 使用 PropertyMapper
讓我們從規定一個實體類 Container 開始:
1 /** 2 * 容器類 3 * 4 * @author pancc 5 * @version 1.0 6 */ 7 @Data 8 @Accessors(chain = true) 9 public class Container { 10 /** 11 * 容器的位置,可以為 null 或者空字符串 12 */ 13 private String location; 14 /** 15 * 容器中的所有數值,可以為 null 或者空 16 */ 17 private Collection<Integer> integers; 18 }
對於這個實體類的字段,我們需要打印 location 去空之后的結果,容器內數值的二進制值。並且這個容器在方法參數中可能為空,對於一般的寫法:
1 private void normal(@Nullable Container container) { 2 String local = ""; 3 List<String> binaries = new ArrayList<>(); 4 5 if (container != null) { 6 if (container.getLocation() != null) { 7 local = container.getLocation().trim(); 8 } 9 if (container.getIntegers() != null) { 10 container.getIntegers().stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries)); 11 } 12 } 13 14 System.out.println("local = " + local); 15 System.out.println("binaries = " + binaries); 16 }
上邊的盡管結合了 Stream ,但如果處理參數變得復雜,程序將會變得冗長不可讀。使用 PropertyMapper 可以極大地改善:
1 private void mapper(@Nullable Container container) { 2 StringBuilder local = new StringBuilder(); 3 List<String> binaries = new ArrayList<>(); 4 5 PropertyMapper mapper = PropertyMapper.get(); 6 mapper.from(container).whenNonNull().as(Container::getLocation).whenNonNull().as(String::trim).to(local::append); 7 mapper.from(container).whenNonNull().as(Container::getIntegers).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries))); 8 9 System.out.println("local = " + local.toString()); 10 System.out.println("binaries = " + binaries); 11 }
4 PropertyMapper 的核心解讀
4.1 核心
PropertyMapper 類主要有四大核心:
- Source<T> :一個存儲提供值的 Supplier<T> 與調用方法時對 Supplier 進行合法測試的 Predicate<T>;
- SourceOperator:對 Source<T> 實例進行處理的 lambda 接口,與 JDK 8 的 Function 接口不同的是它的返回值總是與入參相同;
- NullPointerExceptionSafeSupplier<T>:特殊的 Supplier,供 whenNonNull 方法調用以避免空指針(返回 null 值)
- CachingSupplier<T>:緩存 PropertyMapper 實例中的 Supplier<T> 的返回值 result (保存在上次調用的父實例中)
4.2 細節解讀
4.2.1 靜態入口方法 PropertyMapper::get
PropertyMapper 類內部維護一個靜態實例,我們一開始只能通過獲取它得到 PropertyMapper 實例。從類的構造方法來看,父實例與 SourceOperator 都不存在。
4.2.2 實例方法 PropertyMapper::from
向 PropertyMapper 傳遞初始值的方法有兩種,from(T value) 與 from(Supplier<T> supplier) ,前者實際上是后者的包裝,所以我們只看后者的實現。讓我們看一下內部的流程:
from(Supplier<T> supplier) 方法首先調用 getSource 獲得 Source<T> 實例。另,見 5.1 中的 sourceOperator 操作(需完成第 4 點的閱讀)。
getSource 首先會檢查父 PropertyMapper 是否存在,如果存在則繼承父的 Source<T> 實例(對應上邊 3 點中代碼第二次調用 mapper.from(container) 方法),否則新建一個 CachingSupplier<T> 接收 from 方法傳進來的 Supplier<T> 。可以看到此時於 Source<T> 的第二個構造參數 Predicate<T> 總是返回 true.
4.2.3 實例方法 Source::to
方法邏輯很簡單,首先從由 PropertyMapper 傳遞進來的 CachingSupplier<T> 中獲得值,然后使用內部的 Predicate<T> 對該值進行檢查,如果檢測通過則執行 Comsumer<T> 方法。
4.2.4 實例方法 Source::toInstance
與實例方法 to(Consumer<T> consumer) 相似但在判斷有所不同,實例方法 toInstance(Function<T, R> factory) 首先檢測值的合法與否,合法則進行轉換並返回轉換結果,否則拋出錯誤。實際開發中用到這個方法的場景可以說是沒有。
4.2.5 實例方法 Source::toCall
toCall(Runnable runnable) 方法的代碼不貼出,細節很簡單,當值合法,則執行參數 Runnable
4.2.6 實例方法 Source::as
as 方法首先檢查值是否合法,如果合法則對值進行轉換,非法則將值設為 null,Predicate<T> 則繼續傳遞。
4.2.7 實例方法 Source::asInt
asInt 是對 as 方法的雙重調用,參數 Function<T, R> 需要負責映射當前值 T 到 Number
4.2.8 實例方法 Source::whenNonNull
whenNonNull 方法繼承了原有實例的 Supplier<T> 字段,覆蓋了原有的 Predicate<T> 字段以供后續值判斷或者條件附加使用。
4.2.10 實例方法 Source::when
when 方法是最靈活的,它接收一個 Predicate<T> 參數,用來組合自身持有的 Predicate<T> 字段(如果有的話)。以 when 方法為基礎的 whenEqualTo,whenFalse,whenHasText,whenNot,whenTrue 都是對它的進一步包裝,因此不展開。
4.2.11 實例方法 Source::whenInstanceOf
這個方法實際上組合了 when 方法 與 as 方法,因此具有判斷與轉換的雙重方法,需要格外注意。
5 額外的補充
5.1 實例方法 PropertyMapper::alwaysApplying
alwaysApplying 用於向 PropertyMapper 添加一個 when 條件判定,這個判定在每次 from 方法中都被調用(見 4.2.2 )。alwaysApplyingWhenNonNull 則是對這個方法的包裝。
6 PropertyMapper 的副作用
- 內存/時間開銷:由於每次調用方法都返回一個新的 PropertyMapper 實例,在壓測中有明顯的性能影響 (對於附錄的代碼,常規與簡化方式的執行時間對比是 7ms:48ms)。
- 同一個對象不可復用:從上邊的例子中可以看到,對於常規的 if 判斷,后邊都可以享受到判斷結果,但是在 PropertyMapper 中一般則不可,套用則有可能影響閱讀(可看后邊的代碼)。
7 附 代碼
1 /** 2 * @author pancc 3 * @version 1.0 4 */ 5 public class PropertyTest { 6 7 8 private static Container allNullContainer; 9 private static Container container; 10 11 @BeforeAll 12 static void initContainer() { 13 allNullContainer = new Container().setLocation(null).setIntegers(null); 14 container = new Container().setLocation(" xs").setIntegers(IntStream.rangeClosed(1, 10).boxed().collect(Collectors.toList())); 15 } 16 17 @Test 18 public void testNormal() { 19 normal(allNullContainer); 20 normal(container); 21 } 22 23 @Test 24 void testMapper() { 25 mapper(allNullContainer); 26 mapper(container); 27 } 28 29 private void normal(@Nullable Container container) { 30 String local = ""; 31 List<String> binaries = new ArrayList<>(); 32 33 if (container != null) { 34 if (container.getLocation() != null) { 35 local = container.getLocation().trim(); 36 } 37 if (container.getIntegers() != null) { 38 container.getIntegers().stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries)); 39 } 40 } 41 42 System.out.println("local = " + local); 43 System.out.println("binaries = " + binaries); 44 } 45 46 private void mapper(@Nullable Container container) { 47 StringBuilder local = new StringBuilder(); 48 List<String> binaries = new ArrayList<>(); 49 50 PropertyMapper mapper = PropertyMapper.get(); 51 52 /* mapper.from(container).whenNonNull().to(c -> { 53 mapper.from(c.getLocation()).whenNonNull().as(String::trim).to(local::append); 54 mapper.from(c.getIntegers()).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries))); 55 });*/ 56 mapper.from(container).whenNonNull().as(Container::getLocation).whenNonNull().as(String::trim).to(local::append); 57 mapper.from(container).whenNonNull().as(Container::getIntegers).whenNonNull().to(is -> is.stream().map(Integer::toBinaryString).collect(Collectors.toCollection(() -> binaries))); 58 59 System.out.println("local = " + local.toString()); 60 System.out.println("binaries = " + binaries); 61 } 62 }
CachingSupplier<T>