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;
}
}
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();
}
}