Emulate Property Literals with Java 8 Method Reference
- Transfer
From the translator: to the translation of this article I was pushed offended by the absence of the nameOf operator in the Java language. For the impatient - at the end of the article there is a ready implementation in source codes and binaries.
One of the things that Java developers often lack in Java is property literals. In this post, I will show how you can creatively use the Method Reference from Java 8 to emulate property literals using bytecode generation.
Akin to class literals (for example,
Customer.class
), property literals would allow to refer to properties of bin classes as type safe. This would be useful for an API design where there is a need to perform actions on properties or to configure them in some way. From the translator: Under the cut, we analyze how to implement it from improvised means.
For example, consider the index mapping configuration API in Hibernate Search:
new SearchMapping().entity(Address.class)
.indexed()
.property("city", ElementType.METHOD)
.field();
Alternatively, a method
validateValue()
from the Bean Validation API that allows you to check the value of the property constraints:Set<ConstraintViolation<Address>> violations =
validator.validateValue(Address.class, "city", "Purbeck" );
In both cases, a type is used to refer to the property of an
city
object .
This can lead to errors:Address
String
- The Address class may not have properties at all
city
. Or someone may forget to update the string property name after renaming the get / set methods when refactoring. - in the case
validateValue()
we have no way to make sure that the type of the passed value corresponds to the type of the property.
Users of this API can learn about these problems only by running the application. Wouldn't it be cool if the compiler and type system prevented such use from the very beginning? If there were property literals in Java, then we could do this (this code does not compile):
mapping.entity(Address.class)
.indexed()
.property(Address::city, ElementType.METHOD )
.field();
AND:
validator.validateValue(Address.class, Address::city, "Purbeck");
We could avoid the problems mentioned above: any slip in the name of the property would lead to a compilation error that can be noticed right in your IDE. This would allow the Hibernate Search Configuration API to be developed to accept only the properties of the Address class when we configure the Address entity. And in the case of c Bean Validation,
validateValue()
property literals would help ensure that we are passing the value of the correct type.Java 8 Method Reference
Java 8 does not support property literals (and is not planned to support them in Java 11), but at the same time it provides an interesting way to emulate them: Method Reference (method reference). Initially, Method Reference was added to simplify working with lambda expressions, but they can be used as property literals for the poor.
Consider the idea of using a reference to a getter method as a literal property:
validator.validateValue(Address.class, Address::getCity, "Purbeck");
Obviously, this will work only if you have a getter. But if your classes already follow the JavaBeans convention, which is most often the case, this is normal.
What would a method declaration look like
validateValue()
? The key point is the use of a new type Function
:public <T, P> Set<ConstraintViolation<T>> validateValue(
Class<T> type,
Function<? super T, P> property,
P value);
Using two typing parameters we can verify that the bean type, properties, and the value passed are correct. From the point of view of the API, we got what we needed: it is safe to use and the IDE will even automatically complement method names beginning with
Address::
. But how to display the name of the property from the object Function
in the implementation of the method validateValue()
? And then the fun begins, because the functional interface
Function
only declares one method - apply()
that executes the function code for the transmitted instance T
. This is not what we wanted.ByteBuddy to the rescue
As it turns out, in the application of the function and consists the trick! Creating a proxy instance of type T, we have a goal to call the method and get its name in the proxy call handler. (From the translator: hereinafter we are talking about dynamic proxy Java - java.lang.reflect.Proxy).
Java supports dynamic proxies out of the box, but this support is limited only by interfaces. Since our API should work with any beans, including real classes, I am going to use an excellent tool instead of Proxy - ByteBuddy. ByteBuddy provides a simple DSL for creating classes on the fly, which is what we need.
Let's begin by defining an interface that would allow storing and retrieving the name of a property extracted from the Method Reference.
publicinterfacePropertyNameCapturer{
String getPropertyName();
voidsetPropertyName(String propertyName);
}
Now we will use ByteBuddy to programmatically create proxy classes that are compatible with the types of interest to us (for example, Address) and implement
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) {
thrownew HibernateException(
"Couldn't instantiate proxy for method name retrieval", e
);
}
}
The code may seem a bit confusing, so let me explain it. First we get an instance of ByteBuddy (1), which is the DSL input point. It is used to create dynamic types that either extend the desired type (if it is a class) or inherit Object and implement the desired type (if it is an interface) (2).
Then, we specify that the type implements the PropertyNameCapturer interface and add a field to store the name of the desired property (3). Then we say that calls of all methods should be intercepted by PropertyNameCapturingInterceptor (4). Only setPropertyName () and getPropertyName () (from the PropertyNameCapturer interface) should access the real property created earlier (5). Finally, a class is created, loaded (6), and instantiated (7).
This is all we need to create proxy types, thanks ByteBuddy, this can be done in a few lines of code. Now, let's look at call interceptor:
publicclassPropertyNameCapturingInterceptor{
@RuntimeTypepublicstatic Object intercept(@This PropertyNameCapturer capturer,
@Origin Method method){ (1)
capturer.setPropertyName(getPropertyName(method)); (2)
if (method.getReturnType() == byte.class) { (3)
return (byte) 0;
}
elseif ( ... ) { } // ... handle all primitve types// ...
}
else {
returnnull;
}
}
privatestatic String getPropertyName(Method method){ (4)
finalboolean 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);
}
elseif (name.startsWith("is") && hasGetterSignature) {
propName = name.substring(2, 3).toLowerCase() + name.substring(3);
}
}
else {
thrownew HibernateException(
"Only property getter methods are expected to be passed"); (5)
}
return propName;
}
}
The intercept () method accepts the called Method and the target for the call (1). Annotations
@Origin
and @This
are used to specify the appropriate parameters so that ByteBuddy can generate the correct intercept () calls in a dynamic proxy. Notice that there is no strict dependence of the inteptor on ByteBuddy types, since ByteBuddy is used only to create a dynamic proxy, but not when using it.
Calling
getPropertyName()
(4) we can get the property name corresponding to the passed Method Reference and save it in PropertyNameCapturer
(2). If the method is not a getter, then the code throws an exception (5). The return type of the getter does not matter, so we return null based on the type of property (3).Now we are ready to get the name of the property in the method
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();
// здесь запускам саму валидацию значения
}
After applying the function to the created proxy, we cast the type to PropertyNameCapturer and get the name from Method.
So using a bit of bytecode generation magic, we used the Java 8 Method Reference to emulate property literals.
Of course, if we had real literals of properties in a language, we would all be better off. I would even allow working with private properties and, probably, properties could be referenced from annotations. Real property literals would be more accurate (without the “get” prefix) and would not look like a hack.
From translator
It is worth noting that other good languages already support (or almost) a similar mechanism:
- C # - nameOf operator
- Groovy and Scala - there are famous hacks with metaprogramming and macros
- Kotlin - there is a normal syntax
User::login.name
If you suddenly use the Lombok c Java project, then the compile-time bytecode is written for it .
Inspired by the approach described in the article, your humble servant compiled a small library that implements nameOfProperty () for Java 8: Binary
sources