diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy index 14f01470fdd..ed39e634e41 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/constructor/ConstructorFactorySpec.groovy @@ -32,8 +32,7 @@ class ConstructorFactorySpec extends Specification { void "test injection with constructor supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a constructor that depends on a bean provided by a provider" B b = context.getBean(B) @@ -44,7 +43,10 @@ class ConstructorFactorySpec extends Specification { b.a.c != null b.a.c2 != null b.a.d != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy index a6a010bc797..0ca4609f55c 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldArrayFactorySpec.groovy @@ -31,8 +31,7 @@ class FieldArrayFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -42,7 +41,10 @@ class FieldArrayFactorySpec extends Specification { b.all[0] instanceof AImpl b.all[0].c != null b.all[0].c2 != null - b.all[0].is(context.getBean(AImpl)) + b.all[0].is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy index ad0bf11429a..0c4e4142430 100644 --- a/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy +++ b/inject-groovy/src/test/groovy/io/micronaut/inject/field/FieldFactorySpec.groovy @@ -31,8 +31,7 @@ class FieldFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -42,7 +41,10 @@ class FieldFactorySpec extends Specification { b.a instanceof AImpl b.a.c != null b.a.c2 != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } static interface A { diff --git a/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy index 0fa5f561b6a..6c84ab7001c 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/constructor/factoryinjection/ConstructorFactorySpec.groovy @@ -23,8 +23,7 @@ class ConstructorFactorySpec extends Specification { void "test injection with constructor supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a constructor that depends on a bean provided by a provider" B b = context.getBean(B) @@ -35,6 +34,9 @@ class ConstructorFactorySpec extends Specification { b.a.c != null b.a.c2 != null b.a.d != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy index 60fe2ed00c8..747d56ba04e 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/context/RegisterSingletonSpec.groovy @@ -17,16 +17,77 @@ package io.micronaut.inject.context import io.micronaut.context.BeanContext import io.micronaut.context.DefaultBeanContext +import io.micronaut.context.RuntimeBeanDefinition +import io.micronaut.context.annotation.Bean import io.micronaut.context.annotation.Type +import io.micronaut.core.type.Argument import io.micronaut.inject.qualifiers.Qualifiers +import jakarta.inject.Named +import jakarta.inject.Singleton import spock.lang.Issue import spock.lang.Specification +import java.lang.reflect.Proxy + class RegisterSingletonSpec extends Specification { + void "test register singleton with generic types"() { + given: + BeanContext context = BeanContext.run() + + when: + context.registerSingleton(new TestReporter()) + + then: + context.containsBean(Argument.of(Reporter, Span)) + + cleanup: + context.close() + } + + void "test register singleton and exposed type"() { + given: + BeanContext context = BeanContext.run() + + when: + context.registerBeanDefinition( + RuntimeBeanDefinition.builder(Codec, ()-> new OverridingCodec()) + .singleton(true) + .qualifier(Qualifiers.byName("foo")) + .replaces(ToBeReplacedCodec) + .build() + ) // replaces ToBeReplacedCodec + context.registerSingleton(Codec, { } as Codec) // adds a new codec + context.registerSingleton(Codec, new FooCodec()) // adds another codec + context.registerSingleton(new BarCodec()) // should be registered with bean type BarCodec + context.registerSingleton(Codec, new BazCodec(), Qualifiers.byName("baz")) + + then: + def codecs = context.getBeansOfType(Codec) + codecs.size() == 7 + codecs.find { it in FooCodec } + codecs.find { it in BarCodec } + codecs.find { it in BazCodec } + !codecs.find { it in ToBeReplacedCodec } + codecs.find { it in OverridingCodec } + codecs.find { it in OtherCodec } + codecs.find { it in StuffCodec } + codecs.find { it in Proxy } + codecs == context.getBeansOfType(Codec) // second resolve returns the same result + context.getBeansOfType(FooCodec).size() == 0 // not an exposed type + context.getBeansOfType(BarCodec).size() == 1 // BarCodec type is exposed + context.findBean(FooCodec).isEmpty() // not an exposed type + context.findBean(StuffCodec).isEmpty() // not an exposed type + context.findBean(OtherCodec).isPresent() // an exposed type + + cleanup: + context.close() + } + + void "test register singleton method"() { given: - BeanContext context = new DefaultBeanContext().start() + BeanContext context = BeanContext.run() def b = new B() when: @@ -83,4 +144,26 @@ class RegisterSingletonSpec extends Specification { this.type = type } } + + static interface Codec { + + } + + static class OverridingCodec implements Codec {} + static class FooCodec implements Codec {} + static class BarCodec implements Codec {} + static class BazCodec implements Codec {} + @Singleton + @Bean(typed = Codec) + static class StuffCodec implements Codec {} + @Singleton + static class OtherCodec implements Codec {} + + @Singleton + @Named("foo") + static class ToBeReplacedCodec implements Codec {} + + static interface Reporter {} + static class Span {} + static class TestReporter implements Reporter {} } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy index 2f4457f2cab..10929fa95ae 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/arrayfactoryinjection/FieldArrayFactorySpec.groovy @@ -35,7 +35,7 @@ class FieldArrayFactorySpec extends Specification { b.all[0] instanceof AImpl ((AImpl)b.all[0]).c != null ((AImpl)b.all[0]).c2 != null - b.all[0].is(context.getBean(AImpl)) + b.all[0].is(context.getBean(A)) } } diff --git a/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy b/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy index 4009bd4881c..0e950646dad 100644 --- a/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy +++ b/inject-java/src/test/groovy/io/micronaut/inject/field/factoryinjection/FieldFactorySpec.groovy @@ -25,8 +25,7 @@ class FieldFactorySpec extends Specification { void "test injection with field supplied by a provider"() { given: - BeanContext context = new DefaultBeanContext() - context.start() + BeanContext context = BeanContext.run() when:"A bean is obtained which has a field that depends on a bean provided by a provider" B b = context.getBean(B) @@ -36,7 +35,10 @@ class FieldFactorySpec extends Specification { b.a instanceof AImpl b.a.c != null b.a.c2 != null - b.a.is(context.getBean(AImpl)) + b.a.is(context.getBean(A)) + + cleanup: + context.close() } diff --git a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java index 7a37a421a58..f7bf2f93fff 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultApplicationContext.java @@ -255,7 +255,13 @@ protected void startEnvironment() { .qualifier(PrimaryQualifier.INSTANCE); //noinspection resource - registerBeanDefinition(definition.build()); + + RuntimeBeanDefinition beanDefinition = definition.build(); + BeanDefinition existing = findBeanDefinition(beanDefinition.getBeanType()).orElse(null); + if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { + removeBeanDefinition(runtimeBeanDefinition); + } + registerBeanDefinition(beanDefinition); } @Override diff --git a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java index 50c9661dade..264c4e1ec6f 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java +++ b/inject/src/main/java/io/micronaut/context/DefaultBeanContext.java @@ -730,7 +730,7 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing BeanDefinition beanDefinition; if (inject && running.get()) { // Bean cannot be injected before the start of the context - beanDefinition = findBeanDefinition(type, qualifier).orElse(null); + beanDefinition = findConcreteCandidate(null, Argument.of(type), qualifier, false).orElse(null); if (beanDefinition == null) { // Purge cache miss purgeCacheForBeanInstance(singleton); @@ -758,14 +758,6 @@ public BeanContext registerSingleton(@NonNull Class type, @NonNull T sing ); singletonScope.registerSingletonBean(registration, qualifier); registerBeanDefinition(runtimeBeanDefinition); - - for (Class indexedType : indexedTypes) { - if (indexedType == type || indexedType.isAssignableFrom(type)) { - final Collection indexed = resolveTypeIndex(indexedType); - indexed.add(runtimeBeanDefinition); - break; - } - } } return this; } @@ -1663,21 +1655,15 @@ public Collection> getBeanDefinitionReferences() { @NonNull public BeanContext registerBeanDefinition(@NonNull RuntimeBeanDefinition definition) { Objects.requireNonNull(definition, "Bean definition cannot be null"); - BeanDefinition existing = findBeanDefinition(definition.getGenericBeanType(), definition.getDeclaredQualifier()).orElse(null); - if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { - this.beanDefinitionsClasses.remove(runtimeBeanDefinition); - } + Class beanType = definition.getBeanType(); + this.beanDefinitionsClasses.add(definition); for (Class indexedType : indexedTypes) { - if (definition.isCandidateBean(Argument.of(indexedType))) { - Collection index = resolveTypeIndex(indexedType); - if (existing instanceof RuntimeBeanDefinition runtimeBeanDefinition) { - index.remove(runtimeBeanDefinition); - } - index.add(definition); + if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { + final Collection indexed = resolveTypeIndex(indexedType); + indexed.add(definition); + break; } } - this.beanDefinitionsClasses.add(definition); - Class beanType = definition.getBeanType(); purgeCacheForBeanType(beanType); return this; } @@ -1689,6 +1675,25 @@ private void purgeCacheForBeanType(Class beanType) { containsBeanCache.entrySet().removeIf(entry -> entry.getKey().beanType.isAssignableFrom(beanType)); } + /** + * The definition to remove. + * @param definition The definition to remove + * @param The bean type + */ + @Internal + void removeBeanDefinition(RuntimeBeanDefinition definition) { + Class beanType = definition.getBeanType(); + for (Class indexedType : indexedTypes) { + if (indexedType == beanType || indexedType.isAssignableFrom(beanType)) { + final Collection indexed = resolveTypeIndex(indexedType); + indexed.remove(definition); + break; + } + } + this.beanDefinitionsClasses.remove(definition); + purgeCacheForBeanType(definition.getBeanType()); + } + /** * Get a bean of the given type. * diff --git a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java index 94a3ad2098d..0e84b8cb809 100644 --- a/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/DefaultRuntimeBeanDefinition.java @@ -15,21 +15,27 @@ */ package io.micronaut.context; +import io.micronaut.context.annotation.Replaces; import io.micronaut.context.exceptions.BeanInstantiationException; +import io.micronaut.core.annotation.AnnotationClassValue; import io.micronaut.core.annotation.AnnotationMetadata; import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.naming.Named; import io.micronaut.core.reflect.ReflectionUtils; import io.micronaut.core.type.Argument; import io.micronaut.core.util.ArrayUtils; import io.micronaut.core.util.CollectionUtils; import io.micronaut.inject.BeanDefinition; +import io.micronaut.inject.annotation.MutableAnnotationMetadata; import io.micronaut.inject.qualifiers.PrimaryQualifier; import io.micronaut.inject.qualifiers.TypeArgumentQualifier; import java.lang.annotation.Annotation; import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -57,7 +63,7 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition private final boolean isSingleton; private final Class scope; private final Class[] exposedTypes; - private final Map, List>> typeArguments; + private Map, List>> typeArguments; DefaultRuntimeBeanDefinition( @NonNull Argument beanType, @@ -84,16 +90,30 @@ final class DefaultRuntimeBeanDefinition extends AbstractBeanContextCondition @Override public List> getTypeArguments(Class type) { - if (type == getBeanType()) { + Class bt = getBeanType(); + if (type == bt) { return getTypeArguments(); } - if (typeArguments != null) { - List> args = typeArguments.get(type); - if (args != null) { - return args; + if (type != null && type.isAssignableFrom(bt)) { + if (typeArguments != null) { + List> args = typeArguments.get(type); + if (args != null) { + return args; + } + } + List> list = RuntimeBeanDefinition.super.getTypeArguments(type); + if (CollectionUtils.isNotEmpty(list)) { + if (typeArguments == null) { + synchronized (this.beanType) { + typeArguments = new LinkedHashMap<>(3); + } + } + typeArguments.put(type, list); } + return list; + } else { + return Collections.emptyList(); } - return RuntimeBeanDefinition.super.getTypeArguments(type); } @Override @@ -219,6 +239,7 @@ static final class RuntimeBeanBuilder implements RuntimeBeanDefinition.Builde private Class[] exposedTypes = ReflectionUtils.EMPTY_CLASS_ARRAY; private Map, List>> typeArguments; + private Class replacesType; RuntimeBeanBuilder(Argument beanType, Supplier supplier) { this.beanType = Objects.requireNonNull(beanType, MSG_BEAN_TYPE_CANNOT_BE_NULL); @@ -237,6 +258,12 @@ public Builder qualifier(Qualifier qualifier) { return this; } + @Override + public Builder replaces(Class otherType) { + this.replacesType = otherType; + return this; + } + @Override @SuppressWarnings("java:S1872") public Builder scope(Class scope) { @@ -288,6 +315,24 @@ public Builder annotationMetadata(AnnotationMetadata annotationMetadata) { @Override @NonNull public RuntimeBeanDefinition build() { + if (replacesType != null) { + MutableAnnotationMetadata mutableAnnotationMetadata; + if (this.annotationMetadata instanceof MutableAnnotationMetadata mm) { + mutableAnnotationMetadata = mm; + } else if (this.annotationMetadata == null || this.annotationMetadata == EMPTY_METADATA) { + mutableAnnotationMetadata = new MutableAnnotationMetadata(); + this.annotationMetadata = mutableAnnotationMetadata; + } else { + throw new IllegalStateException("Previous non-mutable annotation metadata set"); + } + + Map values = new HashMap<>(3); + values.put(AnnotationMetadata.VALUE_MEMBER, new AnnotationClassValue<>(replacesType)); + if (qualifier instanceof Named named) { + values.put("named", named.getName()); + } + mutableAnnotationMetadata.addAnnotation(Replaces.class.getName(), values); + } return new DefaultRuntimeBeanDefinition<>( beanType, supplier, diff --git a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java index 0082f506aec..aab361b565a 100644 --- a/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java +++ b/inject/src/main/java/io/micronaut/context/RuntimeBeanDefinition.java @@ -20,6 +20,7 @@ import io.micronaut.core.annotation.Experimental; import io.micronaut.core.annotation.NonNull; import io.micronaut.core.annotation.Nullable; +import io.micronaut.core.reflect.GenericTypeUtils; import io.micronaut.core.type.Argument; import io.micronaut.inject.BeanContextConditional; import io.micronaut.inject.BeanDefinition; @@ -28,8 +29,12 @@ import io.micronaut.inject.qualifiers.Qualifiers; import java.lang.annotation.Annotation; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.function.Supplier; +import java.util.stream.Collectors; /** * Allow the construction for bean definitions programmatically that can be registered @@ -61,6 +66,24 @@ default boolean isEnabled(@NonNull BeanContext context, BeanResolutionContext re return true; } + @Override + default List> getTypeArguments(Class type) { + Class beanType = getBeanType(); + if (type != null && type.isAssignableFrom(beanType)) { + if (type.isInterface()) { + return Arrays.stream(GenericTypeUtils.resolveInterfaceTypeArguments(beanType, type)) + .map(Argument::of) + .collect(Collectors.toList()); + } else { + return Arrays.stream(GenericTypeUtils.resolveSuperTypeGenericArguments(beanType, type)) + .map(Argument::of) + .collect(Collectors.toList()); + } + } else { + return Collections.emptyList(); + } + } + @Override default boolean isContextScope() { return getAnnotationMetadata().hasDeclaredAnnotation(Context.class); @@ -185,14 +208,25 @@ interface Builder { * @param qualifier The qualifier * @return This builder */ + @NonNull Builder qualifier(@Nullable Qualifier qualifier); + /** + * Adds this type as a bean replacement of the given type. + * @param otherType The other type + * @return This bean builder + * @since 4.0.0 + */ + @NonNull + Builder replaces(@Nullable Class otherType); + /** * The qualifier to use. * @param name The named qualifier to use. * @return This builder * @since 3.7.0 */ + @NonNull default Builder named(@Nullable String name) { if (name == null) { qualifier(null); @@ -207,6 +241,7 @@ default Builder named(@Nullable String name) { * @param scope The scope * @return This builder */ + @NonNull Builder scope(@Nullable Class scope); /** @@ -214,6 +249,7 @@ default Builder named(@Nullable String name) { * @param isSingleton True if it is singleton * @return This builder */ + @NonNull Builder singleton(boolean isSingleton); /** @@ -221,6 +257,7 @@ default Builder named(@Nullable String name) { * @param types The exposed types * @return This builder */ + @NonNull Builder exposedTypes(Class...types); /** @@ -228,6 +265,7 @@ default Builder named(@Nullable String name) { * @param arguments The arguments * @return This builder */ + @NonNull Builder typeArguments(Argument... arguments); /** @@ -236,6 +274,7 @@ default Builder named(@Nullable String name) { * @param arguments The arguments * @return This builder */ + @NonNull Builder typeArguments(Class implementedType, Argument... arguments); /** @@ -243,6 +282,7 @@ default Builder named(@Nullable String name) { * @param annotationMetadata The annotation metadata * @return This builder */ + @NonNull Builder annotationMetadata(@Nullable AnnotationMetadata annotationMetadata); /** diff --git a/inject/src/main/java/io/micronaut/context/SingletonScope.java b/inject/src/main/java/io/micronaut/context/SingletonScope.java index b344d279f92..97849359bb5 100644 --- a/inject/src/main/java/io/micronaut/context/SingletonScope.java +++ b/inject/src/main/java/io/micronaut/context/SingletonScope.java @@ -112,13 +112,6 @@ BeanRegistration registerSingletonBean(@NonNull BeanRegistration regis DefaultBeanContext.BeanKey beanKey = new DefaultBeanContext.BeanKey<>(beanDefinition, beanDefinition.getDeclaredQualifier()); singletonByArgumentAndQualifier.put(beanKey, registration); } - if (registration.bean != null && registration.bean.getClass() != beanDefinition.getBeanType()) { - // If the actual type differs, allow to inject the actual implementation for cases like: - // `MyInterface factoryBean() { new Impl.. }` - // This might be something to remove in 4.0 - DefaultBeanContext.BeanKey concrete = new DefaultBeanContext.BeanKey<>((Class) registration.bean.getClass(), qualifier); - singletonByArgumentAndQualifier.put(concrete, registration); - } return registration; } @@ -397,17 +390,12 @@ public boolean equals(Object o) { if (beanDefinition.getBeanType() != that.beanDefinition.getBeanType()) { return false; } - Qualifier qualifier = beanDefinition.getDeclaredQualifier(); - Qualifier thatQualifier = that.beanDefinition.getDeclaredQualifier(); - if (qualifier == thatQualifier) { - return true; - } - return qualifier != null && qualifier.equals(thatQualifier); + return beanDefinition.getBeanDefinitionName().equals(that.beanDefinition.getBeanDefinitionName()); } @Override public int hashCode() { - return Objects.hash(beanDefinition.getBeanType(), beanDefinition.getDeclaredQualifier()); + return Objects.hash(beanDefinition.getBeanDefinitionName()); } } diff --git a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy index ba4f0d1977d..e1ffcfa862e 100644 --- a/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy +++ b/runtime/src/test/groovy/io/micronaut/runtime/executor/ExecutorServiceConfigSpec.groovy @@ -58,7 +58,7 @@ class ExecutorServiceConfigSpec extends Specification { executorServices.size() == expectedExecutorCount when: - ThreadPoolExecutor poolExecutor = ctx.getBean(ThreadPoolExecutor, Qualifiers.byName("one")) + ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) ctx.getBean(ExecutorService, Qualifiers.byName("one")) ExecutorService forkJoinPool = ctx.getBean(ExecutorService, Qualifiers.byName("two")) then: @@ -111,7 +111,7 @@ class ExecutorServiceConfigSpec extends Specification { when: Collection executorServices = ctx.getBeansOfType(ExecutorService.class) - ThreadPoolExecutor poolExecutor = ctx.getBean(ThreadPoolExecutor, Qualifiers.byName("one")) + ThreadPoolExecutor poolExecutor = (ThreadPoolExecutor) ctx.getBean(ExecutorService, Qualifiers.byName("one")) ExecutorService forkJoinPool = ctx.getBean(ExecutorService, Qualifiers.byName("two")) then: diff --git a/src/main/docs/guide/appendix/breaks.adoc b/src/main/docs/guide/appendix/breaks.adoc index 85c52123699..2c98942b7c6 100644 --- a/src/main/docs/guide/appendix/breaks.adoc +++ b/src/main/docs/guide/appendix/breaks.adoc @@ -13,6 +13,23 @@ The `micronaut-runtime` module has been split into separate modules depending on In addition, since `micronaut-retry` is now optional declarative clients annotated with ann:http.client.annotation.Client[] no longer invoke fallbacks by default. To restore the previous behaviour add `micronaut-retry` to your classpath and annotate any declarative clients with ann:retry.annotation.Recoverable[]. +==== Calling `registerSingleton(bean)` no longer overrides existing beans + +If you call `registerSingleton(bean)` on the api:context.BeanContext[] this will no longer override existing beans if the type and qualifier match, instead two beans will now exist which may lead to a api:context.exceptions.NonUniqueBeanException[]. + +If you require replacing an existing bean you must formalize the replacement using the api:context.RuntimeBeanDefinition[] API, for example: + +[source,java] +---- +context.registerBeanDefinition( + RuntimeBeanDefinition.builder(Codec.class, ()-> new OverridingCodec()) + .singleton(true) + // the type of the bean to replace + .replaces(ToBeReplacedCodec.class) + .build() +); +---- + ==== WebSocket No Longer Required The `micronaut-websocket` API is no longer a required dependency of the HTTP server. If you are using annotations such as ann:websocket.annotation.ServerWebSocket[] you should add the `micronaut-websocket` dependency to your application classpath: