库开发人员经常在Java中错过的一件事就是属性字面量。在这篇文章中,我将展示如何通过一些字节码生成,利用Java 8方法引用创造性地模拟属性字面量。
类似于类字面量(例如 Customer.class
),属性字面量将允许以类型安全的方式引用bean类中的属性。这对于设计执行特定bean属性的操作或对其应用某些配置方式的API非常有用。例如,考虑Hibernate Search中对索引映射进行程序性配置的API
new SearchMapping().entity( Address.class )
.indexed()
.property( "city", ElementType.METHOD )
.field();
或者Bean Validation API中的validateValue()
方法,用于验证单个属性的约束条件
Set<ConstraintViolation<Address>> violations = validator.validateValue( Address.class, "city", "Purbeck" );
在这两种情况下,都使用一个字符串来表示Address
类的city
属性。
这在多个层面上都是容易出错的
-
Address
类可能根本不存在一个名为city
的属性。或者,在重命名属性时,可能会忘记更新这个字符串引用。 -
在
validateValue()
的情况下,没有方法可以确保传递的值实际上满足city
属性的类型。
API的用户只能在应用程序实际运行时发现这些问题。如果编译器和语言的类型系统从一开始就阻止了这种错误的使用,那岂不是更好?如果Java有属性字面量,那就正是你将得到的结果(发明语法,这不会编译!)
mapping.entity( Address.class )
.indexed()
.property( Address::city, ElementType.METHOD )
.field();
并且
validator.validateValue( Address.class, Address::city, "Purbeck" );
上述提到的问题可以避免:在属性字面量中有一个拼写错误会导致编译错误,你会在你的IDE中立即注意到。这将允许以只接受配置Address
实体的属性的方式设计Hibernate Search中的配置API。在Bean Validation的validateValue()
中,字面量可以帮助确保只能传递可以分配给所讨论属性类型的值。
Java 8方法引用
尽管Java 8没有真正的属性字面量(而且它们的引入也没有计划在Java 9中进行),但它提供了一种有趣的方法来在一定程度上模拟它们:[方法引用](https://docs.oracle.com/javase/tutorial/java/javaOO/methodreferences.html)。在引入以提高使用Lambda表达式时的开发者体验后,方法引用也可以作为穷人的属性字面量。
将getter方法的引用视为属性字面量
validator.validateValue( Address.class, Address::getCity, "Purbeck" );
显然,这只有在实际存在getter的情况下才会工作。但如果你的类遵循JavaBeans约定——这通常是情况——那就没问题。
现在,validateValue()
方法的定义是什么样的?关键是使用新的Function
类型
public <T, P> Set<ConstraintViolation<T>> validateValue(Class<T> type, Function<? super T, P> property, P value);
通过使用两个类型参数,我们确保bean类型、属性以及传递给属性的值都正确匹配。从API的角度来看,我们得到了我们需要的:它是安全的,IDE甚至会在开始编写Address::
后自动完成方法名称。但在validateValue()
的实现中,我们如何从Function
中推导出属性名称呢?
这很有趣,因为Function
接口仅定义了一个方法,apply()
,它将函数应用于T
的给定实例。这似乎并不特别有帮助,对吗?
ByteBuddy来救命
实际上,应用这个函数是有效的!通过创建一个T
类型的代理实例,我们有一个可以调用方法的目标,并且可以在代理的方法调用处理器中获取其名称。
Java自带对动态代理的支持,但这仅限于代理接口。由于我们的API应该与任何类型的bean一起工作,包括实际类,我将使用一个叫做ByteBuddy的巧妙工具。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) 中。如果给定方法不表示 getter 方法,则会引发异常 (5)。调用的 getter 的返回值无关紧要,因此我们只需确保返回一个与属性类型匹配的合理的“null值” (3)。
有了这些,我们就已经准备好了获取传递给 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();
// perform validation of the property value...
}
当将函数应用于属性名称捕获代理时,拦截器会启动,从 Method
对象中获取属性名称并将其存储在捕获器实例中,最终可以从该实例中检索到。
就这样,一些字节码魔术让我们能够创造性地使用 Java 8 方法引用来模拟属性文字。
尽管如此,将真正的属性文字作为语言的一部分(让我们幻想一下,也许是在 Java 10?)仍然非常有好处。它将允许处理私有属性,并且有望从注解中引用属性文字。真正的属性文字也将更加简洁(没有“get”前缀),整体上感觉不那么像黑客行为 ;)