diff --git a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java index 31a19abaaaa..0e0dc59b5b0 100644 --- a/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java +++ b/aop/src/main/java/io/micronaut/aop/chain/DefaultInterceptorRegistry.java @@ -123,7 +123,7 @@ public DefaultInterceptorRegistry(BeanContext beanContext) { @Override @NonNull - public Interceptor[] resolveConstructorInterceptors( + public Interceptor[] resolveConstructorInterceptors( @NonNull BeanConstructor constructor, @NonNull Collection>> interceptors) { instrumentAnnotationMetadata(beanContext, constructor); diff --git a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy index 588652a82c6..7424da5f1cb 100644 --- a/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy +++ b/inject-groovy/src/main/groovy/io/micronaut/ast/groovy/InjectVisitor.groovy @@ -29,6 +29,7 @@ import io.micronaut.aop.writer.AopProxyWriter import io.micronaut.ast.groovy.annotation.GroovyAnnotationMetadataBuilder import io.micronaut.ast.groovy.utils.AstAnnotationUtils import io.micronaut.ast.groovy.utils.AstGenericUtils +import io.micronaut.ast.groovy.utils.AstMessageUtils import io.micronaut.ast.groovy.utils.PublicAbstractMethodVisitor import io.micronaut.ast.groovy.utils.PublicMethodVisitor import io.micronaut.ast.groovy.visitor.GroovyElementFactory @@ -157,7 +158,7 @@ final class InjectVisitor extends ClassCodeVisitorSupport { this.originatingElement = elementFactory.newClassElement(concreteClass, annotationMetadata) this.concreteClassElement = originatingElement this.isFactoryClass = annotationMetadata.hasStereotype(Factory) - this.isAopProxyType = hasAroundStereotype(annotationMetadata) && !targetClassNode.isAbstract() + this.isAopProxyType = hasAroundStereotype(annotationMetadata) && !targetClassNode.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class) this.aopSettings = isAopProxyType ? annotationMetadata.getValues(AROUND_TYPE, Boolean.class) : OptionalValues. empty() this.isExecutableType = isAopProxyType || annotationMetadata.hasStereotype(Executable) this.isConfigurationProperties = configurationProperties != null ? configurationProperties : annotationMetadata.hasDeclaredStereotype(ConfigurationReader) @@ -469,116 +470,7 @@ final class InjectVisitor extends ClassCodeVisitorSupport { ) if (isFactoryClass && !isConstructor && methodAnnotationMetadata.hasDeclaredStereotype(Bean, Scope)) { methodAnnotationMetadata = new GroovyAnnotationMetadataBuilder(sourceUnit, compilationUnit).buildForParent(methodNode.returnType, methodNode, true) - if (concreteClassAnnotationMetadata.hasDeclaredStereotype(Around)) { - visitExecutableMethod(declaringClass, methodNode, methodAnnotationMetadata, methodName, methodNode.isPublic()) - } - - MethodElement factoryMethodElement = elementFactory.newMethodElement( - concreteClassElement, - methodNode, - methodAnnotationMetadata - ) - ClassElement producedClassElement = factoryMethodElement.genericReturnType - BeanDefinitionWriter beanMethodWriter = new BeanDefinitionWriter( - factoryMethodElement, - OriginatingElements.of(originatingElement), - configurationMetadataBuilder, - groovyVisitorContext, - factoryMethodIndex.getAndIncrement() - ) - - ClassNode returnType = methodNode.getReturnType() - def allTypeArguments = factoryMethodElement.returnType.allTypeArguments - beanMethodWriter.visitTypeArguments(allTypeArguments) - beanMethodWriter.visitBeanFactoryMethod( - originatingElement, - factoryMethodElement - ) - - if (hasAroundStereotype(methodAnnotationMetadata) && !producedClassElement.isAssignable(Interceptor)) { - - if (Modifier.isFinal(returnType.modifiers)) { - addError( - "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: $methodNode", - methodNode - ) - return - } - - AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil - .resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) - OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean) - Map finalSettings = [:] - for (key in aopSettings) { - finalSettings.put(key, aopSettings.get(key).get()) - } - finalSettings.put(Interceptor.PROXY_TARGET, true) - - AopProxyWriter proxyWriter = new AopProxyWriter( - beanMethodWriter, - OptionalValues.of(Boolean.class, finalSettings), - configurationMetadataBuilder, - groovyVisitorContext, - interceptorTypeReferences - ) - proxyWriter.visitTypeArguments(allTypeArguments) - if (producedClassElement.isInterface()) { - proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) - } else { - populateProxyWriterConstructor(producedClassElement, proxyWriter) - } - SourceUnit source = this.sourceUnit - CompilationUnit unit = this.compilationUnit - ClassElement finalConcreteClassElement = this.concreteClassElement - new PublicMethodVisitor(source) { - @Override - void accept(ClassNode classNode, MethodNode targetBeanMethodNode) { - AnnotationMetadata annotationMetadata - if (AstAnnotationUtils.isAnnotated(producedClassElement.name, methodNode)) { - annotationMetadata = AstAnnotationUtils.newBuilder(source, unit) - .buildForParent(producedClassElement.name, methodNode, targetBeanMethodNode) - } else { - annotationMetadata = new AnnotationMetadataReference( - beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, - methodAnnotationMetadata - ) - } - MethodElement targetMethodElement = elementFactory.newMethodElement( - finalConcreteClassElement, - targetBeanMethodNode, - annotationMetadata - ) - - proxyWriter.visitAroundMethod( - targetMethodElement.declaringType, - targetMethodElement - ) - } - }.accept(returnType) - beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter) - - } - Optional preDestroy = methodAnnotationMetadata.getValue(Bean, "preDestroy", String.class) - if (preDestroy.isPresent()) { - String destroyMethodName = preDestroy.get() - MethodNode destroyMethod = ((ClassNode) producedClassElement.nativeType).getMethod(destroyMethodName) - if (destroyMethod != null) { - def destroyMethodElement = elementFactory.newMethodElement( - producedClassElement, - destroyMethod, - AnnotationMetadata.EMPTY_METADATA - ) - beanMethodWriter.visitPreDestroyMethod( - producedClassElement, - destroyMethodElement, - false, - groovyVisitorContext - ) - } else { - addError("@Bean method defines a preDestroy method that does not exist or is not public: $destroyMethodName", methodNode ) - } - } - beanDefinitionWriters.put(methodNode, beanMethodWriter) + visitBeanFactoryElement(declaringClass, methodNode, methodAnnotationMetadata, methodName) } else if (methodAnnotationMetadata.hasStereotype(Inject.name, ProcessedTypes.POST_CONSTRUCT, ProcessedTypes.PRE_DESTROY)) { if (isConstructor && methodAnnotationMetadata.hasStereotype(Inject)) { // constructor with explicit @Inject @@ -735,6 +627,160 @@ final class InjectVisitor extends ClassCodeVisitorSupport { } } + @CompileStatic + private void visitBeanFactoryElement( + ClassNode declaringClass, + AnnotatedNode annotatedNode, + AnnotationMetadata methodAnnotationMetadata, + String elementName) { + if (annotatedNode instanceof MethodNode && concreteClassAnnotationMetadata.hasDeclaredStereotype(Around)) { + visitExecutableMethod(declaringClass, annotatedNode, methodAnnotationMetadata, elementName, annotatedNode.isPublic()) + } + + ClassElement producedClassElement + ClassNode returnType + Map> allTypeArguments + BeanDefinitionWriter beanMethodWriter + if (annotatedNode instanceof MethodNode) { + + def methodNode = (MethodNode) annotatedNode + MethodElement factoryMethodElement = elementFactory.newMethodElement( + concreteClassElement, + methodNode, + methodAnnotationMetadata + ) + producedClassElement = factoryMethodElement.genericReturnType + beanMethodWriter = new BeanDefinitionWriter( + factoryMethodElement, + OriginatingElements.of(originatingElement), + configurationMetadataBuilder, + groovyVisitorContext, + factoryMethodIndex.getAndIncrement() + ) + + returnType = methodNode.getReturnType() + allTypeArguments = factoryMethodElement.returnType.allTypeArguments + beanMethodWriter.visitTypeArguments(allTypeArguments) + beanMethodWriter.visitBeanFactoryMethod( + originatingElement, + factoryMethodElement + ) + } else { + FieldNode fieldNode + if (annotatedNode instanceof PropertyNode) { + fieldNode = ((PropertyNode) annotatedNode).field + } else { + fieldNode = annotatedNode as FieldNode + } + FieldElement factoryField = elementFactory.newFieldElement( + concreteClassElement, + fieldNode, + methodAnnotationMetadata + ) + producedClassElement = factoryField.genericField + beanMethodWriter = new BeanDefinitionWriter( + factoryField, + OriginatingElements.of(originatingElement), + configurationMetadataBuilder, + groovyVisitorContext, + factoryMethodIndex.getAndIncrement() + ) + + returnType = factoryField.type.nativeType as ClassNode + allTypeArguments = factoryField.type.allTypeArguments + beanMethodWriter.visitTypeArguments(allTypeArguments) + beanMethodWriter.visitBeanFactoryField( + originatingElement, + factoryField + ) + } + + if (hasAroundStereotype(methodAnnotationMetadata) && !producedClassElement.isAssignable(Interceptor.class)) { + + if (Modifier.isFinal(returnType.modifiers)) { + addError( + "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: $annotatedNode", + annotatedNode + ) + return + } + + AnnotationValue[] interceptorTypeReferences = InterceptedMethodUtil + .resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND) + OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean) + Map finalSettings = [:] + for (key in aopSettings) { + finalSettings.put(key, aopSettings.get(key).get()) + } + finalSettings.put(Interceptor.PROXY_TARGET, true) + + AopProxyWriter proxyWriter = new AopProxyWriter( + beanMethodWriter, + OptionalValues.of(Boolean.class, finalSettings), + configurationMetadataBuilder, + groovyVisitorContext, + interceptorTypeReferences + ) + proxyWriter.visitTypeArguments(allTypeArguments) + if (producedClassElement.isInterface()) { + proxyWriter.visitDefaultConstructor(AnnotationMetadata.EMPTY_METADATA, groovyVisitorContext) + } else { + populateProxyWriterConstructor(producedClassElement, proxyWriter) + } + SourceUnit source = this.sourceUnit + CompilationUnit unit = this.compilationUnit + ClassElement finalConcreteClassElement = this.concreteClassElement + new PublicMethodVisitor(source) { + @Override + void accept(ClassNode classNode, MethodNode targetBeanMethodNode) { + AnnotationMetadata annotationMetadata + if (AstAnnotationUtils.isAnnotated(producedClassElement.name, annotatedNode)) { + annotationMetadata = AstAnnotationUtils.newBuilder(source, unit) + .buildForParent(producedClassElement.name, annotatedNode, targetBeanMethodNode) + } else { + annotationMetadata = new AnnotationMetadataReference( + beanMethodWriter.getBeanDefinitionName() + BeanDefinitionReferenceWriter.REF_SUFFIX, + methodAnnotationMetadata + ) + } + MethodElement targetMethodElement = elementFactory.newMethodElement( + finalConcreteClassElement, + targetBeanMethodNode, + annotationMetadata + ) + + proxyWriter.visitAroundMethod( + targetMethodElement.declaringType, + targetMethodElement + ) + } + }.accept(returnType) + beanDefinitionWriters.put(new AnnotatedNode(), proxyWriter) + + } + Optional preDestroy = methodAnnotationMetadata.getValue(Bean, "preDestroy", String.class) + if (preDestroy.isPresent()) { + String destroyMethodName = preDestroy.get() + MethodNode destroyMethod = ((ClassNode) producedClassElement.nativeType).getMethod(destroyMethodName) + if (destroyMethod != null) { + def destroyMethodElement = elementFactory.newMethodElement( + producedClassElement, + destroyMethod, + AnnotationMetadata.EMPTY_METADATA + ) + beanMethodWriter.visitPreDestroyMethod( + producedClassElement, + destroyMethodElement, + false, + groovyVisitorContext + ) + } else { + addError("@Bean method defines a preDestroy method that does not exist or is not public: $destroyMethodName", annotatedNode) + } + } + beanDefinitionWriters.put(annotatedNode, beanMethodWriter) + } + private static AnnotationMetadata addPropertyMetadata(Element element, PropertyMetadata propertyMetadata) { element.annotate( Property.class.getName(), @@ -794,7 +840,7 @@ final class InjectVisitor extends ClassCodeVisitorSupport { } boolean hasAround = hasConstraints || hasAroundStereotype(methodAnnotationMetadata) - if ((isAopProxyType && isPublic) || (hasAround && !concreteClass.isAbstract())) { + if ((isAopProxyType && isPublic) || (hasAround && !concreteClass.isAbstract() && !concreteClassElement.isAssignable(Interceptor.class))) { boolean hasExplicitAround = hasDeclaredAroundStereotype(methodAnnotationMetadata) @@ -933,6 +979,25 @@ final class InjectVisitor extends ClassCodeVisitorSupport { ClassNode declaringClass = fieldNode.declaringClass AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) { + if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (fieldNode.isPrivate() || fieldNode.isProtected()) { + AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility") + } else { + visitBeanFactoryElement( + concreteClass, + fieldNode, + fieldAnnotationMetadata, + fieldNode.name + ) + } + } + return + } else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (fieldNode.isPrivate() || fieldNode.isProtected()) { + AstMessageUtils.error(sourceUnit, fieldNode, "Beans produced from fields cannot be private or protected visibility") + } return } boolean isInject = fieldAnnotationMetadata.hasStereotype(Inject) @@ -1021,10 +1086,22 @@ final class InjectVisitor extends ClassCodeVisitorSupport { if (fieldNode.name == 'metaClass') return def modifiers = propertyNode.getModifiers() if (Modifier.isStatic(modifiers)) { + if (isFactoryClass && AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode).hasDeclaredStereotype(Bean.class)) { + AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be static") + } return } AnnotationMetadata fieldAnnotationMetadata = AstAnnotationUtils.getAnnotationMetadata(sourceUnit, compilationUnit, fieldNode) if (Modifier.isFinal(modifiers) && !fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder)) { + if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (propertyNode.isPrivate()) { + AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private") + } else { + visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata) + + } + } return } boolean isInject = fieldNode != null && fieldAnnotationMetadata.hasStereotype(Inject) @@ -1152,9 +1229,35 @@ final class InjectVisitor extends ClassCodeVisitorSupport { getterElement ) } + } else if (isFactoryClass && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (propertyNode.isPrivate()) { + AstMessageUtils.error(sourceUnit, propertyNode, "Beans produced from fields cannot be private"); + } else { + visitFactoryProperty(propertyNode, fieldNode, fieldAnnotationMetadata) + } } } + private void visitFactoryProperty(PropertyNode propertyNode, FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) { + + def getterNode = new MethodNode( + getGetterName(propertyNode), + Modifier.PUBLIC, + fieldNode.type, + new Parameter[0], + null, + null + ) + getterNode.declaringClass = concreteClass + visitBeanFactoryElement( + concreteClass, + getterNode, + fieldAnnotationMetadata, + getterNode.name + ) + } + private boolean isValueInjection(FieldNode fieldNode, AnnotationMetadata fieldAnnotationMetadata) { fieldNode != null && ( fieldAnnotationMetadata.hasStereotype(Value) || diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy new file mode 100644 index 00000000000..86b5dff3811 --- /dev/null +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/factory/FactoryBeanFieldSpec.groovy @@ -0,0 +1,120 @@ +package io.micronaut.inject.factory + +import io.micronaut.ast.transform.test.AbstractBeanDefinitionSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Unroll + +class FactoryBeanFieldSpec extends AbstractBeanDefinitionSpec { + void "test a factory bean can be supplied from a field"() { + given: + ApplicationContext context = buildContext('test.TestFactory$TestField', '''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import javax.inject.*; + +@Factory +class TestFactory$TestField { + + @Singleton + @Bean + @io.micronaut.context.annotation.Primary + Foo one = new Foo("one"); + + // final fields are implicitly singleton + @Bean + @Named("two") + final Foo two = new Foo("two"); + + // non-final fields are prototype + @Bean + @Named("three") + Foo three = new Foo("three"); + + @SomeMeta + @Bean + Foo four = new Foo("four"); +} + +class Foo { + final String name; + Foo(String name) { + this.name = name; + } +} + +@Retention(RUNTIME) +@Singleton +@Named("four") +@AroundConstruct +@interface SomeMeta { +} + +@Singleton +@InterceptorBean(SomeMeta.class) +class TestConstructInterceptor implements ConstructorInterceptor { + boolean invoked = false; + Object[] parameters; + + @Override + public Object intercept(ConstructorInvocationContext context) { + invoked = true; + parameters = context.getParameterValues(); + return context.proceed(); + } +} +''') + + expect: + + getBean(context, "test.Foo").name == 'one' + getBean(context, "test.Foo", Qualifiers.byName("two")).name == 'two' + getBean(context, "test.Foo", Qualifiers.byName("two")).is( + getBean(context, "test.Foo", Qualifiers.byName("two")) + ) + getBean(context, "test.Foo", Qualifiers.byName("three")).is( + getBean(context, "test.Foo", Qualifiers.byName("three")) + ) + getBean(context, 'test.TestConstructInterceptor').invoked == false + getBean(context, "test.Foo", Qualifiers.byName("four")) // around construct + getBean(context, 'test.TestConstructInterceptor').invoked == true + + cleanup: + context.close() + } + + @Unroll + void 'test fail compilation on invalid modifier #modifier'() { + when: + buildBeanDefinition('invalidmod.TestFactory', """ +package invalidmod; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.inject.*; + +@Factory +class TestFactory { + @Bean + $modifier Test test; +} + +class Test {} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("cannot be ") + e.message.contains(modifier) + + where: + modifier << ['private', 'protected', 'static'] + } +} diff --git a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java index cc813645de5..22ba0cf8c03 100644 --- a/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java +++ b/inject-java/src/main/java/io/micronaut/annotation/processing/BeanDefinitionInjectProcessor.java @@ -737,17 +737,7 @@ public Object visitExecutable(ExecutableElement method, Object o) { final AnnotationMetadata annotationMetadata = annotationUtils.getAnnotationMetadata(method); - AnnotationMetadata methodAnnotationMetadata; - - if (annotationMetadata instanceof AnnotationMetadataHierarchy) { - methodAnnotationMetadata = annotationMetadata; - } else { - - methodAnnotationMetadata = new AnnotationMetadataHierarchy( - concreteClassMetadata, - annotationMetadata - ); - } + AnnotationMetadata methodAnnotationMetadata = getMetadataHierarchy(annotationMetadata); TypeKind returnKind = method.getReturnType().getKind(); if ((returnKind == TypeKind.ERROR) && !processingOver) { @@ -757,7 +747,7 @@ public Object visitExecutable(ExecutableElement method, Object o) { // handle @Bean annotation for @Factory class JavaMethodElement javaMethodElement = elementFactory.newMethodElement(concreteClassElement, method, methodAnnotationMetadata); if (isFactoryType && javaMethodElement.hasDeclaredStereotype(Bean.class, Scope.class) && !javaMethodElement.getReturnType().isPrimitive()) { - visitBeanFactoryMethod(javaMethodElement, method); + visitBeanFactoryElement(method); return null; } @@ -814,6 +804,22 @@ public Object visitExecutable(ExecutableElement method, Object o) { return null; } + @NonNull + private AnnotationMetadata getMetadataHierarchy(AnnotationMetadata annotationMetadata) { + AnnotationMetadata methodAnnotationMetadata; + + if (annotationMetadata instanceof AnnotationMetadataHierarchy) { + methodAnnotationMetadata = annotationMetadata; + } else { + + methodAnnotationMetadata = new AnnotationMetadataHierarchy( + concreteClassMetadata, + annotationMetadata + ); + } + return methodAnnotationMetadata; + } + private boolean hasAroundStereotype(AnnotationMetadata annotationMetadata) { if (annotationMetadata.hasStereotype(AROUND_TYPE)) { return true; @@ -902,57 +908,97 @@ private void visitConfigurationPropertySetter(ExecutableElement method) { } /** - * @param beanMethodElement The bean method - * @param beanMethod The {@link ExecutableElement} + * @param element The element */ - void visitBeanFactoryMethod(JavaMethodElement beanMethodElement, ExecutableElement beanMethod) { - if (isFactoryType && annotationUtils.hasStereotype(concreteClass, AROUND_TYPE)) { - visitExecutableMethod(beanMethodElement, beanMethod, annotationUtils.getAnnotationMetadata(beanMethod)); + void visitBeanFactoryElement(Element element) { + final TypeMirror producedType; + if (element instanceof ExecutableElement) { + producedType = ((ExecutableElement) element).getReturnType(); + } else { + producedType = element.asType(); } - TypeElement producedElement = modelUtils.classElementFor(typeUtils.asElement(beanMethod.getReturnType())); - TypeElement factoryElement = modelUtils.classElementFor(beanMethod); - if (producedElement == null || factoryElement == null) { + TypeElement producedTypeElement = modelUtils.classElementFor(typeUtils.asElement(producedType)); + TypeElement factoryTypeElement = modelUtils.classElementFor(element); + + if (producedType.getKind().isPrimitive()) { + error(element, "Produced type from a bean factory cannot be primitive"); return; } - String producedTypeName = producedElement.getQualifiedName().toString(); + if (producedTypeElement == null || factoryTypeElement == null) { + return; + } + String producedTypeName = producedTypeElement.getQualifiedName().toString(); - TypeMirror returnType = beanMethod.getReturnType(); ClassElement declaringClassElement = elementFactory.newClassElement( - factoryElement, + factoryTypeElement, concreteClassMetadata ); AnnotationMetadata methodAnnotationMetadata = annotationUtils.newAnnotationBuilder().buildForParent( - producedElement, - beanMethod - ); - MethodElement javaMethodElement = elementFactory.newMethodElement( - declaringClassElement, - beanMethod, - methodAnnotationMetadata + producedTypeElement, + element ); - BeanDefinitionWriter beanMethodWriter = createFactoryBeanMethodWriterFor(beanMethod, producedElement); - Map> allTypeArguments = javaMethodElement.getGenericReturnType().getAllTypeArguments(); + io.micronaut.inject.ast.Element beanProducingElement; + ClassElement producedClassElement; + if (element instanceof ExecutableElement) { + final ExecutableElement executableElement = (ExecutableElement) element; + final JavaMethodElement methodElement = elementFactory.newMethodElement( + declaringClassElement, + executableElement, + methodAnnotationMetadata + ); + if (isFactoryType && annotationUtils.hasStereotype(concreteClass, AROUND_TYPE)) { + final JavaMethodElement aopMethod = elementFactory.newMethodElement( + declaringClassElement, + executableElement, + getMetadataHierarchy(methodAnnotationMetadata) + ); + visitExecutableMethod(aopMethod, executableElement, methodAnnotationMetadata); + } + + beanProducingElement = methodElement; + producedClassElement = methodElement.getGenericReturnType(); + } else { + final FieldElement fieldElement = elementFactory.newFieldElement( + declaringClassElement, + (VariableElement) element, + methodAnnotationMetadata + ); + beanProducingElement = fieldElement; + producedClassElement = fieldElement.getGenericField(); + } + + + BeanDefinitionWriter beanMethodWriter = createFactoryBeanMethodWriterFor(element, producedTypeElement); + Map> allTypeArguments = producedClassElement.getAllTypeArguments(); beanMethodWriter.visitTypeArguments(allTypeArguments); - beanDefinitionWriters.put(new DynamicName(javaMethodElement.getDescription(false)), beanMethodWriter); - beanMethodWriter.visitBeanFactoryMethod( - concreteClassElement, - javaMethodElement - ); + beanDefinitionWriters.put(new DynamicName(beanProducingElement.getDescription(false)), beanMethodWriter); + if (beanProducingElement instanceof MethodElement) { + beanMethodWriter.visitBeanFactoryMethod( + concreteClassElement, + (MethodElement) beanProducingElement + ); + } else { + beanMethodWriter.visitBeanFactoryField( + concreteClassElement, + (FieldElement) beanProducingElement + ); + } + if (methodAnnotationMetadata.hasStereotype(AROUND_TYPE) && !modelUtils.isAbstract(concreteClass)) { io.micronaut.core.annotation.AnnotationValue[] interceptorTypes = InterceptedMethodUtil.resolveInterceptorBinding(methodAnnotationMetadata, InterceptorKind.AROUND); - TypeElement returnTypeElement = (TypeElement) ((DeclaredType) beanMethod.getReturnType()).asElement(); - if (modelUtils.isFinal(returnTypeElement)) { - error(returnTypeElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + returnTypeElement); + if (producedClassElement.isFinal()) { + final Element nativeElement = (Element) producedClassElement.getNativeType(); + error(nativeElement, "Cannot apply AOP advice to final class. Class must be made non-final to support proxying: " + nativeElement); return; } - MethodElement constructor = javaMethodElement.getGenericReturnType().getPrimaryConstructor().orElse(null); + MethodElement constructor = producedClassElement.getPrimaryConstructor().orElse(null); OptionalValues aopSettings = methodAnnotationMetadata.getValues(AROUND_TYPE, Boolean.class); Map finalSettings = new LinkedHashMap<>(); for (CharSequence setting : aopSettings) { @@ -971,7 +1017,7 @@ void visitBeanFactoryMethod(JavaMethodElement beanMethodElement, ExecutableEleme ); proxyWriter.visitTypeArguments(allTypeArguments); - returnType.accept(new PublicMethodVisitor(javaVisitorContext) { + producedType.accept(new PublicMethodVisitor(javaVisitorContext) { @Override protected void accept(DeclaredType type, Element element, AopProxyWriter aopProxyWriter) { ExecutableElement method = (ExecutableElement) element; @@ -983,7 +1029,7 @@ protected void accept(DeclaredType type, Element element, AopProxyWriter aopProx AnnotationMetadata annotationMetadata; // if the method is annotated we build metadata for the method if (annotationUtils.isAnnotated(producedTypeName, method)) { - annotationMetadata = annotationUtils.getAnnotationMetadata(beanMethod, method); + annotationMetadata = annotationUtils.getAnnotationMetadata(element, method); } else { // otherwise we setup a reference to the parent metadata (essentially the annotations declared on the bean factory method) annotationMetadata = new AnnotationMetadataReference( @@ -1005,9 +1051,9 @@ protected void accept(DeclaredType type, Element element, AopProxyWriter aopProx } }, proxyWriter); } else if (methodAnnotationMetadata.hasStereotype(Executable.class)) { - DeclaredType dt = (DeclaredType) returnType; + DeclaredType dt = (DeclaredType) producedType; Map> finalBeanTypeArgumentsMirrors = genericUtils.buildGenericTypeArgumentElementInfo(dt.asElement(), dt, Collections.emptyMap()); - returnType.accept(new PublicMethodVisitor(javaVisitorContext) { + producedType.accept(new PublicMethodVisitor(javaVisitorContext) { @Override protected void accept(DeclaredType type, Element element, BeanDefinitionWriter beanWriter) { ExecutableElement method = (ExecutableElement) element; @@ -1020,7 +1066,7 @@ protected void accept(DeclaredType type, Element element, BeanDefinitionWriter b methodAnnotationMetadata ); - ClassElement declaringClassElement = elementFactory.newClassElement(producedElement, concreteClassMetadata); + ClassElement declaringClassElement = elementFactory.newClassElement(producedTypeElement, concreteClassMetadata); MethodElement executableMethod = elementFactory.newMethodElement( declaringClassElement, method, @@ -1043,7 +1089,7 @@ protected void accept(DeclaredType type, Element element, BeanDefinitionWriter b preDestroyMethod .ifPresent(destroyMethodName -> { if (StringUtils.isNotEmpty(destroyMethodName)) { - TypeElement destroyMethodDeclaringClass = (TypeElement) typeUtils.asElement(returnType); + TypeElement destroyMethodDeclaringClass = (TypeElement) typeUtils.asElement(producedType); ClassElement destroyMethodDeclaringElement = elementFactory.newClassElement(destroyMethodDeclaringClass, AnnotationMetadata.EMPTY_METADATA); final Optional destroyMethodRef = modelUtils.findAccessibleNoArgumentInstanceMethod(destroyMethodDeclaringClass, destroyMethodName); if (destroyMethodRef.isPresent()) { @@ -1060,7 +1106,7 @@ protected void accept(DeclaredType type, Element element, BeanDefinitionWriter b javaVisitorContext ); } else { - error(beanMethod, "@Bean method defines a preDestroy method that does not exist or is not public: " + destroyMethodName); + error(element, "@Bean method defines a preDestroy method that does not exist or is not public: " + destroyMethodName); } } @@ -1481,12 +1527,25 @@ public Object visitVariable(VariableElement variable, Object o) { } if (modelUtils.isStatic(variable)) { + AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getAnnotationMetadata(variable); + if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + error(variable, "Beans produced from fields cannot be static"); + } return null; } else if (modelUtils.isFinal(variable)) { AnnotationMetadata fieldAnnotationMetadata = annotationUtils.getAnnotationMetadata(variable); - boolean isConfigBuilder = fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class); - if (isConfigBuilder) { - visitConfigurationProperty(variable, fieldAnnotationMetadata); + if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (modelUtils.isPrivate(variable) || modelUtils.isProtected(variable)) { + error(variable, "Beans produced from fields cannot be private or protected"); + } else { + visitBeanFactoryElement(variable); + } + } else { + boolean isConfigBuilder = fieldAnnotationMetadata.hasStereotype(ConfigurationBuilder.class); + if (isConfigBuilder) { + visitConfigurationProperty(variable, fieldAnnotationMetadata); + } } return null; } @@ -1537,6 +1596,13 @@ public Object visitVariable(VariableElement variable, Object o) { } } else if (isConfigurationPropertiesType) { visitConfigurationProperty(variable, fieldAnnotationMetadata); + } else if (isFactoryType && fieldAnnotationMetadata.hasDeclaredStereotype(Bean.class)) { + // field factory for bean + if (modelUtils.isPrivate(variable) || modelUtils.isProtected(variable)) { + error(variable, "Beans produced from fields cannot be private or protected"); + } else { + visitBeanFactoryElement(variable); + } } return null; } @@ -1920,18 +1986,30 @@ private void tryAddAnnotationValue(Set additionalInterfaces, Annota } } - private BeanDefinitionWriter createFactoryBeanMethodWriterFor(ExecutableElement method, TypeElement producedElement) { + private BeanDefinitionWriter createFactoryBeanMethodWriterFor(Element method, TypeElement producedElement) { AnnotationMetadata annotationMetadata = annotationUtils.newAnnotationBuilder().buildForParent(producedElement, method, true); - final JavaMethodElement factoryMethodElement = elementFactory.newMethodElement( - concreteClassElement, - method, - annotationMetadata - ); + io.micronaut.inject.ast.Element factoryElement; + if (method instanceof ExecutableElement) { + factoryElement = elementFactory.newMethodElement( + concreteClassElement, + (ExecutableElement) method, + annotationMetadata + ); + + } else { + factoryElement = elementFactory.newFieldElement( + concreteClassElement, + (VariableElement) method, + annotationMetadata + ); + } return new BeanDefinitionWriter( - factoryMethodElement, - OriginatingElements.of(factoryMethodElement), - metadataBuilder, javaVisitorContext, - factoryMethodIndex.getAndIncrement()); + factoryElement, + OriginatingElements.of(factoryElement), + metadataBuilder, + javaVisitorContext, + factoryMethodIndex.getAndIncrement() + ); } private boolean shouldExclude(Set includes, Set excludes, String propertyName) { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy new file mode 100644 index 00000000000..d7f3004db05 --- /dev/null +++ b/inject-java/src/test/groovy/io/micronaut/inject/factory/beanfield/FactoryBeanFieldSpec.groovy @@ -0,0 +1,142 @@ +package io.micronaut.inject.factory.beanfield + +import io.micronaut.annotation.processing.test.AbstractTypeElementSpec +import io.micronaut.context.ApplicationContext +import io.micronaut.inject.qualifiers.Qualifiers +import spock.lang.Unroll + +class FactoryBeanFieldSpec extends AbstractTypeElementSpec { + void "test a factory bean can be supplied from a field"() { + given: + ApplicationContext context = buildContext('test.TestFactory$TestField', '''\ +package test; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; + +import io.micronaut.inject.annotation.*; +import io.micronaut.aop.*; +import io.micronaut.context.annotation.*; +import io.micronaut.inject.factory.enummethod.TestEnum; +import javax.inject.*; + +@Factory +class TestFactory$TestField { + + @Singleton + @Bean + @io.micronaut.context.annotation.Primary + Foo one = new Foo("one"); + + // final fields are implicitly singleton + @Bean + @Named("two") + final Foo two = new Foo("two"); + + // non-final fields are prototype + @Bean + @Named("three") + Foo three = new Foo("three"); + + @SomeMeta + @Bean + Foo four = new Foo("four"); +} + +class Foo { + final String name; + Foo(String name) { + this.name = name; + } +} + +@Retention(RUNTIME) +@Singleton +@Named("four") +@AroundConstruct +@interface SomeMeta { +} + +@Singleton +@InterceptorBean(SomeMeta.class) +class TestConstructInterceptor implements ConstructorInterceptor { + boolean invoked = false; + Object[] parameters; + + @Override + public Object intercept(ConstructorInvocationContext context) { + invoked = true; + parameters = context.getParameterValues(); + return context.proceed(); + } +} +''') + + expect: + + getBean(context, "test.Foo").name == 'one' + getBean(context, "test.Foo", Qualifiers.byName("two")).name == 'two' + getBean(context, "test.Foo", Qualifiers.byName("two")).is( + getBean(context, "test.Foo", Qualifiers.byName("two")) + ) + getBean(context, "test.Foo", Qualifiers.byName("three")).is( + getBean(context, "test.Foo", Qualifiers.byName("three")) + ) + getBean(context, 'test.TestConstructInterceptor').invoked == false + getBean(context, "test.Foo", Qualifiers.byName("four")) // around construct + getBean(context, 'test.TestConstructInterceptor').invoked == true + + cleanup: + context.close() + } + + void 'test fail compilation on bean field primitive'() { + when: + buildBeanDefinition('testprim.TestFactory', ''' +package testprim; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.inject.*; + +@Factory +class TestFactory { + @Bean + int junk; +} +''') + + then: + def e = thrown(RuntimeException) + e.message.contains('Produced type from a bean factory cannot be primitive') + } + + @Unroll + void 'test fail compilation on invalid modifier #modifier'() { + when: + buildBeanDefinition('invalidmod.TestFactory', """ +package invalidmod; + +import io.micronaut.inject.annotation.*; +import io.micronaut.context.annotation.*; +import javax.inject.*; + +@Factory +class TestFactory { + @Bean + $modifier Test test; +} + +class Test {} +""") + + then: + def e = thrown(RuntimeException) + e.message.contains("cannot be ") + e.message.contains(modifier) + + where: + modifier << ['private', 'protected', 'static'] + } +} diff --git a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java index c3f9f7707e6..1a0b6bf54f0 100644 --- a/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/AbstractBeanDefinition.java @@ -83,6 +83,7 @@ public class AbstractBeanDefinition extends AbstractBeanContextConditional im private final Class type; private final boolean isAbstract; private final boolean isConfigurationProperties; + private final boolean singleton; private final Class declaringType; private final ConstructorInjectionPoint constructor; private final Collection> requiredComponents = new HashSet<>(3); @@ -90,6 +91,39 @@ public class AbstractBeanDefinition extends AbstractBeanContextConditional im private Environment environment; private Set> exposedTypes; + /** + * Constructs a bean definition that is produced from a method call on another type (factory bean). + * + * @param producedType The produced type + * @param declaringType The declaring type of the method + * @param fieldName The method name + * @param fieldMetadata The metadata for the method + * @param isFinal Is the field final + * @since 3.0 + */ + @SuppressWarnings({"WeakerAccess"}) + @Internal + @UsedByGeneratedCode + protected AbstractBeanDefinition(Class producedType, + Class declaringType, + String fieldName, + AnnotationMetadata fieldMetadata, + boolean isFinal) { + this.type = producedType; + this.isAbstract = false; // factory beans are never abstract + this.declaringType = declaringType; + + this.constructor = new DefaultFieldConstructorInjectionPoint<>( + this, + declaringType, + producedType, + fieldName, + fieldMetadata + ); + this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); + this.singleton = isFinal; + } + /** * Constructs a bean definition that is produced from a method call on another type (factory bean). * @@ -132,6 +166,7 @@ protected AbstractBeanDefinition(Class producedType, } this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); this.addRequiredComponents(arguments); + this.singleton = getAnnotationMetadata().hasDeclaredStereotype(Singleton.class); } /** @@ -168,6 +203,7 @@ protected AbstractBeanDefinition(Class type, } this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable(); this.addRequiredComponents(arguments); + this.singleton = getAnnotationMetadata().hasDeclaredStereotype(Singleton.class); } @Override @@ -264,7 +300,7 @@ public boolean isProvided() { @Override public boolean isSingleton() { - return getAnnotationMetadata().hasDeclaredStereotype(Singleton.class); + return singleton; } @Override diff --git a/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java b/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java new file mode 100644 index 00000000000..bbc25771c3e --- /dev/null +++ b/inject/src/main/java/io/micronaut/context/DefaultFieldConstructorInjectionPoint.java @@ -0,0 +1,60 @@ +/* + * Copyright 2017-2021 original authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.micronaut.context; + +import io.micronaut.core.annotation.AnnotationMetadata; +import io.micronaut.core.annotation.Internal; +import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.type.Argument; +import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.ConstructorInjectionPoint; + + +/** + * Represents an injection point for a bean produced from a field. + * + * @param The field type + * @author Graeme Rocher + * @since 3.0 + */ +@Internal +final class DefaultFieldConstructorInjectionPoint extends DefaultFieldInjectionPoint implements ConstructorInjectionPoint { + /** + * @param declaringBean The declaring bean + * @param declaringType The declaring type + * @param fieldType The field type + * @param field The name of the field + * @param annotationMetadata The annotation metadata + */ + DefaultFieldConstructorInjectionPoint( + BeanDefinition declaringBean, + Class declaringType, + Class fieldType, + String field, + @Nullable AnnotationMetadata annotationMetadata) { + super(declaringBean, declaringType, fieldType, field, annotationMetadata, Argument.ZERO_ARGUMENTS); + } + + @Override + public Argument[] getArguments() { + return Argument.ZERO_ARGUMENTS; + } + + @Override + public T invoke(Object... args) { + throw new UnsupportedOperationException("Use BeanFactory.instantiate(..) instead"); + } +} diff --git a/inject/src/main/java/io/micronaut/context/annotation/Bean.java b/inject/src/main/java/io/micronaut/context/annotation/Bean.java index f5133c67c6f..794191bf00b 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Bean.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Bean.java @@ -41,7 +41,7 @@ */ @Documented @Retention(RUNTIME) -@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE, ElementType.TYPE, ElementType.FIELD}) public @interface Bean { /** diff --git a/inject/src/main/java/io/micronaut/context/annotation/Requirements.java b/inject/src/main/java/io/micronaut/context/annotation/Requirements.java index 5e0b22c4bd2..38f5cd42e54 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Requirements.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Requirements.java @@ -29,7 +29,7 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) public @interface Requirements { /** diff --git a/inject/src/main/java/io/micronaut/context/annotation/Requires.java b/inject/src/main/java/io/micronaut/context/annotation/Requires.java index d69730d38fc..c90f19112d3 100644 --- a/inject/src/main/java/io/micronaut/context/annotation/Requires.java +++ b/inject/src/main/java/io/micronaut/context/annotation/Requires.java @@ -35,7 +35,7 @@ */ @Documented @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD}) +@Target({ElementType.PACKAGE, ElementType.TYPE, ElementType.ANNOTATION_TYPE, ElementType.METHOD, ElementType.FIELD}) @Repeatable(Requirements.class) public @interface Requires { diff --git a/inject/src/main/java/io/micronaut/inject/ast/Element.java b/inject/src/main/java/io/micronaut/inject/ast/Element.java index 03a43596123..181eb979339 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/Element.java +++ b/inject/src/main/java/io/micronaut/inject/ast/Element.java @@ -18,6 +18,7 @@ import io.micronaut.core.annotation.AnnotatedElement; import io.micronaut.core.annotation.AnnotationMetadataDelegate; import io.micronaut.core.annotation.AnnotationValueBuilder; +import io.micronaut.core.naming.Described; import io.micronaut.core.util.ArgumentUtils; import io.micronaut.core.annotation.NonNull; @@ -32,7 +33,7 @@ * @author graemerocher * @since 1.0 */ -public interface Element extends AnnotationMetadataDelegate, AnnotatedElement { +public interface Element extends AnnotationMetadataDelegate, AnnotatedElement, Described { /** * An empty array of elements. * @since 2.1.1 @@ -170,4 +171,20 @@ default boolean isPrivate() { default boolean isFinal() { return false; } + + @NonNull + @Override + default String getDescription() { + return getDescription(true); + } + + @NonNull + @Override + default String getDescription(boolean simple) { + if (simple) { + return getSimpleName(); + } else { + return getName(); + } + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java index 623c7956890..2afd25c3e3a 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/FieldElement.java @@ -15,6 +15,8 @@ */ package io.micronaut.inject.ast; +import io.micronaut.core.annotation.NonNull; + /** * Stores data about an element that references a field. * @@ -29,4 +31,14 @@ public interface FieldElement extends TypedElement, MemberElement { default ClassElement getGenericField() { return getGenericType(); } + + @NonNull + @Override + default String getDescription(boolean simple) { + if (simple) { + return getType().getSimpleName() + " " + getName(); + } else { + return getType().getName() + " " + getName(); + } + } } diff --git a/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java b/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java index db947939a91..9b5a0047a6f 100644 --- a/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java +++ b/inject/src/main/java/io/micronaut/inject/ast/ParameterElement.java @@ -33,6 +33,16 @@ public interface ParameterElement extends TypedElement { @NonNull ClassElement getType(); + @NonNull + @Override + default String getDescription(boolean simple) { + if (simple) { + return getType().getSimpleName() + " " + getName(); + } else { + return getType().getName() + " " + getName(); + } + } + /** * Creates a parameter element for a simple type and name. * diff --git a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java index 3fcf2ad0f51..88a36754359 100644 --- a/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java +++ b/inject/src/main/java/io/micronaut/inject/writer/BeanDefinitionWriter.java @@ -167,6 +167,10 @@ public class BeanDefinitionWriter extends AbstractClassFileWriter implements Bea Class.class, Class.class, String.class, AnnotationMetadata.class, boolean.class, Argument[].class )); + private static final org.objectweb.asm.commons.Method BEAN_DEFINITION_FIELD_CONSTRUCTOR = new org.objectweb.asm.commons.Method(CONSTRUCTOR_NAME, getConstructorDescriptor( + Class.class, Class.class, String.class, AnnotationMetadata.class, boolean.class + )); + private static final Type TYPE_ABSTRACT_BEAN_DEFINITION = Type.getType(AbstractBeanDefinition.class); private static final Type TYPE_ABSTRACT_PARAMETRIZED_BEAN_DEFINITION = Type.getType(AbstractParametrizedBeanDefinition.class); private static final org.objectweb.asm.commons.Method METHOD_OPTIONAL_EMPTY = org.objectweb.asm.commons.Method.getMethod( @@ -334,6 +338,21 @@ public BeanDefinitionWriter(Element beanProducingElement, } final ClassElement declaringType = factoryMethodElement.getDeclaringType(); this.beanDefinitionName = declaringType.getPackageName() + ".$" + declaringType.getSimpleName() + "$" + upperCaseMethodName + uniqueIdentifier + "Definition"; + } else if (beanProducingElement instanceof FieldElement) { + FieldElement factoryMethodElement = (FieldElement) beanProducingElement; + final ClassElement producedElement = factoryMethodElement.getGenericField(); + this.beanTypeElement = producedElement; + this.packageName = producedElement.getPackageName(); + this.isInterface = producedElement.isInterface(); + this.beanFullClassName = producedElement.getName(); + this.beanSimpleClassName = producedElement.getSimpleName(); + this.providedBeanClassName = producedElement.getName(); + String fieldName = NameUtils.capitalize(factoryMethodElement.getName()); + if (uniqueIdentifier == null) { + throw new IllegalArgumentException("Factory fields require passing a unique identifier"); + } + final ClassElement declaringType = factoryMethodElement.getDeclaringType(); + this.beanDefinitionName = declaringType.getPackageName() + ".$" + declaringType.getSimpleName() + "$" + fieldName + uniqueIdentifier + "Definition"; } else { throw new IllegalArgumentException("Unsupported element type: " + beanProducingElement.getClass().getName()); } @@ -480,7 +499,7 @@ public String getBeanDefinitionClassFile() { * {@link io.micronaut.context.annotation.Bean} this method should be called.

* * @param factoryClass The factory class - * @param factoryMethod The factor method + * @param factoryMethod The factory method */ public void visitBeanFactoryMethod(ClassElement factoryClass, MethodElement factoryMethod) { @@ -504,6 +523,35 @@ public void visitBeanFactoryMethod(ClassElement factoryClass, } } + /** + *

In the case where the produced class is produced by a factory field annotated with + * {@link io.micronaut.context.annotation.Bean} this method should be called.

+ * + * @param factoryClass The factory class + * @param factoryField The factory field + */ + public void visitBeanFactoryField(ClassElement factoryClass, FieldElement factoryField) { + if (constructorVisitor != null) { + throw new IllegalStateException("Only a single call to visitBeanFactoryMethod(..) is permitted"); + } else { + // now prepare the implementation of the build method. See BeanFactory interface + visitBuildFactoryMethodDefinition(factoryClass, factoryField); + + // now implement the constructor + buildFactoryFieldClassConstructor( + factoryClass, + factoryField.getGenericField(), + factoryField.getName(), + factoryField.getAnnotationMetadata(), + factoryField.isFinal() + ); + + // now override the injectBean method + visitInjectMethodDefinition(); + } + } + + /** * Visits the constructor used to create the bean definition. * @@ -1402,6 +1450,51 @@ private void pushGetValueForPathCall(GeneratorAdapter injectMethodVisitor, Class injectMethodVisitor.visitVarInsn(ALOAD, optionalInstanceIndex); } + private void buildFactoryFieldClassConstructor( + ClassElement factoryClass, + ClassElement producedType, + String fieldName, + AnnotationMetadata fieldAnnotationMetadata, + boolean isFinal) { + Type factoryTypeRef = JavaModelUtils.getTypeReference(factoryClass); + Type producedTypeRef = JavaModelUtils.getTypeReference(producedType); + this.constructorVisitor = buildProtectedConstructor(BEAN_DEFINITION_FIELD_CONSTRUCTOR); + + GeneratorAdapter defaultConstructor = new GeneratorAdapter( + startConstructor(classWriter), + ACC_PUBLIC, + CONSTRUCTOR_NAME, + DESCRIPTOR_DEFAULT_CONSTRUCTOR + ); + + // ALOAD 0 + defaultConstructor.loadThis(); + + // 1st argument: The factory type + defaultConstructor.push(producedTypeRef); + + // 2nd argument: the produced type + defaultConstructor.push(factoryTypeRef); + + // 3rd argument: The field name + defaultConstructor.push(fieldName); + + // 4th argument: The annotation metadata + pushAnnotationMetadata(fieldAnnotationMetadata, defaultConstructor); + + // 5th argument: is final + defaultConstructor.push(isFinal); + + defaultConstructor.invokeConstructor( + beanDefinitionType, + BEAN_DEFINITION_FIELD_CONSTRUCTOR + ); + + defaultConstructor.visitInsn(RETURN); + defaultConstructor.visitMaxs(DEFAULT_MAX_STACK, 1); + defaultConstructor.visitEnd(); + } + private void buildFactoryMethodClassConstructor( ClassElement factoryClass, ClassElement producedType, @@ -2144,11 +2237,18 @@ private void invokeSuperInjectMethod(MethodVisitor methodVisitor, Method methodT private void visitBuildFactoryMethodDefinition( ClassElement factoryClass, - MethodElement factoryMethod) { + Element factoryMethod) { if (buildMethodVisitor == null) { - ParameterElement[] parameters = factoryMethod.getParameters(); + ParameterElement[] parameters; + + if (factoryMethod instanceof MethodElement) { + parameters = ((MethodElement) factoryMethod).getParameters(); + } else { + parameters = new ParameterElement[0]; + } + List parameterList = Arrays.asList(parameters); - boolean isParametrized = isParametrized(factoryMethod); + boolean isParametrized = isParametrized(parameters); boolean isIntercepted = isConstructorIntercepted(factoryMethod); Type factoryType = JavaModelUtils.getTypeReference(factoryClass); @@ -2190,12 +2290,20 @@ private void visitBuildFactoryMethodDefinition( } else { if (!parameterList.isEmpty()) { - pushConstructorArguments(buildMethodVisitor, factoryMethod); + pushConstructorArguments(buildMethodVisitor, parameters); + } + if (factoryMethod instanceof MethodElement) { + buildMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, + factoryType.getInternalName(), + factoryMethod.getName(), + methodDescriptor, false); + } else { + buildMethodVisitor.getField( + factoryType, + factoryMethod.getName(), + beanType + ); } - buildMethodVisitor.visitMethodInsn(INVOKEVIRTUAL, - factoryType.getInternalName(), - factoryMethod.getName(), - methodDescriptor, false); } @@ -2210,9 +2318,10 @@ private void visitBuildFactoryMethodDefinition( private void visitBuildMethodDefinition(MethodElement constructor) { if (buildMethodVisitor == null) { - boolean isParametrized = isParametrized(constructor); boolean isIntercepted = isConstructorIntercepted(constructor); - List parameters = Arrays.asList(constructor.getParameters()); + final ParameterElement[] parameterArray = constructor.getParameters(); + List parameters = Arrays.asList(parameterArray); + boolean isParametrized = isParametrized(parameterArray); defineBuilderMethod(isParametrized); // load this @@ -2229,7 +2338,7 @@ private void visitBuildMethodDefinition(MethodElement constructor) { } else { buildMethodVisitor.visitTypeInsn(NEW, beanType.getInternalName()); buildMethodVisitor.visitInsn(DUP); - pushConstructorArguments(buildMethodVisitor, constructor); + pushConstructorArguments(buildMethodVisitor, parameterArray); String constructorDescriptor = getConstructorDescriptor(parameters); buildMethodVisitor.visitMethodInsn(INVOKESPECIAL, beanType.getInternalName(), "", constructorDescriptor, false); } @@ -2368,13 +2477,18 @@ private void initInterceptedConstructorWriter( } if (hasFactoryMethod) { - invokeMethod.visitMethodInsn( - INVOKEVIRTUAL, - factoryType.getInternalName(), - factoryMethodDef.factoryMethod.getName(), - factoryMethodDef.methodDescriptor, - false - ); + if (factoryMethodDef.factoryMethod instanceof MethodElement) { + + invokeMethod.visitMethodInsn( + INVOKEVIRTUAL, + factoryType.getInternalName(), + factoryMethodDef.factoryMethod.getName(), + factoryMethodDef.methodDescriptor, + false + ); + } else { + invokeMethod.getField(factoryType, factoryMethodDef.factoryMethod.getName(), beanType); + } } else { String constructorDescriptor = getConstructorDescriptor(parameters); invokeMethod.visitMethodInsn(INVOKESPECIAL, beanType.getInternalName(), "", constructorDescriptor, false); @@ -2465,7 +2579,7 @@ private int createParameterArray(List parameters, GeneratorAda return pushNewBuildLocalVariable(); } - private boolean isConstructorIntercepted(MethodElement constructor) { + private boolean isConstructorIntercepted(Element constructor) { // a constructor is intercepted when this bean is an advised type but not proxied // and any AROUND_CONSTRUCT annotations are present AnnotationMetadataHierarchy annotationMetadata = new AnnotationMetadataHierarchy(this.annotationMetadata, constructor.getAnnotationMetadata()); @@ -2513,8 +2627,7 @@ private boolean isInterceptedLifeCycleByType(AnnotationMetadata annotationMetada } private void pushConstructorArguments(GeneratorAdapter buildMethodVisitor, - MethodElement constructor) { - ParameterElement[] parameters = constructor.getParameters(); + ParameterElement[] parameters) { int size = parameters.length; if (size > 0) { for (int i = 0; i < parameters.length; i++) { @@ -2590,8 +2703,8 @@ private boolean isAnnotatedWithParameter(AnnotationMetadata annotationMetadata) return false; } - private boolean isParametrized(MethodElement constructor) { - return Arrays.stream(constructor.getParameters()).anyMatch(p -> isAnnotatedWithParameter(p.getAnnotationMetadata())); + private boolean isParametrized(ParameterElement...parameters) { + return Arrays.stream(parameters).anyMatch(p -> isAnnotatedWithParameter(p.getAnnotationMetadata())); } private void defineBuilderMethod(boolean isParametrized) { @@ -2903,11 +3016,11 @@ public boolean isRequiresReflection() { private class FactoryMethodDef { private final Type factoryType; - private final MethodElement factoryMethod; + private final Element factoryMethod; private final String methodDescriptor; private final int factoryVar; - public FactoryMethodDef(Type factoryType, MethodElement factoryMethod, String methodDescriptor, int factoryVar) { + public FactoryMethodDef(Type factoryType, Element factoryMethod, String methodDescriptor, int factoryVar) { this.factoryType = factoryType; this.factoryMethod = factoryMethod; this.methodDescriptor = methodDescriptor; diff --git a/src/main/docs/guide/introduction/whatsNew.adoc b/src/main/docs/guide/introduction/whatsNew.adoc index 913cb03e59f..ba3c55e3927 100644 --- a/src/main/docs/guide/introduction/whatsNew.adoc +++ b/src/main/docs/guide/introduction/whatsNew.adoc @@ -24,6 +24,14 @@ snippet::io.micronaut.docs.inject.typed.V8Engine[tags="class",indent=0] For more information see the section on <>. +==== Factories can produce bean from fields + +Beans defined with the ann:context.annotation.Factory[] annotation can now produce beans from public or package protected fields, for example: + +snippet::io.micronaut.docs.factories.VehicleMockSpec[tags="class",indent=0] + +For more information see the <> section of the documentation. + === AOP Features ==== Support for Constructor Interception diff --git a/src/main/docs/guide/ioc/factories.adoc b/src/main/docs/guide/ioc/factories.adoc index 5c2dd60ae05..1addea9c28f 100644 --- a/src/main/docs/guide/ioc/factories.adoc +++ b/src/main/docs/guide/ioc/factories.adoc @@ -45,3 +45,17 @@ snippet::io.micronaut.docs.injectionpoint.EngineFactory[tags="class",indent=0] <3> The value is used to construct an engine, throwing an exception if an engine cannot be constructed. NOTE: It is important to note that the factory is declared as ann:context.annotation.Prototype[] scope so the method is invoked for each injection point. If the `V8Engine` and `V6Engine` types are required to be singletons, the factory should use a Map to ensure the objects are only constructed once. + +=== Beans from Fields + +With Micronaut 3.0 or above it is also possible to produce beans from fields by declaring the ann:context.annotation.Bean[] annotation on a field. + +Whilst generally this approach should be discouraged in favour for factory methods, which provide more flexibility it does simplify testing code. For example with bean fields you can easily produce mocks in your test code: + +snippet::io.micronaut.docs.factories.VehicleMockSpec[tags="imports, class",indent=0] + +<1> A bean is defined from a field that replaces the existing `Engine`. +<2> The `Vehicle` is injected. +<3> The code asserts that the mock implementation is called. + +Note that only public or package protected fields are supported on non-primitive types. If the field is `static`, `private`, or `protected` a compilation error will occur. \ No newline at end of file diff --git a/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/VehicleMockSpec.groovy b/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/VehicleMockSpec.groovy new file mode 100644 index 00000000000..bd38d7b075e --- /dev/null +++ b/test-suite-groovy/src/test/groovy/io/micronaut/docs/factories/VehicleMockSpec.groovy @@ -0,0 +1,27 @@ +package io.micronaut.docs.factories + +// tag::imports[] +import io.micronaut.context.annotation.* +import io.micronaut.test.extensions.spock.annotation.MicronautTest +import spock.lang.Specification +import javax.inject.Inject +// end::imports[] + +// tag::class[] +@MicronautTest +class VehicleMockSpec extends Specification { + @Requires(beans=VehicleMockSpec.class) + @Bean @Replaces(Engine.class) + Engine mockEngine = {-> "Mock Started" } as Engine // <1> + + @Inject Vehicle vehicle // <2> + + void "test start engine"() { + given: + final String result = vehicle.start() + + expect: + result == "Mock Started" // <3> + } +} +// end::class[] \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Engine.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Engine.kt index 4eb0958b209..bb0a39a7f6f 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Engine.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Engine.kt @@ -16,7 +16,7 @@ package io.micronaut.docs.factories // tag::class[] -internal interface Engine { +interface Engine { fun start(): String } // end::class[] \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt index 610fe11edbb..58d9250238c 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/Vehicle.kt @@ -19,7 +19,7 @@ import javax.inject.Singleton // tag::class[] @Singleton -internal class Vehicle(val engine: Engine) { +class Vehicle(val engine: Engine) { fun start(): String { return engine.start() diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt new file mode 100644 index 00000000000..fc33cd15728 --- /dev/null +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/factories/VehicleMockSpec.kt @@ -0,0 +1,32 @@ +package io.micronaut.docs.factories + +// tag::imports[] +import io.micronaut.context.annotation.Bean +import io.micronaut.context.annotation.Replaces +import io.micronaut.test.extensions.junit5.annotation.MicronautTest +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Test +import javax.inject.Inject +// end::imports[] + +// tag::class[] +@MicronautTest +class VehicleMockSpec { + @get:Bean + @get:Replaces(Engine::class) + val mockEngine: Engine = object : Engine { // <1> + override fun start(): String { + return "Mock Started" + } + } + + @Inject + lateinit var vehicle : Vehicle // <2> + + @Test + fun testStartEngine() { + val result = vehicle.start() + Assertions.assertEquals("Mock Started", result) // <3> + } +} +// tag::class[] \ No newline at end of file diff --git a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt index b8cbdd25d17..267bff45709 100644 --- a/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt +++ b/test-suite-kotlin/src/test/kotlin/io/micronaut/docs/inject/typed/V8Engine.kt @@ -13,4 +13,4 @@ class V8Engine : Engine { // <2> override val cylinders: Int = 8 } -// ebd::class[] \ No newline at end of file +// end::class[] \ No newline at end of file diff --git a/test-suite/src/test/java/io/micronaut/docs/factories/VehicleMockSpec.java b/test-suite/src/test/java/io/micronaut/docs/factories/VehicleMockSpec.java new file mode 100644 index 00000000000..eef8100ff55 --- /dev/null +++ b/test-suite/src/test/java/io/micronaut/docs/factories/VehicleMockSpec.java @@ -0,0 +1,27 @@ +package io.micronaut.docs.factories; + +// tag::imports[] +import io.micronaut.context.annotation.*; +import io.micronaut.test.extensions.junit5.annotation.MicronautTest; +import org.junit.jupiter.api.Test; +import javax.inject.Inject; + +import static org.junit.jupiter.api.Assertions.assertEquals; +// end::imports[] + +// tag::class[] +@MicronautTest +public class VehicleMockSpec { + @Requires(beans=VehicleMockSpec.class) + @Bean @Replaces(Engine.class) + Engine mockEngine = () -> "Mock Started"; // <1> + + @Inject Vehicle vehicle; // <2> + + @Test + void testStartEngine() { + final String result = vehicle.start(); + assertEquals("Mock Started", result); // <3> + } +} +// end::class[] \ No newline at end of file