Skip to content

Commit

Permalink
Add support for producing beans from fields (micronaut-projects#5343)
Browse files Browse the repository at this point in the history
* Support for beans produced from fields

* cleanup

* fix failing tests

* checkstyle
  • Loading branch information
graemerocher authored Apr 30, 2021
1 parent a5883cb commit d46beb7
Show file tree
Hide file tree
Showing 22 changed files with 1,007 additions and 208 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ public DefaultInterceptorRegistry(BeanContext beanContext) {

@Override
@NonNull
public <T> Interceptor<T, T>[] resolveConstructorInterceptors(
public <T> Interceptor<T, T>[] resolveConstructorInterceptors(
@NonNull BeanConstructor<T> constructor,
@NonNull Collection<BeanRegistration<Interceptor<T, T>>> interceptors) {
instrumentAnnotationMetadata(beanContext, constructor);
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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<Object> {
boolean invoked = false;
Object[] parameters;
@Override
public Object intercept(ConstructorInvocationContext<Object> 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']
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -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<Object> {
boolean invoked = false;
Object[] parameters;
@Override
public Object intercept(ConstructorInvocationContext<Object> 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']
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,13 +83,47 @@ public class AbstractBeanDefinition<T> extends AbstractBeanContextConditional im
private final Class<T> type;
private final boolean isAbstract;
private final boolean isConfigurationProperties;
private final boolean singleton;
private final Class<?> declaringType;
private final ConstructorInjectionPoint<T> constructor;
private final Collection<Class<?>> requiredComponents = new HashSet<>(3);
private AnnotationMetadata beanAnnotationMetadata;
private Environment environment;
private Set<Class<?>> 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<T> 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).
*
Expand Down Expand Up @@ -132,6 +166,7 @@ protected AbstractBeanDefinition(Class<T> producedType,
}
this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable();
this.addRequiredComponents(arguments);
this.singleton = getAnnotationMetadata().hasDeclaredStereotype(Singleton.class);
}

/**
Expand Down Expand Up @@ -168,6 +203,7 @@ protected AbstractBeanDefinition(Class<T> type,
}
this.isConfigurationProperties = hasStereotype(ConfigurationReader.class) || isIterable();
this.addRequiredComponents(arguments);
this.singleton = getAnnotationMetadata().hasDeclaredStereotype(Singleton.class);
}

@Override
Expand Down Expand Up @@ -264,7 +300,7 @@ public boolean isProvided() {

@Override
public boolean isSingleton() {
return getAnnotationMetadata().hasDeclaredStereotype(Singleton.class);
return singleton;
}

@Override
Expand Down
Loading

0 comments on commit d46beb7

Please sign in to comment.