Statically Validated Links to Java Bean Properties

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

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

    So the inconvenience


    There is no syntax in Java to reference a bean property. It’s easier to explain with an example. Suppose there is Accountone that has a customertype property Customer, which, in turn, has a property name. In other words:

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

    And there is TableBuilderone who knows how to create plates on the interface to display a list of beans, you just need to tell him what their properties (possibly nested) we want to display, and he will already do all the routine work.

    How to say that we want to show name customer'a Account' a? Usually use string literals:

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

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

    There is another less obvious inconvenience here: typing addColumn(String)does not tell us in any way that this method expects not just any string, but a chain of properties. I would like the compiler to check everything, the environment prompted, and refactoring did 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 really is no syntactic construction to refer to a member of a class without accessing it. However, this has long prevented mock frameworks from elegantly and strictly expressing “when the method will be called ...”, as, for example, Mockito can:

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

    The method mock()creates and returns a proxy that looks like  Account, but behaves in a completely different way: it stores information about the called method in a ThreadLocal variable, which it then retrieves 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 in a ThreadLocal variable and returns the next proxy, allowing you to write chains of calls that turn into a chain of properties.

    $()It returns not a string, but an object of type  BeanPaththat represents a chain of properties in an object-oriented manner. You can navigate through the individual elements of this chain (for each element, the name and type are saved) or convert to a string we already know:

    $( 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 to  TableBuilder:

    public  ColumnBuilder addColumn(BeanPath path) {...}

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

    Usage Aspects


    Implementation through dynamic proxy imposes the following limitations. First, the “root” and non-closure properties in a chain cannot be final classes (including enum'ohm, string, j.l.Integeretc.). The framework cannot proxy them and returns null:

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

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

    Secondly, getters should be visible to the framework, that is, they should not be privateor  package-local. But the default constructor and the public constructor in general may not exist - the proxy is instantiated bypassing the constructor. Since this cannot be done in a legal way, the intrinsic proprietary for HotSpot JVM is used sun.misc.Unsafe.allocateObject(), which makes the framework intolerable to other JVMs. Ruts can and should be reused, they do not contain a state:

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

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

    public class BeanPathMagicAlias {
       public static  BeanPath 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 announced in  beanpath:

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

    Useful for using beanpath in code that expects string literals. You BeanPathcan also construct an instance manually - its behavior is completely determined by the state that is set during construction. So:

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

    This may come in handy to circumvent the limitations mentioned above (if the chain has a final class or no public getters). At the same time, the correctness of the chain remains on the conscience of the developer.

    Future plans


    Now beanpath is available only in source code. Therefore, first of all, I want to establish its full assembly and deployment in Maven Central. Then replace the use sun.misc.Unsafewith Objnesis to make the beanpath portable. Well, quite a prospect - to approach the solution of the problem from another angle: use static code generation a la JPA static metamodel.
    This option has several advantages:

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

    P. S. There is a similar functionality in  Querydsl , and it is implemented in the same way, but is strongly tied to the Querydsl infrastructure.

    Also popular now: