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>