📜 ⬆️ ⬇️

Statically verifiable references to Java bean properties

When you use a tool for a long time and seriously, inevitably there are complaints about it - inconveniences that you put up with first, but at some point you realize that it is easier to fix it once than to suffer all the time. Good is the tool that allows you to "finish" yourself.

Java is a good tool, so one such inconvenience and how we corrected it will be discussed.

So inconvenience


There is no syntax in Java to refer to the bean property. It is easier to explain with an example. Suppose there is an Account that has a customer property of type Customer , which, in turn, has a name property. In other words:

 public class Account { public Customer getCustomer() { ... } } public class Customer { public String getName() { ... } } 

And there is a TableBuilder that can create labels on the interface to display the list of bins, you just need to tell it which properties (possibly nested) we want to display, and it will already do all the routine work.
')
How to say that we want to show the name customer 'and Account ' eh? Usually use string literals:

 TableBuilder<Account> tableBuilder = TableBuilder.of(Account.class); ... tableBuilder.addColumn("customer.name"); 

The disadvantage of this method is that the compiler does not know that it is not just a string, and cannot verify its correctness, which means that all typos will turn into errors only during execution. For the same reason, the development environment will not be able to tell us what properties Customer . And even if we check and debug everything, the first refactoring will destroy our efforts.

There is one more less obvious inconvenience: the typing addColumn(String) does not tell us that this method expects not just any string, but a chain of properties. I want the compiler to check everything, the environment prompts, but the refactoring does not break. This is not so much, considering that all the necessary information for this is already there.

Towards a solution


It would seem that the task is unsolvable: in Java, there is really no syntactic construct to refer to a class member without reference to it. However, this does not prevent mock frameworks from gracefully and strictly expressing “when the method will be called ...”, as, for example, Mockito can:

 Account account = mock(Account.class); when(account.getCustomer()).thenReturn(...); 

The mock() method creates and returns a proxy that looks like an Account , but behaves quite differently: it stores the information about the called method in a ThreadLocal variable, which it then extracts, and uses when() . You can use the same trick to solve our problem:

 Account account = root(Account.class); tableBuilder.addColumn( $( account.gertCustomer().getName() ) ); 

root() returns a proxy that stores the called methods into a ThreadLocal variable and returns the next proxy, allowing you to write call chains that turn into a property chain.

$() does not return a string, but an object of type BeanPath , which represents a chain of properties in an object-oriented form. You can navigate through the individual elements of this chain (for each element, the name and type is saved) or convert to a string already familiar to us:

 $( account.gertCustomer().getName() ).toDotDelimitedString() => "customer.name" 

$() , in addition to its main function, captures the type of chain (the last property in the chain), which means it allows you to add another drop of typing in the TableBuilder :

 public <T> ColumnBuilder<T> addColumn(BeanPath<T> path) {...} 

Here we wrote such a small framework in CUSTIS, we use it ourselves, and now we’ve posted it on GitHub .

Usage Aspects


Implementation through dynamic proxying imposes the following restrictions. First, the “root” and non-closing properties in a chain cannot be final-classes (including enum , string, jlInteger , etc.). The framework cannot proxy them and returns null :

 $( account.getCustomer().getName().length() ) // => NPX! 

Nevertheless, a property of any type can close a chain: both a final-class and a primitive (which is meaningless and impossible in the middle of the chain).

Secondly, getters should be visible for the framework, that is, should not be private or package-local . But the default constructor and the public constructor in general may not be - the proxy is instantiated bypassing the constructor. Since it cannot be done legally, the sun.misc.Unsafe.allocateObject() proprietary for HotSpot JVM is used, which makes the framework non-portable to other JVMs. “Ruths” can and should be reused, they do not contain the state:

 Account account = root(Account.class); tableBuilder.addColumn( $( account.getCustomer().getName() ) ); tableBuilder.addColumn( $( account.getNumber() ) ); tableBuilder.addColumn( $( account.getOpenDate() ) ); 

The root() and $() methods can be aliased, since these are just static methods:

 public class BeanPathMagicAlias { public static <T> BeanPath<T> path(T callChain) { return BeanPathMagic.$(callChain); } } 

You can use this to rename methods to suit your taste or to create a useful shortcut. In particular, one such has already been declared in the beanpath :

 public static String $$(Object callChain) { return $(callChain).toDotDelimitedString(); } 

It is useful to use the beanpath in code that expects string literals. An instance of a BeanPath can BeanPath be designed manually — its behavior is completely determined by the state that is set during construction. So:

 BeanPath<String> bp1 = $( account.getCustomer().getName()); BeanPath<String> bp2 = BeanPath.root(Account.class) .append("customer", Customer.class) .append("name", String.class); bp1.equals(bp2) // => true 

This can be useful to circumvent the limitations mentioned above (if there is a final class in the chain or no public getters). In this case, the correctness of the chain remains on the developer’s conscience.

Future plans


The beanpath is currently available only in source code. Therefore, first of all I want to establish its full-fledged assembly and deploy in Maven Central. Then replace the use of sun.misc.Unsafe with Objnesis to make the beanpath portable. Well, quite for the future - to approach the solution of the problem from the other side: use static code generation Ă  la JPA static metamodel.
This option has a number of advantages:

  1. Zero overhead in runtime.
  2. The ability to capture the typing "root" of the chain.
  3. In the generated classes API, you can filter out extra methods (which are not related to properties).

PS Similar functionality is in Querydsl , and it is implemented in the same way, but it is strongly tied to the Querydsl infrastructure.

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


All Articles