📜 ⬆️ ⬇️

Emulate Property Literals with Java 8 Method Reference


From the translator: to the translation of this article I was pushed by the insult from the lack 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 library developers often lack 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 make it possible to refer to the 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: 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, the validateValue() method from the Bean Validation API allows you to check the value of the property constraints:

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

In both cases, the String type is used to refer to the city property of the Address object.

This can lead to errors:

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, 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, which can be seen 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 pass 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 validateValue() method declaration look like? The key point is the use of the new Function type:

 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 supplement method names beginning with Address:: . But how to display the property name from the Function object in the implementation of the validateValue() method?

And then the fun begins, since the Function functional interface only declares one method — apply() , which executes the function code for the passed-in instance T This is not what we needed.

ByteBuddy to the rescue


As it turns out, the application of the function is the trick! By 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 Java proxies - 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 bin, 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 start by defining an interface that would allow storing and retrieving the property name extracted from the Method Reference.

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

Now we 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) { throw new 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:

 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; } } 

The intercept () method accepts the called Method and the target for the call (1). The @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.

By calling getPropertyName() (4) we can get the name of the property corresponding to the given 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 have everything ready to get the name of the property in the validateValue() method:

 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 you to work with private properties and, probably, you could refer to properties 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:


If you suddenly use the Lombok Java project with c, then the compile-time generator 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:

Sources
Binary

Source: https://habr.com/ru/post/420533/


All Articles