diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java index 73190e38faf..6dedd1354a1 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/BeanIntrospectionWriter.java @@ -722,7 +722,8 @@ private void writeIntrospectionClass(ClassWriterOutputVisitor classWriterOutputV dispatchWriter.buildGetTargetMethodByIndex(classWriter); buildFindIndexedProperty(classWriter); buildGetIndexedProperties(classWriter); - boolean hasBuilder = annotationMetadata != null && annotationMetadata.isPresent(Introspected.class, "builder"); + boolean hasBuilder = annotationMetadata != null && + (annotationMetadata.isPresent(Introspected.class, "builder") || annotationMetadata.hasDeclaredAnnotation("lombok.Builder")); if (defaultConstructor != null) { writeInstantiateMethod(classWriter, defaultConstructor, "instantiate"); // in case invoked directly or via instantiateUnsafe diff --git a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java index 44407f9be05..a7a0ff06c97 100644 --- a/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java +++ b/core-processor/src/main/java/io/micronaut/inject/beans/visitor/IntrospectedTypeElementVisitor.java @@ -67,6 +67,7 @@ public class IntrospectedTypeElementVisitor implements TypeElementVisitor writers = new LinkedHashMap<>(10); @@ -180,62 +181,101 @@ private void processBuilderDefinition(ClassElement element, VisitorContext conte AnnotationClassValue builderClass = builder.annotationClassValue("builderClass").orElse(null); String[] writePrefixes = builder.getAnnotation("accessorStyle", AccessorsStyle.class) .map(a -> a.stringValues("writePrefixes")).orElse(new String[]{""}); - if (builderMethod != null) { - MethodElement methodElement = element - .getEnclosedElement(ElementQuery.ALL_METHODS.onlyStatic() - .filter(m -> m.getName().equals(builderMethod) && !m.getGenericReturnType().isVoid()) - .onlyAccessible(element)) - .orElse(null); - if (methodElement != null) { - ClassElement returnType = methodElement.getGenericReturnType(); - if (returnType.isPublic() || returnType.getPackageName().equals(element.getPackageName())) { - AnnotationValueBuilder replaceIntrospected = AnnotationValue.builder(introspected, RetentionPolicy.RUNTIME); - replaceIntrospected.member("builderClass", new AnnotationClassValue<>(returnType.getName())); - element.annotate(replaceIntrospected.build()); - AnnotationMetadata methodMetadata = methodElement.getMethodAnnotationMetadata().getTargetAnnotationMetadata(); - - handleBuilder( - element, - context, - creatorMethod, - writePrefixes, - methodElement, - null, - returnType, - methodMetadata, - index, - targetPackage - ); - } else { - context.fail("Builder return type is not public. The method must be static and accessible.", methodElement); - } - } else { - context.fail("Method specified by builderMethod not found. The method must be static and accessible.", element); - } - } else if (builderClass != null) { - ClassElement builderClassElement = context.getClassElement(builderClass.getName()).orElse(null); - if (builderClassElement != null) { + processBuilderDefinition( + element, + context, + introspected, + index, + targetPackage, + builderMethod, + creatorMethod, + writePrefixes, + builderClass + ); + } else if (element.hasDeclaredAnnotation(ANN_LOMBOK_BUILDER)) { + AnnotationValue lombokBuilder = element.getAnnotation(ANN_LOMBOK_BUILDER); + String builderMethod = lombokBuilder.stringValue("builderMethodName").orElse("builder"); + MethodElement methodElement = element + .getEnclosedElement(ElementQuery.ALL_METHODS.onlyStatic() + .filter(m -> m.getName().equals(builderMethod) && !m.getGenericReturnType().isVoid()) + .onlyAccessible(element)) + .orElse(null); + if (methodElement == null) { + // Lombok processing not done yet, try again in the next round. + throw new ElementPostponedToNextRoundException(element); + } + String creatorMethod = lombokBuilder.stringValue("buildMethodName").orElse("build"); + String[] writePrefixes = lombokBuilder.stringValue("setterPrefix").map(sp -> new String[] { sp }).orElse(new String[]{""}); + processBuilderDefinition( + element, + context, + introspected, + index, + targetPackage, + builderMethod, + creatorMethod, + writePrefixes, + null + ); + } + } + + private void processBuilderDefinition(ClassElement element, VisitorContext context, AnnotationValue introspected, int index, String targetPackage, String builderMethod, String creatorMethod, String[] writePrefixes, AnnotationClassValue builderClass) { + if (builderMethod != null) { + MethodElement methodElement = element + .getEnclosedElement(ElementQuery.ALL_METHODS.onlyStatic() + .filter(m -> m.getName().equals(builderMethod) && !m.getGenericReturnType().isVoid()) + .onlyAccessible(element)) + .orElse(null); + if (methodElement != null) { + ClassElement returnType = methodElement.getGenericReturnType(); + if (returnType.isPublic() || returnType.getPackageName().equals(element.getPackageName())) { AnnotationValueBuilder replaceIntrospected = AnnotationValue.builder(introspected, RetentionPolicy.RUNTIME); - replaceIntrospected.member("builderClass", new AnnotationClassValue<>(builderClassElement.getName())); + replaceIntrospected.member("builderClass", new AnnotationClassValue<>(returnType.getName())); element.annotate(replaceIntrospected.build()); + AnnotationMetadata methodMetadata = methodElement.getMethodAnnotationMetadata().getTargetAnnotationMetadata(); handleBuilder( element, context, creatorMethod, writePrefixes, - builderClassElement.getPrimaryConstructor().orElse(null), - builderClassElement.getDefaultConstructor().orElse(null), - builderClassElement, - builderClassElement.getTargetAnnotationMetadata(), + methodElement, + null, + returnType, + methodMetadata, index, - targetPackage); + targetPackage + ); } else { - context.fail("Builder class not found on compilation classpath: " + builderClass.getName(), element); + context.fail("Builder return type is not public. The method must be static and accessible.", methodElement); } } else { - context.fail("When specifying the 'builder' member of @Introspected you must supply either a builderClass or builderMethod", element); + context.fail("Method " + builderMethod + "() specified by builderMethod not found. The method must be static and accessible.", element); } + } else if (builderClass != null) { + ClassElement builderClassElement = context.getClassElement(builderClass.getName()).orElse(null); + if (builderClassElement != null) { + AnnotationValueBuilder replaceIntrospected = AnnotationValue.builder(introspected, RetentionPolicy.RUNTIME); + replaceIntrospected.member("builderClass", new AnnotationClassValue<>(builderClassElement.getName())); + element.annotate(replaceIntrospected.build()); + + handleBuilder( + element, + context, + creatorMethod, + writePrefixes, + builderClassElement.getPrimaryConstructor().orElse(null), + builderClassElement.getDefaultConstructor().orElse(null), + builderClassElement, + builderClassElement.getTargetAnnotationMetadata(), + index, + targetPackage); + } else { + context.fail("Builder class not found on compilation classpath: " + builderClass.getName(), element); + } + } else { + context.fail("When specifying the 'builder' member of @Introspected you must supply either a builderClass or builderMethod", element); } } @@ -380,13 +420,18 @@ private void processElement(boolean metadata, List beanProperties = ce.getBeanProperties(propertyElementQuery).stream() .filter(p -> !p.isExcluded()) .toList(); - Optional constructorElement = ce.getPrimaryConstructor(); - constructorElement.ifPresent(constructorEl -> { - if (ArrayUtils.isNotEmpty(constructorEl.getParameters())) { - writer.visitConstructor(constructorEl); - } - }); - ce.getDefaultConstructor().ifPresent(writer::visitDefaultConstructor); + // unfortunately sometimes we don't see the Lombok transformations + // so assume if the class is annotated with Lombok builder we cannot + // access the constructor. + if (!ce.hasDeclaredAnnotation(ANN_LOMBOK_BUILDER)) { + Optional constructorElement = ce.getPrimaryConstructor(); + constructorElement.ifPresent(constructorEl -> { + if (ArrayUtils.isNotEmpty(constructorEl.getParameters())) { + writer.visitConstructor(constructorEl); + } + }); + ce.getDefaultConstructor().ifPresent(writer::visitDefaultConstructor); + } for (PropertyElement beanProperty : beanProperties) { if (beanProperty.isExcluded()) { diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java index 2d26416e059..e68fac8b282 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/TypeElementVisitorProcessor.java @@ -35,6 +35,7 @@ import io.micronaut.inject.ast.MemberElement; import io.micronaut.inject.ast.MethodElement; import io.micronaut.inject.processing.ProcessingException; +import io.micronaut.inject.visitor.ElementPostponedToNextRoundException; import io.micronaut.inject.visitor.TypeElementVisitor; import io.micronaut.inject.visitor.VisitorContext; import io.micronaut.inject.writer.AbstractBeanDefinitionBuilder; @@ -42,6 +43,7 @@ import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; import javax.annotation.processing.SupportedOptions; +import javax.lang.model.element.Element; import javax.lang.model.element.TypeElement; import javax.lang.model.type.TypeMirror; import java.io.IOException; @@ -276,6 +278,15 @@ public boolean process(Set annotations, RoundEnvironment error(originatingElement.element(), e.getMessage()); } catch (PostponeToNextRoundException e) { postponedTypes.put(javaClassElement.getCanonicalName(), e.getErrorElement()); + } catch (ElementPostponedToNextRoundException e) { + Object nativeType = e.getOriginatingElement().getNativeType(); + if (nativeType instanceof JavaNativeElement jne) { + Element element = jne.element(); + postponedTypes.put(javaClassElement.getCanonicalName(), element); + } else { + // should never happen. + throw e; + } } } } diff --git a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java index 05fad119a7e..cab98699d45 100644 --- a/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java +++ b/inject/src/main/java/io/micronaut/inject/beans/AbstractInitializableBeanIntrospection.java @@ -472,9 +472,11 @@ private IntrospectionBuilderData getBuilderData() { AnnotationValue builderAnn = getAnnotationMetadata().findAnnotation(Introspected.class) .flatMap(a -> a.getAnnotation("builder", Introspected.IntrospectionBuilder.class)).orElse(null); - if (builderAnn != null) { - Class builderClass = getAnnotationMetadata().classValue(Introspected.class, "builderClass").orElse(null); - if (builderClass != null) { + Class builderClass = getAnnotationMetadata().classValue(Introspected.class, "builderClass").orElse(null); + if (builderAnn != null || builderClass != null) { + if (builderClass == null) { + throw new IntrospectionException("Introspection defines invalid builder member for type: " + getBeanType()); + } else { BeanIntrospection builderIntrospection = (BeanIntrospection) BeanIntrospection.getIntrospection(builderClass); Collection> beanMethods = builderIntrospection.getBeanMethods(); @@ -520,8 +522,6 @@ private IntrospectionBuilderData getBuilderData() { arguments.toArray(Argument.ZERO_ARGUMENTS) ); } - } else { - throw new IntrospectionException("Introspection defines invalid builder member for type: " + getBeanType()); } } else { int constructorLength = constructorArguments.length; diff --git a/test-suite/build.gradle b/test-suite/build.gradle index 29c6095ecb4..690bf5429bb 100644 --- a/test-suite/build.gradle +++ b/test-suite/build.gradle @@ -118,3 +118,4 @@ test { // Prevent scanning classes with missing classes exclude '**/classnotfound/**' } + diff --git a/test-suite/src/test/java/io/micronaut/test/lombok/LombokIntrospectedBuilderTest.java b/test-suite/src/test/java/io/micronaut/test/lombok/LombokIntrospectedBuilderTest.java index 7424b20a457..76f1f54290f 100644 --- a/test-suite/src/test/java/io/micronaut/test/lombok/LombokIntrospectedBuilderTest.java +++ b/test-suite/src/test/java/io/micronaut/test/lombok/LombokIntrospectedBuilderTest.java @@ -1,5 +1,7 @@ package io.micronaut.test.lombok; +import static org.junit.jupiter.api.Assertions.assertEquals; + import io.micronaut.core.beans.BeanIntrospection; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -17,7 +19,19 @@ void testLombokBuilder() { RobotEntity robotEntity = builder.with("name", "foo") .build(); - Assertions.assertEquals("foo", robotEntity.getName()); + assertEquals("foo", robotEntity.getName()); + } + + @Test + void testLombokBuilder2() { + BeanIntrospection.Builder builder = BeanIntrospection.getIntrospection(MyEntity.class) + .builder(); + MyEntity.MyEntityBuilder builder1 = MyEntity.builder(); + builder.with("name", "foo"); + builder.with("id", "123"); + MyEntity myEntity = builder.build(); + assertEquals("foo", myEntity.getName()); + assertEquals("123", myEntity.getId()); } @Test @@ -29,7 +43,7 @@ void testLombokBuilderWithInnerClasses() { SimpleEntity simpleEntity = builder.with("id", id) .build(); - Assertions.assertEquals(id, simpleEntity.getId()); + assertEquals(id, simpleEntity.getId()); BeanIntrospection innerClassIntrospection = BeanIntrospection.getIntrospection(SimpleEntity.CompartmentCreationTimeIndexPrefix.class); @@ -42,7 +56,7 @@ void testLombokBuilderWithInnerClasses() { SimpleEntity.CompartmentCreationTimeIndexPrefix innerClassEntity = innerClassBuilder.with("compartmentId", "c1").with("timeCreated", current).build(); - Assertions.assertEquals("c1", innerClassEntity.getCompartmentId()); - Assertions.assertEquals(current, innerClassEntity.getTimeCreated()); + assertEquals("c1", innerClassEntity.getCompartmentId()); + assertEquals(current, innerClassEntity.getTimeCreated()); } } diff --git a/test-suite/src/test/java/io/micronaut/test/lombok/MyEntity.java b/test-suite/src/test/java/io/micronaut/test/lombok/MyEntity.java new file mode 100644 index 00000000000..a91af297fc3 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/test/lombok/MyEntity.java @@ -0,0 +1,19 @@ +package io.micronaut.test.lombok; + + +import io.micronaut.core.annotation.Introspected; +import lombok.Builder; +import lombok.Value; + +@Introspected +@Value +@Builder +public class MyEntity { + public static final String NAME_INDEX = "name"; + + @lombok.NonNull + String id; + + @lombok.NonNull + String name; +}