Skip to content

Commit

Permalink
Compute a serialVersionUID for @AutoAnnotation implementations ba…
Browse files Browse the repository at this point in the history
…sed on the names and types of the `@AutoAnnotation` method parameters.

RELNOTES=n/a
PiperOrigin-RevId: 332000022
  • Loading branch information
eamonnmcmanus authored and Google Java Core Libraries committed Sep 16, 2020
1 parent 86573bb commit 6bed859
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 5 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import com.google.common.collect.ImmutableSortedSet;
import com.google.common.testing.EqualsTester;
import com.google.common.testing.SerializableTester;
import java.io.ObjectStreamClass;
import java.lang.annotation.Annotation;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
Expand Down Expand Up @@ -414,6 +415,28 @@ public void testSerialization() {
}
}

@Test
@SuppressWarnings("GetClassOnAnnotation") // yes, we really do want the implementation classes
public void testSerialVersionUid() {
Class<? extends Everything> everythingImpl = EVERYTHING_FROM_AUTO.getClass();
Class<? extends Everything> everythingFromCollectionsImpl =
EVERYTHING_FROM_AUTO_COLLECTIONS.getClass();
assertThat(everythingImpl).isNotEqualTo(everythingFromCollectionsImpl);
long everythingUid = ObjectStreamClass.lookup(everythingImpl).getSerialVersionUID();
long everythingFromCollectionsUid =
ObjectStreamClass.lookup(everythingFromCollectionsImpl).getSerialVersionUID();
// Two different implementations of the same annotation with the same members being provided
// (not defaulted) should have the same serialVersionUID. They won't be serial-compatible, of
// course, because their classes are different. So we're really just checking that the
// serialVersionUID depends only on the names and types of those members.
assertThat(everythingFromCollectionsUid).isEqualTo(everythingUid);
Class<? extends StringValues> stringValuesImpl = newStringValues(new String[0]).getClass();
long stringValuesUid = ObjectStreamClass.lookup(stringValuesImpl).getSerialVersionUID();
// The previous assertion would be vacuously true if every implementation had the same
// serialVersionUID, so check that that's not true.
assertThat(stringValuesUid).isNotEqualTo(everythingUid);
}

