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() { ... } }
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.name customer
'and Account
' eh? Usually use string literals: TableBuilder<Account> tableBuilder = TableBuilder.of(Account.class); ... tableBuilder.addColumn("customer.name");
Customer
. And even if we check and debug everything, the first refactoring will destroy our efforts.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. Account account = mock(Account.class); when(account.getCustomer()).thenReturn(...);
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) {...}
enum
, string, jlInteger
, etc.). The framework cannot proxy them and returns null
: $( account.getCustomer().getName().length() ) // => NPX!
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() ) );
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); } }
beanpath
: public static String $$(Object callChain) { return $(callChain).toDotDelimitedString(); }
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
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.Source: https://habr.com/ru/post/243803/
All Articles