使用Java 8方法参考模拟属性文字


译者的话:Java中缺少nameOf运算符的冒犯促使我翻译这篇文章。 对于不耐烦的人-在文章结尾,在源代码和二进制文件中都有一个现成的实现。

Java库开发人员经常缺少的一件事是属性文字。 在本文中,我将展示如何创造性地使用Java 8中的方法参考来通过字节码生成来模拟属性文字。

类似于类文字(例如Customer.class ),属性文字使引用类型安全的Bean类的属性成为可能。 这对于设计需要对属性执行操作或以某种方式配置它们的API很有用。

来自译者:在削减的基础上,我们分析了如何通过即兴手段实现这一目标。

例如,考虑一下Hibernate Search中的索引映射配置API:

 new SearchMapping().entity(Address.class) .indexed() .property("city", ElementType.METHOD) .field(); 

或Bean验证API中的validateValue()方法,该方法使您可以对照属性限制检查值:

 Set<ConstraintViolation<Address>> violations = validator.validateValue(Address.class, "city", "Purbeck" ); 

在这两种情况下,都使用String类型来引用Address对象的city属性。

这可能导致错误:
  • 地址类可能根本没有city属性。 或者,有人在重构时重命名get / set方法后可能会忘记更新属性的字符串名称。
  • 对于validateValue() ,我们无法验证传递的值的类型与属性的类型匹配。

使用此API的用户只能通过启动应用程序来了解这些问题。 如果编译器和类型系统从一开始就禁止这种用法,那会很酷吗? 如果Java具有属性文字,那么我们可以这样做(此代码无法编译):

 mapping.entity(Address.class) .indexed() .property(Address::city, ElementType.METHOD ) .field(); 

并且:

 validator.validateValue(Address.class, Address::city, "Purbeck"); 

我们可以避免上面提到的问题:属性名称中的任何错字都会导致编译错误,可以在您的IDE中直接注意到该错误。 这将使我们能够设计Hibernate Search配置API,以便在配置Address实体时,它仅接受Address类的属性。 对于Bean Validation validateValue()属性文字将有助于确保我们传递的是正确类型的值。

Java 8方法参考


Java 8不支持属性文字(在Java 11中不打算支持它们),但是同时,它提供了一种有趣的方式来模拟它们:方法参考(方法参考)。 最初,添加了方法参考以简化lambda表达式的使用,但是它们可以用作穷人的属性文字。

考虑使用对getter方法的引用作为属性文字的想法:

 validator.validateValue(Address.class, Address::getCity, "Purbeck"); 

显然,这只有在有吸气剂的情况下才有效。 但是,如果您的类已经遵循JavaBeans约定(通常是这种情况),那很好。

validateValue()方法的声明是什么样的? 关键是新Function类型的使用:

 public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value); 

使用两个键入参数,我们可以验证bin类型,属性和传递的值正确。 从API的角度来看,我们满足了我们的需求:使用安全,IDE甚至会自动补充以Address::开头的方法名称。 但是,如何在validateValue()方法的实现中从Function对象派生属性名称?

然后,有趣的事情开始了,因为Function功能接口仅声明了一个方法apply() ,该方法为传递的T实例执行功能代码。 这似乎不是我们所需要的。

ByteBuddy进行救援


事实证明,诀窍在于应用该功能! 通过创建类型T的代理实例,我们的目标是调用该方法并在Proxy调用处理程序中获取其名称。 (来自翻译者:以下我们谈论动态Java代理-java.lang.reflect.Proxy)。

Java开箱即用地支持动态代理,但是这种支持仅限于接口。 由于我们的API应该可以与任何bean(包括实际类)一起使用,因此我将使用一个很棒的工具ByteBuddy代替Proxy。 ByteBuddy提供了一个简单的DSL,可以快速创建类,这正是我们所需要的。

让我们从定义一个接口开始,该接口允许我们存储和检索从“方法参考”中提取的属性名称。

 public interface PropertyNameCapturer { String getPropertyName(); void setPropertyName(String propertyName); } 

