Annotation driven equals and hashCode
The following implementation of equals, hashcode and toString is using the concept of one or more business keys defined by annotations.The annotation @BusinessKey can be applied with an include/exclude filter on field or method level.
Enumeration for include/exclude filter:
public enum Method { ALL, NONE, EQUALS, HASH_CODE, TO_STRING }
Business key annotation:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target( { ElementType.FIELD, ElementType.METHOD }) public @interface BusinessKey { Method[] include() default Method.ALL; Method[] exclude() default Method.NONE; }
Implementation of equals, hashCode and toString using cached reflection:
import java.lang.reflect.AccessibleObject; import java.lang.reflect.Field; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; import java.util.Hashtable; import java.util.List; import java.util.Map; import org.apache.commons.lang.builder.EqualsBuilder; import org.apache.commons.lang.builder.HashCodeBuilder; import org.apache.commons.lang.builder.ToStringBuilder; import org.apache.commons.lang.builder.ToStringStyle; public class BeanUtils { private static Map<String, List<AccessibleObject>> cache = new Hashtable<String, List<AccessibleObject>>(); private BeanUtils() {} public static boolean equals(Object obj1, Object obj2) { if (obj1 == obj2) { return true; } if (obj2 == null || obj2.getClass() != obj1.getClass()) { return false; } EqualsBuilder builder = new EqualsBuilder(); for (AccessibleObject ao : getAccessibleObjects(obj1, 1)) { try { if (ao instanceof Field) { builder.append(((Field) ao).get(obj1), ((Field) ao).get(obj2)); } else { builder.append(((java.lang.reflect.Method) ao).invoke(obj1, (Object[]) null), ((java.lang.reflect.Method) ao).invoke(obj2, (Object[]) null)); } } catch (Exception e) {} } return builder.isEquals(); } public static int hashCode(Object obj) { HashCodeBuilder builder = new HashCodeBuilder(); for (AccessibleObject ao : getAccessibleObjects(obj, 2)) { try { if (ao instanceof Field) { builder.append(((Field) ao).get(obj)); } else { builder.append(((java.lang.reflect.Method) ao).invoke(obj, (Object[]) null)); } } catch (Exception e) {} } return builder.toHashCode(); } public static String toString(Object obj) { ToStringBuilder builder = new ToStringBuilder(obj, ToStringStyle.SHORT_PREFIX_STYLE); for (AccessibleObject ao : getAccessibleObjects(obj, 4)) { try { if (ao instanceof Field) { builder.append(((Field) ao).getName(), ((Field) ao).get(obj)); } else { builder.append(((java.lang.reflect.Method) ao).getName(), ((java.lang.reflect.Method) ao).invoke(obj, (Object[]) null)); } } catch (Exception e) {} } return builder.toString(); } private static List<AccessibleObject> getAccessibleObjects(Object obj, int filter) { Class<?> clazz = obj.getClass(); String name = clazz.getName() + filter; if (!cache.containsKey(name)) { List<AccessibleObject> aos = new ArrayList<AccessibleObject>(); do { Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { BusinessKey bk = field.getAnnotation(BusinessKey.class); if (bk != null && (filter(bk) & filter) == filter) { field.setAccessible(true); aos.add(field); } } java.lang.reflect.Method[] methods = clazz.getDeclaredMethods(); for (java.lang.reflect.Method method : methods) { BusinessKey bk = method.getAnnotation(BusinessKey.class); if (bk != null && (filter(bk) & filter) == filter) { method.setAccessible(true); aos.add(method); } } clazz = clazz.getSuperclass(); } while (clazz != null); Collections.sort(aos, new AccessibleObjectComparator()); cache.put(name, aos); } return cache.get(name); } private static int filter(BusinessKey bk) { int filter = 0; for (Method method : bk.include()) { switch (method) { case ALL: filter = filter | 7; break; case EQUALS: filter = filter | 1; break; case HASH_CODE: filter = filter | 2; break; case TO_STRING: filter = filter | 4; break; } } for (Method method : bk.exclude()) { switch (method) { case ALL: filter -= filter & 7; break; case EQUALS: filter -= filter & 1; break; case HASH_CODE: filter -= filter & 2; break; case TO_STRING: filter -= filter & 4; break; } } return filter; } private static class AccessibleObjectComparator implements Comparator<AccessibleObject> { public int compare(AccessibleObject o1, AccessibleObject o2) { boolean o1IsField = o1 instanceof Field; boolean o2IsField = o2 instanceof Field; if (!o1IsField && o2IsField) { return 1; } else if (o1IsField && !o2IsField) { return -1; } if (o1IsField) { return ((Field) o1).getName().compareTo(((Field) o2).getName()); } else { return ((java.lang.reflect.Method) o1).getName() .compareTo(((java.lang.reflect.Method) o2).getName()); } } } }
Example of usage in a JPA annotated bean:
@Entity public class User { private Long id; private String username; private byte[] password; private Set<Role> roles = new TreeSet<Role>(); protected User() {} public User(String username, String password) { this.username = username; setPassword(password); } @BusinessKey(include = Method.TO_STRING) @Id @GeneratedValue public Long getId() { return id; } protected void setId(Long id) { this.id = id; } @BusinessKey @Column(nullable = false, unique = true) public String getUsername() { return username; } public User setUsername(String username) { this.username = username; return this; } @BusinessKey @Column(length = 32, nullable = false) public byte[] getPassword() { return password; } public void setPassword(String password) { try { MessageDigest md = MessageDigest.getInstance("SHA-256"); this.password = md.digest(password.getBytes()); } catch (NoSuchAlgorithmException e) {} } @ManyToMany public Set<Role> getRoles() { return roles; } public void setRoles(Set<Role> roles) { this.roles = roles; } @Override public boolean equals(Object obj) { return BeanUtils.equals(this, obj); } @Override public int hashCode() { return BeanUtils.hashCode(this); } @Override public String toString() { return BeanUtils.toString(this); } }
If you don't mind to use a superclass in your beans the following superclass can be added as a convenience:
public abstract class Bean { @Override public boolean equals(Object obj) { return BeanUtils.equals(this, obj); } @Override public int hashCode() { return BeanUtils.hashCode(this); } @Override public String toString() { return BeanUtils.toString(this); } }
Comments