/*
* Copyright 2014 - 2020 Rafael Winterhalter
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.bytebuddy.build;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.EqualsMethod;
import net.bytebuddy.implementation.HashCodeMethod;
import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers;
import java.lang.annotation.*;
import java.util.Comparator;
import static net.bytebuddy.matcher.ElementMatchers.*;
A build tool plugin that adds Object.hashCode()
and Object.equals(Object)
methods to a class if the Enhance
annotation is present and no explicit method declaration was added. This plugin does not need to be closed. /**
* A build tool plugin that adds {@link Object#hashCode()} and {@link Object#equals(Object)} methods to a class if the
* {@link Enhance} annotation is present and no explicit method declaration was added. This plugin does not need to be closed.
*/
@HashCodeAndEqualsPlugin.Enhance
public class HashCodeAndEqualsPlugin implements Plugin, Plugin.Factory {
{@inheritDoc}
/**
* {@inheritDoc}
*/
public Plugin make() {
return this;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public boolean matches(TypeDescription target) {
return target.getDeclaredAnnotations().isAnnotationPresent(Enhance.class);
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public DynamicType.Builder<?> apply(DynamicType.Builder<?> builder, TypeDescription typeDescription, ClassFileLocator classFileLocator) {
Enhance enhance = typeDescription.getDeclaredAnnotations().ofType(Enhance.class).load();
if (typeDescription.getDeclaredMethods().filter(isHashCode()).isEmpty()) {
builder = builder.method(isHashCode()).intercept(enhance.invokeSuper()
.hashCodeMethod(typeDescription)
.withIgnoredFields(enhance.includeSyntheticFields()
? ElementMatchers.<FieldDescription>none()
: ElementMatchers.<FieldDescription>isSynthetic())
.withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE))
.withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY))));
}
if (typeDescription.getDeclaredMethods().filter(isEquals()).isEmpty()) {
EqualsMethod equalsMethod = enhance.invokeSuper()
.equalsMethod(typeDescription)
.withIgnoredFields(enhance.includeSyntheticFields()
? ElementMatchers.<FieldDescription>none()
: ElementMatchers.<FieldDescription>isSynthetic())
.withIgnoredFields(new ValueMatcher(ValueHandling.Sort.IGNORE))
.withNonNullableFields(nonNullable(new ValueMatcher(ValueHandling.Sort.REVERSE_NULLABILITY)))
.withFieldOrder(AnnotationOrderComparator.INSTANCE);
if (enhance.simpleComparisonsFirst()) {
equalsMethod = equalsMethod
.withPrimitiveTypedFieldsFirst()
.withEnumerationTypedFieldsFirst()
.withPrimitiveWrapperTypedFieldsFirst()
.withStringTypedFieldsFirst();
}
builder = builder.method(isEquals()).intercept(enhance.permitSubclassEquality() ? equalsMethod.withSubclassEquality() : equalsMethod);
}
return builder;
}
Resolves the matcher to identify non-nullable fields.
Params: - matcher – The matcher that identifies fields that are either nullable or non-nullable.
Returns: The actual matcher to identify non-nullable fields.
/**
* Resolves the matcher to identify non-nullable fields.
*
* @param matcher The matcher that identifies fields that are either nullable or non-nullable.
* @return The actual matcher to identify non-nullable fields.
*/
protected ElementMatcher<FieldDescription> nonNullable(ElementMatcher<FieldDescription> matcher) {
return matcher;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public void close() {
/* do nothing */
}
A version of the HashCodeAndEqualsPlugin
that assumes that all fields are non-nullable unless they are explicitly marked. /**
* A version of the {@link HashCodeAndEqualsPlugin} that assumes that all fields are non-nullable unless they are explicitly marked.
*/
@HashCodeAndEqualsPlugin.Enhance
public static class WithNonNullableFields extends HashCodeAndEqualsPlugin {
{@inheritDoc}
/**
* {@inheritDoc}
*/
protected ElementMatcher<FieldDescription> nonNullable(ElementMatcher<FieldDescription> matcher) {
return not(matcher);
}
}
Instructs the HashCodeAndEqualsPlugin
to generate Object.hashCode()
and Object.equals(Object)
for the annotated class unless these methods are already declared explicitly. /**
* Instructs the {@link HashCodeAndEqualsPlugin} to generate {@link Object#hashCode()} and {@link Object#equals(Object)} for the annotated
* class unless these methods are already declared explicitly.
*/
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface Enhance {
Determines the base value of any added method, i.e. if hash code or equality is based on the super type or not.
Returns: A strategy for determining the base value.
/**
* Determines the base value of any added method, i.e. if hash code or equality is based on the super type or not.
*
* @return A strategy for determining the base value.
*/
InvokeSuper invokeSuper() default InvokeSuper.IF_DECLARED;
Determines if fields with primitive types, then enumeration types, then primtive wrapper types and then String
types should be compared for equality before fields with other types. Before determining such a field order, the Sorted
property is always considered first if it is defined. Returns: true
if fields with simple comparison methods should be compared first.
/**
* Determines if fields with primitive types, then enumeration types, then primtive wrapper types and then {@link String} types
* should be compared for equality before fields with other types. Before determining such a field order,
* the {@link Sorted} property is always considered first if it is defined.
*
* @return {@code true} if fields with simple comparison methods should be compared first.
*/
boolean simpleComparisonsFirst() default true;
Determines if synthetic fields should be included in the hash code and equality contract.
Returns: true
if synthetic fields should be included.
/**
* Determines if synthetic fields should be included in the hash code and equality contract.
*
* @return {@code true} if synthetic fields should be included.
*/
boolean includeSyntheticFields() default false;
Determines if instances subclasses of the instrumented type are accepted upon an equality check.
Returns: true
if instances subclasses of the instrumented type are accepted upon an equality check.
/**
* Determines if instances subclasses of the instrumented type are accepted upon an equality check.
*
* @return {@code true} if instances subclasses of the instrumented type are accepted upon an equality check.
*/
boolean permitSubclassEquality() default false;
A strategy for determining the base value of a hash code or equality contract.
/**
* A strategy for determining the base value of a hash code or equality contract.
*/
enum InvokeSuper {
Only invokes the super method's hash code and equality methods if any super class that is not Object
explicitly defines such a method. /**
* Only invokes the super method's hash code and equality methods if any super class that is not {@link Object} explicitly defines such a method.
*/
IF_DECLARED {
@Override
protected HashCodeMethod hashCodeMethod(TypeDescription instrumentedType) {
TypeDefinition typeDefinition = instrumentedType.getSuperClass();
while (typeDefinition != null && !typeDefinition.represents(Object.class)) {
if (typeDefinition.asErasure().getDeclaredAnnotations().isAnnotationPresent(Enhance.class)) {
return HashCodeMethod.usingSuperClassOffset();
}
MethodList<?> hashCode = typeDefinition.getDeclaredMethods().filter(isHashCode());
if (!hashCode.isEmpty()) {
return hashCode.getOnly().isAbstract()
? HashCodeMethod.usingDefaultOffset()
: HashCodeMethod.usingSuperClassOffset();
}
typeDefinition = typeDefinition.getSuperClass();
}
return HashCodeMethod.usingDefaultOffset();
}
@Override
protected EqualsMethod equalsMethod(TypeDescription instrumentedType) {
TypeDefinition typeDefinition = instrumentedType.getSuperClass();
while (typeDefinition != null && !typeDefinition.represents(Object.class)) {
if (typeDefinition.asErasure().getDeclaredAnnotations().isAnnotationPresent(Enhance.class)) {
return EqualsMethod.requiringSuperClassEquality();
}
MethodList<?> hashCode = typeDefinition.getDeclaredMethods().filter(isHashCode());
if (!hashCode.isEmpty()) {
return hashCode.getOnly().isAbstract()
? EqualsMethod.isolated()
: EqualsMethod.requiringSuperClassEquality();
}
typeDefinition = typeDefinition.getSuperClass().asErasure();
}
return EqualsMethod.isolated();
}
},
Only invokes the super method's hash code and equality methods if the super class is also annotated with Enhance
. /**
* Only invokes the super method's hash code and equality methods if the super class is also annotated with {@link Enhance}.
*/
IF_ANNOTATED {
@Override
protected HashCodeMethod hashCodeMethod(TypeDescription instrumentedType) {
TypeDefinition superClass = instrumentedType.getSuperClass();
return superClass != null && superClass.asErasure().getDeclaredAnnotations().isAnnotationPresent(Enhance.class)
? HashCodeMethod.usingSuperClassOffset()
: HashCodeMethod.usingDefaultOffset();
}
@Override
protected EqualsMethod equalsMethod(TypeDescription instrumentedType) {
TypeDefinition superClass = instrumentedType.getSuperClass();
return superClass != null && superClass.asErasure().getDeclaredAnnotations().isAnnotationPresent(Enhance.class)
? EqualsMethod.requiringSuperClassEquality()
: EqualsMethod.isolated();
}
},
Always invokes the super class's hash code and equality methods.
/**
* Always invokes the super class's hash code and equality methods.
*/
ALWAYS {
@Override
protected HashCodeMethod hashCodeMethod(TypeDescription instrumentedType) {
return HashCodeMethod.usingSuperClassOffset();
}
@Override
protected EqualsMethod equalsMethod(TypeDescription instrumentedType) {
return EqualsMethod.requiringSuperClassEquality();
}
},
Never invokes the super class's hash code and equality methods.
/**
* Never invokes the super class's hash code and equality methods.
*/
NEVER {
@Override
protected HashCodeMethod hashCodeMethod(TypeDescription instrumentedType) {
return HashCodeMethod.usingDefaultOffset();
}
@Override
protected EqualsMethod equalsMethod(TypeDescription instrumentedType) {
return EqualsMethod.isolated();
}
};
Resolves the hash code method to use.
Params: - instrumentedType – The instrumented type.
Returns: The hash code method to use.
/**
* Resolves the hash code method to use.
*
* @param instrumentedType The instrumented type.
* @return The hash code method to use.
*/
protected abstract HashCodeMethod hashCodeMethod(TypeDescription instrumentedType);
Resolves the equals method to use.
Params: - instrumentedType – The instrumented type.
Returns: The equals method to use.
/**
* Resolves the equals method to use.
*
* @param instrumentedType The instrumented type.
* @return The equals method to use.
*/
protected abstract EqualsMethod equalsMethod(TypeDescription instrumentedType);
}
}
Determines how a field should be used within generated hash code and equality methods.
/**
* Determines how a field should be used within generated hash code and equality methods.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ValueHandling {
Determines the handling of the annotated field.
Returns: The handling of the annotated field.
/**
* Determines the handling of the annotated field.
*
* @return The handling of the annotated field.
*/
Sort value();
Determines how a field should be handled.
/**
* Determines how a field should be handled.
*/
enum Sort {
Excludes the field from hash code and equality methods.
/**
* Excludes the field from hash code and equality methods.
*/
IGNORE,
Reverses the nullability of the field, i.e. assumes this field to be non-null or null
if WithNonNullableFields
is used. /**
* Reverses the nullability of the field, i.e. assumes this field to be non-null or {@code null} if {@link WithNonNullableFields} is used.
*/
REVERSE_NULLABILITY
}
}
Determines the sort order of fields for the equality check when implementing the Object.equals(Object)
method. Any field that is not annotated is considered with a value of DEFAULT
where fields with a higher value are checked for equality first. This sort order is applied first after which the type order is considered if Enhance.simpleComparisonsFirst()
is considered as additional sort criteria. /**
* Determines the sort order of fields for the equality check when implementing the {@link Object#equals(Object)} method. Any field
* that is not annotated is considered with a value of {@link Sorted#DEFAULT} where fields with a higher value are checked for equality
* first. This sort order is applied first after which the type order is considered if {@link Enhance#simpleComparisonsFirst()} is considered
* as additional sort criteria.
*/
@Documented
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Sorted {
The default sort weight.
/**
* The default sort weight.
*/
int DEFAULT = 0;
The value for the sort order where fields with higher values are checked for equality first.
Returns: The value for the sort order where fields with higher values are checked for equality first.
/**
* The value for the sort order where fields with higher values are checked for equality first.
*
* @return The value for the sort order where fields with higher values are checked for equality first.
*/
int value();
}
A comparator that arranges fields in the order of Sorted
. /**
* A comparator that arranges fields in the order of {@link Sorted}.
*/
protected enum AnnotationOrderComparator implements Comparator<FieldDescription.InDefinedShape> {
The singleton instance.
/**
* The singleton instance.
*/
INSTANCE;
{@inheritDoc}
/**
* {@inheritDoc}
*/
public int compare(FieldDescription.InDefinedShape left, FieldDescription.InDefinedShape right) {
AnnotationDescription.Loadable<Sorted> leftAnnotation = left.getDeclaredAnnotations().ofType(Sorted.class);
AnnotationDescription.Loadable<Sorted> rightAnnotation = right.getDeclaredAnnotations().ofType(Sorted.class);
int leftValue = leftAnnotation == null ? Sorted.DEFAULT : leftAnnotation.load().value();
int rightValue = rightAnnotation == null ? Sorted.DEFAULT : rightAnnotation.load().value();
if (leftValue > rightValue) {
return -1;
} else if (leftValue < rightValue) {
return 1;
} else {
return 0;
}
}
}
An element matcher for a ValueHandling
annotation. /**
* An element matcher for a {@link ValueHandling} annotation.
*/
@HashCodeAndEqualsPlugin.Enhance
protected static class ValueMatcher implements ElementMatcher<FieldDescription> {
The matched value.
/**
* The matched value.
*/
private final ValueHandling.Sort sort;
Creates a new value matcher.
Params: - sort – The matched value.
/**
* Creates a new value matcher.
*
* @param sort The matched value.
*/
protected ValueMatcher(ValueHandling.Sort sort) {
this.sort = sort;
}
{@inheritDoc}
/**
* {@inheritDoc}
*/
public boolean matches(FieldDescription target) {
AnnotationDescription.Loadable<ValueHandling> annotation = target.getDeclaredAnnotations().ofType(ValueHandling.class);
return annotation != null && annotation.load().value() == sort;
}
}
}