我们使用lambdas使用静态链接指向对象属性

历史上曾经发生过这样的情况:在Java中,对于对象的属性(属性),没有提供物理实体。 Java中的属性是命名字段和它们的访问方法(访问器)中的一些约定。 而且,尽管该语言中物理属性的存在可以简化许多情况(从笨拙的getter-setter方法开始),但在不久的将来,Java的情况似乎不会改变。


但是,在开发多层业务应用程序并使用各种框架来映射和绑定数据时,通常需要将引用传递给对象属性。 让我们考虑一下这有什么选择。


使用属性名称


到目前为止,引用对象属性的唯一公认方法是使用带有名称的字符串。 基础库使用反射或自省来搜索访问器方法和访问字段。 要引用嵌套对象,通常使用以下表示法:


person.contact.address.city 

此方法的问题在于,对于属性的名称和类型的拼写缺乏任何控制,其含义是:


  • 在编译阶段没有错误控制。 您可以在名称中输入错误,可以将其应用于错误的类,属性的类型不受控制。 我们还必须编写非常愚蠢的测试。
  • IDE不提供支持。 应付200多个字段时非常累。 最好是六月可以关闭。
  • 复杂的代码重构。 更改字段名称,很多事情一下子就会消失。 好的IDE还会带出数百个出现类似单词的地方。
  • 支持和代码分析。 我们想查看该属性的使用位置,但是“查找用法”将不会显示该字符串。

因此,我们仍然希望有一个静态的类型安全属性引用。 吸气剂是此角色的最佳人选,因为:


  • 绑定到特定类别
  • 包含属性的名称。
  • 有类型

我如何指称吸气剂?


代理


有趣的方法之一是代理(或弄湿)对象以拦截getter调用链,该链在某些库中使用: MockitoQueryDSLBeanPath 。 关于哈布雷的最后一篇文章 ,作者发表了一篇文章
这个想法很简单,但是实现起来并不容易(提到的文章中的一个例子)。


 Account account = root(Account.class); tableBuilder.addColumn( $( account.getCustomer().getName() ) ); 

使用动态代码生成,将创建一个特殊的代理类,该类从Bean类继承并拦截链中的所有getter调用,从而在ThreadLocal变量中构造路径。 在这种情况下,不会发生对象的这些吸气剂的调用。


在本文中,我们将考虑一种替代方法。


方法链接


随着Java 8的到来,lambdas和使用方法引用的功能也应运而生。 因此,具有以下内容是很自然的:


 Person person = … assertEquals("name", $(Person::getName).getPath()); 

$方法接受以下lambda,在其中传递getter引用:


 public interface MethodReferenceLambda<BEAN, TYPE> extends Function<BEAN, TYPE>, Serializable {} ... public static <BEAN, TYPE> BeanProperty<BEAN, TYPE> $(MethodReferenceLambda<BEAN, TYPE> methodReferenceLambda) 

问题在于,由于类型擦除,无法在运行时获取BEAN和TYPE类型,也没有关于吸气剂名称的信息:被称为“外部”的方法为Function.apply()。


不过,有一个技巧-这是使用序列化的lambda。


 MethodReferenceLambda<Person,String> lambda = Person::getName(); Method writeMethod = lambda.getClass().getDeclaredMethod("writeReplace"); writeMethod.setAccessible(true); SerializedLambda serLambda = (SerializedLambda) writeMethod.invoke(lambda); String className = serLambda.getImplClass().replaceAll("/", "."); String methodName = serLambda.getImplMethodName(); 

SerializedLambda类包含有关被调用类和方法的所有必要信息。 接下来是技术问题。
由于我经常处理数据结构,因此该方法鼓励我编写一个小的库以静态访问属性。


BeanRef库


使用该库看起来像这样:


 Person person = ... //     final BeanPath<Person, String> personCityProperty = $(Person::getContact).$(Contact::getAddress).$(Address::getCity); assertEquals("contact.address.city", personCityProperty.getPath()); 

并且不需要代码生成和第三方依赖的魔力。 代替getter链,使用了lambda链来引用getter。 同时,尊重类型安全性,并且基于IDE的自动补全效果很好:


您可以在标准符号(getXXX()/ isXXX())和非标准(xxx())中使用吸气剂名称。 该库将尝试查找相应的setter,如果不存在,则将该属性声明为只读。


为了提高性能,缓存了已解析的属性,当您使用相同的lambda再次调用它时,结果已经保存。


除了属性/路径名之外,使用BeanPath对象,您还可以访问对象的属性值:


 Person person = ... final BeanPath<Person, String> personCityProperty = $(Person::getContact).$(Contact::getAddress).$(Address::getCity); String personCity = personCityProperty.get(person); 

此外,如果链中的中间对象为null,则相应的调用也将返回null而不是NPE。 这将大大简化代码,而无需验证。


通过BeanPath,如果对象属性不是只读的,还可以更改它的值:


 personCityProperty.set(person, “Madrid”); 

按照相同的想法-尽可能少的NPE-在这种情况下,如果链中的中间对象之一为空,则库将尝试自动创建它并将其保存在字段中。 为此,相应的属性必须是可写的,并且对象类必须具有不带参数的公共构造函数。


作为实验功能,提供了使用集合的机会。 对于某些特殊情况,有时有必要构造路径,以引用集合中的对象。 为此,提供了$$方法,该方法构造到集合的最后一个元素的链接(将其视为唯一一个)。


 final BeanPath<Person, String> personPhonePath = $(Person::getContact).$$(Contact::getPhoneList).$(Phone::getPhone); assertEquals("contact.phoneList.phone", personPhonePath.getPath()); assertEquals(personPhonePath.get(person), person.getContact().getPhoneList() .get(person.getContact().getPhoneList().size()-1).getPhone()); 

该项目位于以下位置: https : //github.com/throwable/beanref ,可从jcenter maven存储库获得二进制文件。


有用的


java.beans.Introspector
标准Java Java中的Introspector类允许您解析bin属性。


Apache Commons BeanUtils
用于Java Bean的最全面的库。


豆径
提到通过代理执行相同操作的库。


肥胖症
我们使用任何构造函数集实例化任何类的对象。


QueryDSL别名
使用代理类在QueryDSL中设置条件


进q
一个有趣的库,它使用lambda来设置JPA中的条件。 很多魔术:代理,序列化lambda,解释字节码。

Source: https://habr.com/ru/post/zh-CN469181/


All Articles