译者的话: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 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 ( ... ) { }
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():
源代码二进制文件