New annotation @Proxy

356 views
Skip to first unread message

mmce

unread,
Mar 21, 2017, 12:36:25 PM3/21/17
to Project Lombok
I really like the annotation @Data. Unfortunately if you try to build an entity relationship model with a lot of classes referencing each other, you'll get entangled with the generated hashCode(), equals() and toString() methods.
This comes down to bear and result in a StackOverflowException, if you'll use JPA.

Therefore I propose the following annotation:

@Target(value=FIELD) @Retention(value=SOURCE)
public interface @Proxy{
   String method();
   String field();
   String onObjectMethod();
   String onObjectField();
}

Declaring this annotation on a field (User user) with method="getUserId" will force hashCode(), equals() and toString()to use the return value of the method getUserId()instead of this fields value. The method getUserId()has to be declared in the class of the annotated field.
Declaring this annotation on a field (User user) with field="userId" will force hashCode(), equals() and toString()to use the value of the field userId instead of this fields value. The field userId has to be declared in the class of the annotated field.
Declaring this annotation on a field (User user) with onObjectMethod="getId" will force hashCode(), equals() and toString()to use the return value of the method getId()of User instead of this fields value. The method getUserId()has to be declared in the class of the type of the annotated field.
Declaring this annotation on a field (User user) with onObjectField="id" will force hashCode(), equals() and toString()to use the value of the field id of User instead of this fields value. The field id has to be declared in the class of the type of the annotated field.

Other names for this annotation could be @Relegate, @Delegate, @Indicate, etc. .

Reinier Zwitserloot

unread,
Mar 22, 2017, 5:44:41 AM3/22/17
to Project Lombok
This doesn't really make any sense.

* Does proxy also apply to toString? If not, that's surprising and suggests the name 'proxy' isn't suitable.
* If hashCode and equals actually use the value of the 'userId' field instead of the user field, does that mean the userId field is simply included twice, which is pointless?

You can put 'of' or 'exclude' on @ToString and @EqualsAndHashCode. That seems like the way to solve this problem. We're still thinking about an @Exclude annotation.

mmce

unread,
Mar 22, 2017, 8:11:56 AM3/22/17
to Project Lombok
On Wednesday, March 22, 2017 at 10:44:41 AM UTC+1, Reinier Zwitserloot wrote:
This doesn't really make any sense.

* Does proxy also apply to toString? If not, that's surprising and suggests the name 'proxy' isn't suitable.
* If hashCode and equals actually use the value of the 'userId' field instead of the user field, does that mean the userId field is simply included twice, which is pointless?

You can put 'of' or 'exclude' on @ToString and @EqualsAndHashCode. That seems like the way to solve this problem. We're still thinking about an @Exclude annotation

1. It's in the text. It should apply to the automatic generation of toString() for the anntotation @ToString as well as hashCode() and equals() for @EqualsAndHashCode. I'll post an example at the end to clarify my idea.
2. Your question doesn't make any sense, because if you include something instead of something else you'll not include it twice. So instead of using User user with it's hashCode(), toString() and equals() implementation, you'll use the value of a method, which should describe the user. The return value of this method will be used for hashCode(), toString() as well as for equals().
3. I know, that I can use "of" or "exclude" for via the annotation @ToString or @EqaulsAndHashCode, but in this case you'll drop a characteristic feature of the type you want to describe by hashCode(), toString() and equals(). In the above mentioned case you'll use just the id of user of type User for i.e. equals() instead of using user and eventually causing a StackOverFlowException.

mmce

unread,
Mar 22, 2017, 9:58:23 AM3/22/17
to Project Lombok
@Target(value=FIELD) @Retention(value=SOURCE)
public interface @Proxy{
String method(); // delegates toString(), hashCode() and equals() to the specified method.
String field(); // delegates toString(), hashCode() and equals() to the specified method.
String onObjectMethod(); // delegates toString(), hashCode() and equals() to the specified member method of the annotated type.
String onObjectField(); // delegates toString(), hashCode() and equals() to the specified member field of the annotated type.
boolean applyToEquals() default true; // The generated equals() method (@EqualsAndHashCode) will ignore this annotation, if it is set to false.
boolean applyToHashCode() default true; // The generated hashCode() method (@EqualsAndHashCode) will ignore this annotation, if it is set to false.
boolean applyToToString() default true; // The generated toString() method (@ToString) will ignore this annotation, if it is set to false.
}

@Data
public class User{
private long id;
private Collection<User> friends; // potential long list of Users
private String description;
// long description
}

// Variant 1 lombok
@Data
public class UserInformation{
private User user;
private String email;
...
}

// Variant 1 Vanilla Java
public class UserInformation{
private User user;
private String email;

// auto generated  getters an setters
...

// auto generated hashCode() and toString() code
...

// autogenerated equals() code (without null-reference check!)

public boolean equals(UserInformation info){ // a simpler version
boolean result = false;
result = result || this.user.equals(info.user);
result = result || this.email.equals(info.email);
return result;
}
}

// Variant 2 lombok modification
@Data
public class UserInformation{
@Proxy(method=getUserId)
private User user;
private String email;
...

private long getUserId(){
return user == null ? -1 : user.getId();
}
}

