Generated immutable value classes for Java 8+ using annotation processing
—
AutoValue provides an extension framework that allows you to customize the generated code by creating custom AutoValueExtension implementations.
public class MyExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
// Determine if this extension should apply to the given AutoValue class
return context.properties().containsKey("specialProperty");
}
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
// Generate the extension class code
return "package " + context.packageName() + ";\n" +
"public " + (isFinal ? "final" : "abstract") + " class " + className +
" extends " + classToExtend + " {\n" +
" // Extension implementation\n" +
"}";
}
}Register extensions using the ServiceLoader mechanism by creating:
META-INF/services/com.google.auto.value.extension.AutoValueExtension
com.example.MyExtension
com.example.AnotherExtensionThe Context provides access to AutoValue class metadata:
public interface Context {
ProcessingEnvironment processingEnvironment();
String packageName();
TypeElement autoValueClass();
String finalAutoValueClassName();
Map<String, ExecutableElement> properties();
Map<String, TypeMirror> propertyTypes();
Set<ExecutableElement> abstractMethods();
Set<ExecutableElement> builderAbstractMethods();
List<AnnotationMirror> classAnnotationsToCopy(TypeElement classToCopyFrom);
List<AnnotationMirror> methodAnnotationsToCopy(ExecutableElement method);
Optional<BuilderContext> builder();
}Create an extension that adds validation methods:
public class ValidationExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
// Apply to classes with @Validated annotation
return context.autoValueClass()
.getAnnotationMirrors()
.stream()
.anyMatch(mirror -> mirror.getAnnotationType().toString().endsWith("Validated"));
}
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
StringBuilder code = new StringBuilder();
code.append("package ").append(context.packageName()).append(";\n\n");
code.append("public ").append(isFinal ? "final" : "abstract")
.append(" class ").append(className)
.append(" extends ").append(classToExtend).append(" {\n");
// Constructor
code.append(" ").append(className).append("(");
boolean first = true;
for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
if (!first) code.append(", ");
code.append(property.getValue().toString()).append(" ").append(property.getKey());
first = false;
}
code.append(") {\n super(");
first = true;
for (String propertyName : context.properties().keySet()) {
if (!first) code.append(", ");
code.append(propertyName);
first = false;
}
code.append(");\n");
// Add validation
for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
if (property.getValue().toString().equals("java.lang.String")) {
code.append(" if (").append(property.getKey()).append(".isEmpty()) {\n");
code.append(" throw new IllegalArgumentException(\"")
.append(property.getKey()).append(" cannot be empty\");\n");
code.append(" }\n");
}
}
code.append(" }\n");
code.append("}\n");
return code.toString();
}
}Usage:
@Validated
@AutoValue
public abstract class Person {
public abstract String name();
public abstract String email();
public static Person create(String name, String email) {
return new AutoValue_Person(name, email);
}
}
// The extension will add validation to the constructor
Person person = Person.create("", "test@example.com"); // Throws IllegalArgumentExceptionExtensions can consume properties to exclude them from the default implementation:
public class TimestampExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return context.properties().containsKey("timestamp");
}
@Override
public Set<String> consumeProperties(Context context) {
// Consume the timestamp property - AutoValue won't include it in equals/hashCode
return ImmutableSet.of("timestamp");
}
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
return "package " + context.packageName() + ";\n" +
"public " + (isFinal ? "final" : "abstract") + " class " + className +
" extends " + classToExtend + " {\n" +
" private final long timestamp;\n" +
" \n" +
" " + className + "(/* constructor parameters */) {\n" +
" super(/* super parameters */);\n" +
" this.timestamp = System.currentTimeMillis();\n" +
" }\n" +
" \n" +
" @Override\n" +
" public long timestamp() {\n" +
" return timestamp;\n" +
" }\n" +
"}";
}
}Extensions can also modify builder behavior:
public class DefaultsExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return context.builder().isPresent();
}
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
Optional<BuilderContext> builderContext = context.builder();
if (!builderContext.isPresent()) {
return null; // No builder, no extension needed
}
StringBuilder code = new StringBuilder();
code.append("package ").append(context.packageName()).append(";\n\n");
code.append("public ").append(isFinal ? "final" : "abstract")
.append(" class ").append(className)
.append(" extends ").append(classToExtend).append(" {\n");
// Add builder with smart defaults
code.append(" public static class Builder extends ")
.append(classToExtend).append(".Builder {\n");
code.append(" public Builder() {\n");
// Set intelligent defaults based on property types
for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
String propertyType = property.getValue().toString();
String propertyName = property.getKey();
if (propertyType.equals("java.lang.String") && propertyName.equals("id")) {
code.append(" ").append(propertyName).append("(java.util.UUID.randomUUID().toString());\n");
} else if (propertyType.startsWith("java.time.") && propertyName.contains("timestamp")) {
code.append(" ").append(propertyName).append("(java.time.Instant.now());\n");
}
}
code.append(" }\n");
code.append(" }\n");
code.append("}\n");
return code.toString();
}
}Extensions can consume abstract methods to provide custom implementations:
public class JsonExtension extends AutoValueExtension {
@Override
public boolean applicable(Context context) {
return context.abstractMethods().stream()
.anyMatch(method -> method.getSimpleName().toString().equals("toJson"));
}
@Override
public Set<ExecutableElement> consumeMethods(Context context) {
return context.abstractMethods().stream()
.filter(method -> method.getSimpleName().toString().equals("toJson"))
.collect(Collectors.toSet());
}
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
StringBuilder code = new StringBuilder();
code.append("package ").append(context.packageName()).append(";\n\n");
code.append("public ").append(isFinal ? "final" : "abstract")
.append(" class ").append(className)
.append(" extends ").append(classToExtend).append(" {\n");
// Constructor
code.append(" ").append(className).append("(");
// ... constructor parameters
code.append(") {\n super(");
// ... super call
code.append(");\n }\n");
// Generate toJson() method
code.append(" @Override\n");
code.append(" public String toJson() {\n");
code.append(" StringBuilder json = new StringBuilder();\n");
code.append(" json.append(\"{\");\n");
boolean first = true;
for (String propertyName : context.properties().keySet()) {
if (!first) {
code.append(" json.append(\",\");\n");
}
code.append(" json.append(\"\\\"\").append(\"").append(propertyName).append("\")");
code.append(".append(\"\\\":\\\"\").append(").append(propertyName).append("())");
code.append(".append(\"\\\"\");\n");
first = false;
}
code.append(" json.append(\"}\");\n");
code.append(" return json.toString();\n");
code.append(" }\n");
code.append("}\n");
return code.toString();
}
}Extensions can support incremental compilation:
public class MyExtension extends AutoValueExtension {
@Override
public IncrementalExtensionType incrementalType(ProcessingEnvironment processingEnvironment) {
return IncrementalExtensionType.ISOLATING; // or AGGREGATING, UNKNOWN
}
@Override
public Set<String> getSupportedOptions() {
return ImmutableSet.of("myExtension.option1", "myExtension.option2");
}
// ... other methods
}Extensions are applied in the order they appear on the classpath. If you need specific ordering:
public class FinalExtension extends AutoValueExtension {
@Override
public boolean mustBeFinal(Context context) {
return true; // This extension must be the final class in the hierarchy
}
// ... other methods
}Only one extension can return true from mustBeFinal().
Use JavaPoet for sophisticated code generation:
public class JavaPoetExtension extends AutoValueExtension {
@Override
public String generateClass(
Context context,
String className,
String classToExtend,
boolean isFinal) {
TypeSpec.Builder classBuilder = TypeSpec.classBuilder(className)
.superclass(ClassName.bestGuess(classToExtend))
.addModifiers(isFinal ? Modifier.FINAL : Modifier.ABSTRACT);
// Add constructor
MethodSpec.Builder constructorBuilder = MethodSpec.constructorBuilder();
CodeBlock.Builder superCallBuilder = CodeBlock.builder().add("super(");
boolean first = true;
for (Map.Entry<String, TypeMirror> property : context.propertyTypes().entrySet()) {
TypeName typeName = TypeName.get(property.getValue());
String paramName = property.getKey();
constructorBuilder.addParameter(typeName, paramName);
if (!first) superCallBuilder.add(", ");
superCallBuilder.add("$N", paramName);
first = false;
}
superCallBuilder.add(")");
constructorBuilder.addStatement(superCallBuilder.build());
classBuilder.addMethod(constructorBuilder.build());
TypeSpec generatedClass = classBuilder.build();
JavaFile javaFile = JavaFile.builder(context.packageName(), generatedClass)
.build();
return javaFile.toString();
}
}Test extensions using compile-testing:
@Test
public void testMyExtension() {
JavaFileObject autoValueClass = JavaFileObjects.forSourceString("test.Test",
"package test;",
"",
"import com.google.auto.value.AutoValue;",
"",
"@AutoValue",
"abstract class Test {",
" abstract String value();",
"}");
Compilation compilation = Compiler.javac()
.withProcessors(new AutoValueProcessor())
.withClasspath(/* extension classpath */)
.compile(autoValueClass);
assertThat(compilation).succeeded();
assertThat(compilation).generatedSourceFile("test.AutoValue_Test")
.contentsAsUtf8String()
.contains("// Expected extension code");
}AutoValue includes several built-in extensions:
These serve as excellent examples for creating custom extensions.
Install with Tessl CLI
npx tessl i tessl/maven-com-google-auto-value--auto-value