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