现在,我们使用ByteBuddy以编程方式创建与我们感兴趣的类型兼容的代理类(例如:Address),并实现PropertyNameCapturer

 public <T> T /* & PropertyNameCapturer */ getPropertyNameCapturer(Class<T> type) { DynamicType.Builder<?> builder = new ByteBuddy() (1) .subclass( type.isInterface() ? Object.class : type ); if (type.isInterface()) { (2) builder = builder.implement(type); } Class<?> proxyType = builder .implement(PropertyNameCapturer.class) (3) .defineField("propertyName", String.class, Visibility.PRIVATE) .method( ElementMatchers.any()) (4) .intercept(MethodDelegation.to( PropertyNameCapturingInterceptor.class )) .method(named("setPropertyName").or(named("getPropertyName"))) (5) .intercept(FieldAccessor.ofBeanProperty()) .make() .load( (6) PropertyNameCapturer.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER ) .getLoaded(); try { @SuppressWarnings("unchecked") Class<T> typed = (Class<T>) proxyType; return typed.newInstance(); (7) } catch (InstantiationException | IllegalAccessException e) { throw new HibernateException( "Couldn't instantiate proxy for method name retrieval", e ); } } 

该代码可能看起来有些混乱,所以让我解释一下。 首先,我们获得ByteBuddy(1)的实例,它是DSL的入口点。 它用于创建扩展所需类型(如果是类)或继承Object并实现所需类型(如果是接口)的动态类型(2)。

然后,我们指示该类型实现了PropertyNameCapturer接口,并添加了一个字段来存储所需属性的名称(3)。 然后,我们说对所有方法的调用应由PropertyNameCapturingInterceptor(4)拦截。 只有setPropertyName()和getPropertyName()(来自PropertyNameCapturer接口)才能访问之前创建的实际属性(5)。 最后,创建,加载(6)和实例化(7)该类。

这就是创建代理类型所需要的全部,感谢ByteBuddy,这可以通过几行代码来完成。 现在让我们看一下呼叫拦截器:

 public class PropertyNameCapturingInterceptor { @RuntimeType public static Object intercept(@This PropertyNameCapturer capturer, @Origin Method method) { (1) capturer.setPropertyName(getPropertyName(method)); (2) if (method.getReturnType() == byte.class) { (3) return (byte) 0; } else if ( ... ) { } // ... handle all primitve types // ... } else { return null; } } private static String getPropertyName(Method method) { (4) final boolean hasGetterSignature = method.getParameterTypes().length == 0 && method.getReturnType() != null; String name = method.getName(); String propName = null; if (hasGetterSignature) { if (name.startsWith("get") && hasGetterSignature) { propName = name.substring(3, 4).toLowerCase() + name.substring(4); } else if (name.startsWith("is") && hasGetterSignature) { propName = name.substring(2, 3).toLowerCase() + name.substring(3); } } else { throw new HibernateException( "Only property getter methods are expected to be passed"); (5) } return propName; } } 

Intercept()方法接受被调用的Method和该调用的目标(1)。 @Origin@This用于指定适当的参数,以便ByteBuddy可以在动态代理中生成正确的intercept()调用。

请注意,由于ByteBuddy仅用于创建动态代理,而不用于创建动态代理,因此对ByteBuddy类型没有严格的依赖关系。

通过调用getPropertyName() (4),我们可以获得与传递的方法引用相对应的属性名称,并将其保存在PropertyNameCapturer (2)中。 如果该方法不是获取方法,则代码将引发异常(5)。 getter的返回类型无关紧要,因此考虑到属性(3)的类型,我们返回null。

现在我们准备在validateValue()方法中获取属性名称:

 public <T, P> Set<ConstraintViolation<T>> validateValue( Class<T> type, Function<? super T, P> property, P value) { T capturer = getPropertyNameCapturer(type); property.apply(capturer); String propertyName = ((PropertyLiteralCapturer) capturer).getPropertyName(); //      } 

将函数应用于创建的代理后,我们将类型转换为PropertyNameCapturer并从Method中获取名称。

因此,使用一些生成字节码的方法,我们使用了Java 8中的“方法参考”来模拟属性文字。

当然,如果我们在语言中使用不动产字面量,那么我们所有人都会过得更好。 我什至允许使用私有属性,并且可能可以从批注中引用属性。 不动产字面量会比较整洁(不带“ get”前缀),并且看起来不会像黑客一样。

来自翻译


值得注意的是,其他好的语言已经支持(或几乎)类似的机制:


如果您突然将Lombok项目与Java一起使用,则会为其编写一个字节码编译时间生成器

受到本文所述方法的启发,谦虚的仆人将一个小型库放在一起,该库实现了Java 8的nameOfProperty():

源代码
二进制文件

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


All Articles