public static class IntList extends ArrayList<Integer> {
IntList(Collection<Integer> c) {
super(c);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@

import static com.google.auto.common.GeneratedAnnotations.generatedAnnotation;
import static com.google.auto.value.processor.ClassNames.AUTO_ANNOTATION_NAME;
import static com.google.common.collect.Maps.immutableEntry;
import static java.util.Comparator.comparing;
import static java.util.stream.Collectors.joining;

import com.google.auto.common.MoreElements;
import com.google.auto.common.SuperficialValidation;
Expand All @@ -25,6 +28,7 @@
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.hash.Hashing;
import com.google.common.primitives.Primitives;
import com.google.errorprone.annotations.FormatMethod;
import java.io.IOException;
Expand Down Expand Up @@ -172,6 +176,7 @@ private void processMethod(ExecutableElement method) {
vars.pkg = pkg;
vars.wrapperTypesUsedInCollections = wrapperTypesUsedInCollections;
vars.gwtCompatible = isGwtCompatible(annotationElement);
vars.serialVersionUID = computeSerialVersionUid(members, parameters);
ImmutableMap<String, Integer> invariableHashes = invariableHashes(members, parameters.keySet());
vars.invariableHashSum = 0;
for (int h : invariableHashes.values()) {
Expand Down Expand Up @@ -448,6 +453,43 @@ private static String fullyQualifiedName(String pkg, String cls) {
return pkg.isEmpty() ? cls : pkg + "." + cls;
}

/**
* We compute a {@code serialVersionUID} for the generated class based on the names and types of
* the annotation members that the {@code @AutoAnnotation} method defines. These are exactly the
* names and types of the instance fields in the generated class. So in the common case where the
* annotation acquires a new member with a default value, if the {@code @AutoAnnotation} method is
* not changed then the generated class will acquire an implementation of the new member method
* which just returns the default value. The {@code serialVersionUID} will not change, which makes
* sense because the instance fields haven't changed, and instances that were serialized before
* the new member was added should deserialize fine. On the other hand, if you then add a
* parameter to the {@code @AutoAnnotation} method for the new member, the implementation class
* will acquire a new instance field, and we will compute a different {@code serialVersionUID}.
* That's because an instance serialized before that change would not have a value for the new
* instance field, which would end up zero or null. Users don't expect annotation methods to
* return null so that would be bad.
*
* <p>We could instead add a {@code readObject(ObjectInputStream)} method that would check that
* all of the instance fields are really present in the deserialized instance, and perhaps
* replace them with their default values from the annotation if not. That seems a lot more
* complicated than is justified, though, especially since the instance fields are final and
* would have to be set in the deserialized object through reflection.
*/
private static long computeSerialVersionUid(
ImmutableMap<String, Member> members, ImmutableMap<String, Parameter> parameters) {
// TypeMirror.toString() isn't fully specified so it could potentially differ between
// implementations. Our member.getType() string comes from TypeEncoder and is predictable, but
// it includes `...` markers around fully-qualified type names, which are used to handle
// imports. So we remove those markers below.
String namesAndTypesString =
members.entrySet().stream()
.filter(e -> parameters.containsKey(e.getKey()))
.map(e -> immutableEntry(e.getKey(), e.getValue().getType().replace("`", "")))
.sorted(comparing(Map.Entry::getKey))
.map(e -> e.getKey() + ":" + e.getValue())
.collect(joining(";"));
return Hashing.murmur3_128().hashUnencodedChars(namesAndTypesString).asLong();
}

private void writeSourceFile(String className, String text, TypeElement originatingType) {
try {
JavaFileObject sourceFile =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ class AutoAnnotationTemplateVars extends TemplateVars {
/** The sum of the hash code contributions from the members in {@link #invariableHashes}. */
Integer invariableHashSum;

/**
* A computed {@code serialVersionUID} based on the names and types of the {@code @AutoAnnotation}
* method parameters.
*/
Long serialVersionUID;

private static final Template TEMPLATE = parsedTemplateForResource("autoannotation.vm");

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ package $pkg;
// Generated by com.google.auto.value.processor.AutoAnnotationProcessor
#end
final class $className implements $annotationName, `java.io.Serializable` {
private static final long serialVersionUID = 1L;
private static final long serialVersionUID = ${serialVersionUID}L;

## Fields

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ public void testSimple() {
"@Generated(\"" + AutoAnnotationProcessor.class.getName() + "\")",
"final class AutoAnnotation_AnnotationFactory_newMyAnnotation",
" implements MyAnnotation, Serializable {",
" private static final long serialVersionUID = 1L;",
" private static final long serialVersionUID = -7473814294717163169L;",
" private final MyEnum value;",
" private static final int defaultedValue = 23;",
"",
Expand Down Expand Up @@ -165,7 +165,7 @@ public void testEmptyPackage() {
"@Generated(\"" + AutoAnnotationProcessor.class.getName() + "\")",
"final class AutoAnnotation_AnnotationFactory_newMyAnnotation",
" implements MyAnnotation, Serializable {",
" private static final long serialVersionUID = 1L;",
" private static final long serialVersionUID = 0L;",
" AutoAnnotation_AnnotationFactory_newMyAnnotation() {",
" }",
"",
Expand Down Expand Up @@ -248,7 +248,7 @@ public void testGwtSimple() {
"@Generated(\"" + AutoAnnotationProcessor.class.getName() + "\")",
"final class AutoAnnotation_AnnotationFactory_newMyAnnotation implements MyAnnotation,"
+ " Serializable {",
" private static final long serialVersionUID = 1L;",
" private static final long serialVersionUID = -8116050813861599066L;",
" private final int[] value;",
"",
" AutoAnnotation_AnnotationFactory_newMyAnnotation(int[] value) {",
Expand Down Expand Up @@ -359,7 +359,7 @@ public void testCollectionsForArrays() {
"@Generated(\"" + AutoAnnotationProcessor.class.getName() + "\")",
"final class AutoAnnotation_AnnotationFactory_newMyAnnotation implements MyAnnotation,"
+ " Serializable {",
" private static final long serialVersionUID = 1L;",
" private static final long serialVersionUID = -2102364343628921304L;",
" private final int[] value;",
" private final MyEnum[] enums;",
"",
Expand Down

0 comments on commit 6bed859

Please sign in to comment.