Search code examples
javagradleannotationsannotation-processing

Annotation Processor to generate class like lombok


Hi I'm starting study annotation processing in Java using gradle, follow multi project struct:

ROOT
|
|-sample  # contains main classes
|-annotation # annotations read by processor
|-processor # processor to generate code

I try to create a annotation to do like a lombok @Builder, I can generate a class in a build/generated/sources/annotationProcessor/java ... but if I try to generate class in same package in a annotated class but a build task fail saying I try to replace file

sample:

plugins {
    id("java")
}

group = "br.com.dio"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
    implementation(project(":annotation"))
    compileOnly(project(":annotation"))
    implementation(project(":processor"))
    annotationProcessor(project(":processor"))
}

processor:

plugins {
    id("java")
}

group = "br.com.dio"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

val autoServiceVersion = "1.1.1"

dependencies {
    compileOnly("com.google.auto.service:auto-service:$autoServiceVersion")
    annotationProcessor("com.google.auto.service:auto-service:$autoServiceVersion")
    implementation("com.squareup:javapoet:1.13.0")
}

annotation:

plugins {
    id("java")
}

group = "br.com.dio"
version = "1.0-SNAPSHOT"

repositories {
    mavenCentral()
}

dependencies {
}

settings.gradle.kts

rootProject.name = "annotation-processor"
include("processor")
include("sample")
include("annotation")

follow my code:

public class BuilderProcessor extends AbstractProcessor {

    @Override
    public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
        for (TypeElement annotation : annotations) {
            for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                Map<String, TypeMirror> fields =element.getEnclosedElements().stream()
                        .filter(e -> e.getKind() == FIELD)
                        .collect(Collectors.toMap(e -> e.getSimpleName().toString(), Element::asType));
                final var packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
                final var className = element.getSimpleName().toString();
                final var fullNameClass = packageName + "." + className;
                final var typeSpec = new BuilderCreate().create(packageName, className, fields);
                var javaFile = JavaFile.builder(packageName, typeSpec)
                        .indent("    ")
                        .build();
                try(
                        var out = new PrintWriter(processingEnv.getFiler()
                                .createSourceFile(className)
                                .openWriter()
                        )
                ){
                    out.println(javaFile);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return true;
    }

}

Solution

  • For close this question, I finished it create a builder class in another file using annotation processor, I thought how I could modify generate files and create a inner builder, like lombok, but in a comments Slaw help me to understand this is more complex ( and this is my first time using annotation processor) follow my final code:

    @SupportedAnnotationTypes("br.com.dio.annotation.Builder")
    @SupportedSourceVersion(SourceVersion.RELEASE_21)
    @AutoService(Processor.class)
    public class BuilderProcessor extends AbstractProcessor {
    
        @Override
        public boolean process(final Set<? extends TypeElement> annotations, final RoundEnvironment roundEnv) {
            for (TypeElement annotation : annotations) {
                for (Element element : roundEnv.getElementsAnnotatedWith(annotation)) {
                    Map<String, TypeMirror> fields =element.getEnclosedElements().stream()
                            .filter(e -> e.getKind() == FIELD)
                            .collect(Collectors.toMap(e -> e.getSimpleName().toString(), Element::asType));
                    final var packageName = processingEnv.getElementUtils().getPackageOf(element).toString();
                    final var className = element.getSimpleName().toString();
                    final var builderName = className + "Builder";
    
    
                    final var typeSpec = new BuilderCreate().create(packageName, className, builderName,fields);
                    var javaFile = JavaFile.builder(packageName, typeSpec)
                            .indent("    ")
                            .build();
                    try(
                            var out = new PrintWriter(processingEnv.getFiler()
                                    .createSourceFile(className + "Builder")
                                    .openWriter()
                            )
                    ){
                        out.write(javaFile.toString());
                    } catch (IOException e) {
                        throw new RuntimeException(e);
                    }
                }
            }
            return true;
        }
    
    }
    
    
    public class BuilderCreate {
    
        public TypeSpec create(final String packageName, final String className,
                               final String builderName,final Map<String, TypeMirror> fields){
            var generatedBuilderClass = TypeSpec.classBuilder(builderName)
                    .addModifiers(PUBLIC);
    
            fields.forEach((k, v) -> generatedBuilderClass.addField(TypeName.get(v), k, PRIVATE));
            fields.forEach((k, v) -> generatedBuilderClass.addMethod(buildBuilderSetter(
                            packageName,
                            builderName,
                            k,
                            TypeName.get(v))
                    )
            );
    
            var buildMethod = MethodSpec.methodBuilder("build")
                    .addModifiers(PUBLIC)
                    .returns(ClassName.get(packageName, className))
                    .addStatement("var target = new $N()", className);
    
            fields.keySet().forEach(f -> buildMethod.addStatement(
                    "target.set$N($N)",
                    f.substring(0,1).toUpperCase() + f.substring(1, f.length()),
                    f)
            );
            buildMethod.addStatement("return target");
    
            return generatedBuilderClass.addMethod(buildMethod.build())
                    .build();
        }
    
        private MethodSpec buildBuilderSetter(final String packageName, final String name, final String param, final TypeName type){
            return MethodSpec.methodBuilder(param)
                    .addModifiers(PUBLIC)
                    .returns(ClassName.get(packageName, name))
                    .addParameter(type, param, FINAL)
                    .addStatement("this.$N = $N", param, param)
                    .addStatement("return this")
                    .build();
        }
    
    }