// Variant 2 Vanilla Java
public class UserInformation{
private User user;
private String email;

// auto generated  getters an setters
...

// auto generated hashCode() and toString() code
...

// autogenerated equals() code (without null-reference check!)


public boolean equals(UserInformation info){ // a simpler version
boolean result = false;
result = result || this.getUserId == user.getUserId;
result = result || this.email.equals(info.email);
return result;
}

private long getUserId(){
return user == null ? -1 : user.getId();
}
}

mmce

unread,
Mar 22, 2017, 11:04:20 AM3/22/17
to Project Lombok
Another variant would be:

@Target(value=FIELD) @Retention(value=SOURCE)
public interface @Proxy{
String lambdaSupplier(); // A String with a valid lambda expression, which matches to Supplier<?>.

boolean applyToEquals() default true; // The generated equals() method (@EqualsAndHashCode) will ignore this annotation, if it is set to false.
boolean applyToHashCode() default true; // The generated hashCode() method (@EqualsAndHashCode) will ignore this annotation, if it is set to false.
boolean applyToToString() default true; // The generated toString() method (@ToString) will ignore this annotation, if it is set to false.
}

In this case you would need to declare the @Proxy annotation like this:

@Proxy(lambdaSupplier="user::getId") // for getting the method getId from the type User of the variable user.
@Proxy(lambdaSupplier="UserInformation::getUserId") // for getting using the member method getUserId.

This proposal would have the benefit of having only one possible parameter for the annotation @Proxy instead of four. Unfortunately I don't know if it's possible to invoke an lambda expression over reflection.

Lenny Primak

unread,
Mar 22, 2017, 12:19:58 PM3/22/17
to Project Lombok
Hi, I also am running into a problem like this.
My thoughts would be just to add "of=" and "exclude=" to @Data annotation itself, 
and it would just apply to both @ToString, @EqualsAndHashCode etc.

mmce

unread,
Mar 22, 2017, 6:40:35 PM3/22/17
to Project Lombok
This won't solve the problem of the actual contract of equals() and hashCode() I want to uphold. For example a UserInformation object should only be equal, if the corresponding user is equal too, besides all other information.

Another example would be the collection friends in User. If I would like to have just the size of the collection instead of a list of all User objects and their toString() representation, I could do it like this:
@Proxy(applyToHashCode=false, applyToEquals=false, method="getFriendCount")
Collection<User> friends;

...
private String getFriendCountAsString(){
return friends == null ? "null" : "count: " + friend.size();
}

The output would be for example:
User(id=234, description=null, friends=count: 15)

Reinier Zwitserloot

unread,
Mar 22, 2017, 8:31:52 PM3/22/17
to Project Lombok
Proxy as is only makes sense if it's on a method; a method that does not simply return a field. I don't see why that would come up in non-exotic situations.

Martin Grajcar

unread,
Mar 22, 2017, 9:11:48 PM3/22/17
to project...@googlegroups.com
I find the original proposal too complicated. The last variant is rather flexibel, but it's not perfect (you'd need multiple @Proxy annotations if you wanted to change the behavior of toString and equals in a different way). I also dislike its stringly typed arguments.

My much simpler proposal is based on the observation that by default, @EqHC uses all fields and ignores all methods. Instead of @Proxy(method=getUserId), use two annotations:

@Data
public class UserInformation {
@EqHC.Exclude
private User user; 
private String email;
...

@EqHC.Include
private long getUserId(){
return user == null ? -1 : user.getId();
}
}

Just use @EqHC.Exclude to exclude a field and @EqHC.Include to include a method instead. There's no relationship between the excluded field and the method used instead, but I don't think we need one.

As the name says, @EqHC.Exclude and @EqHC.Include work for @EqHC only. There could be an analogous annotation for @ToString, and maybe even one (possibly called just Exclude/Include), which would work for both. This may be a lot of annotations, but they all are very obvious.

Martin Grajcar

unread,
Mar 22, 2017, 9:28:56 PM3/22/17
to project...@googlegroups.com
On Thu, Mar 23, 2017 at 1:31 AM, Reinier Zwitserloot <rein...@gmail.com> wrote:
Proxy as is only makes sense if it's on a method; a method that does not simply return a field. I don't see why that would come up in non-exotic situations.

When dealing with entities, cycles are commonplace, e.g., a Book has an Author who has a Collection<Book>. In order to avoid endless recursion you have to print the Author without its books. The proposed Proxy annotation offers more. Using my simpler proposal, it's possible, too

@Data
public class User {
    @EqHC.Exclude
    @ToString.Exclude
    private Collection<User> friends;

    @ToString.Include(name="friends")
    private String getFriendCountAsString() {
        return friends == null ? "null" : "count: " + friend.size();
    }
}

Note that the field gets excluded from EqHC without any replacement as it doesn't contribute to the object's identity. There is a replacement for toString, which allows to use manually written code together with generated code for all other fields.

mmce

unread,
Mar 23, 2017, 3:03:15 AM3/23/17
to Project Lombok
@Data
public class User {
    @Exclude
    private Collection<User> friends;

    @ToString.Include(name="friends")
    private String getFriendCountAsString() {
        return friends == null ? "null" : "count: " + friend.size();
    }
}

I think in this could work for me, but I would prefer a general @Exclude and @Include as shown above. Additional specific includes should be provided through @ToString.Include and @EqualsAndHashCode.Include as well as for excludes through @ToString.Exclude and @EqualsAndHashCode.Exclude.
Reply all
Reply to author
Forward
0 new messages