diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/beantypes/LimitedFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/beantypes/LimitedFactory.java index b4b601727..77f4a5b0f 100644 --- a/blackbox-test-inject/src/main/java/org/example/myapp/beantypes/LimitedFactory.java +++ b/blackbox-test-inject/src/main/java/org/example/myapp/beantypes/LimitedFactory.java @@ -7,6 +7,7 @@ import org.jspecify.annotations.Nullable; import java.io.Serializable; +import java.util.GregorianCalendar; import java.util.Optional; @Factory @@ -14,7 +15,7 @@ public class LimitedFactory { @Bean @Named("factory") - @BeanTypes(LimitedInterface.class) + @BeanTypes(value = LimitedInterface.class, registerTypes = GregorianCalendar.class) BeanTypeComponent bean() { return new BeanTypeComponent(); } diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericImpl.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericImpl.java index a9011aee7..678c7575c 100644 --- a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericImpl.java +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericImpl.java @@ -1,5 +1,6 @@ package org.example.myapp.lazy.generic; +import java.util.Calendar; import java.util.concurrent.atomic.AtomicBoolean; import io.avaje.inject.BeanScope; @@ -13,7 +14,7 @@ @Lazy @Singleton @Named("single") -@BeanTypes(LazyGenericInterface.class) +@BeanTypes(value = LazyGenericInterface.class, registerTypes = Calendar.class) public class LazyGenericImpl implements LazyGenericInterface { AtomicBoolean initialized; diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/beantypes/BeanTypeComponentTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/beantypes/BeanTypeComponentTest.java index cba899a80..ee7f71baa 100644 --- a/blackbox-test-inject/src/test/java/org/example/myapp/beantypes/BeanTypeComponentTest.java +++ b/blackbox-test-inject/src/test/java/org/example/myapp/beantypes/BeanTypeComponentTest.java @@ -3,10 +3,14 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertFalse; +import org.example.myapp.lazy.generic.LazyGenericImpl; import org.junit.jupiter.api.Test; import io.avaje.inject.BeanScope; +import java.util.Calendar; +import java.util.GregorianCalendar; + class BeanTypesTest { @Test @@ -17,6 +21,14 @@ void testBeanTypesRestrictingInjection() { assertThat(scope.get(AbstractSuperClass.class)).isNotNull(); assertThat(scope.get(LimitedInterface.class)).isNotNull(); assertThat(scope.get(CharSequence.class)).isEqualTo("IAmNullable"); + + Object extraTypeGregorianCalendar = scope.get(Calendar.class); + assertThat(extraTypeGregorianCalendar).isNotNull(); + assertThat(extraTypeGregorianCalendar).isInstanceOf(LazyGenericImpl.class); + + Object extraTypeCalendar = scope.get(GregorianCalendar.class); + assertThat(extraTypeCalendar).isNotNull(); + assertThat(extraTypeCalendar).isInstanceOf(BeanTypeComponent.class); } } } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/AssistBeanReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/AssistBeanReader.java index 2ad1950c9..85c20a8bf 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/AssistBeanReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/AssistBeanReader.java @@ -39,7 +39,7 @@ final class AssistBeanReader { this.beanType = beanType; this.type = beanType.getQualifiedName().toString(); this.typeReader = - new TypeReader(List.of(), UType.parse(beanType.asType()), beanType, importTypes, false); + new TypeReader(List.of(), List.of(), UType.parse(beanType.asType()), beanType, importTypes, false); typeReader.process(); qualifierName = typeReader.name(); diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java index d39ca8df9..c8eae017e 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java @@ -71,14 +71,22 @@ final class BeanReader { this.primary = PrimaryPrism.isPresent(actualType); this.secondary = !primary && SecondaryPrism.isPresent(actualType); this.priority = Util.priority(actualType); + var beanTypes = BeanTypesPrism.getOptionalOn(actualType) .map(BeanTypesPrism::value) .or(() -> proxy ? Optional.of(List.of(actualType.asType())) : Optional.empty()); beanTypes.ifPresent(t -> Util.validateBeanTypes(actualType, t)); + + var extraTypes = + BeanTypesPrism.getOptionalOn(actualType) + .map(BeanTypesPrism::registerTypes) + .orElse(List.of()); + this.typeReader = new TypeReader( beanTypes.orElse(List.of()), + extraTypes, UType.parse(beanType.asType()), beanType, importTypes, @@ -149,7 +157,12 @@ private boolean shouldDelay() { .stream() .flatMap(List::stream); - return Stream.of(constructors, fields, methods, interfaces, superClass, beanTypes) + var beanRegisterTypes = BeanTypesPrism.getOptionalOn(beanType) + .map(BeanTypesPrism::registerTypes) + .stream() + .flatMap(List::stream); + + return Stream.of(constructors, fields, methods, interfaces, superClass, beanTypes, beanRegisterTypes) .flatMap(s -> s) .map(TypeMirror::getKind) .anyMatch(TypeKind.ERROR::equals); @@ -356,6 +369,10 @@ void buildBeanAbsent(Append writer) { } writer.append(typeReader.typesRegister()); writer.append(")) {").eol(); + String extraTypesRegister = typeReader.extraTypesRegister(); + if (extraTypesRegister != null) { + writer.append(" builder.registerTypes(%s);", extraTypesRegister).eol(); + } } void buildRegister(Append writer) { diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java index 79c332927..8d0fcd26a 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java @@ -141,9 +141,14 @@ final class MethodReader { } else { var beanTypes = BeanTypesPrism.getOptionalOn(element).map(BeanTypesPrism::value); beanTypes.ifPresent(t -> Util.validateBeanTypes(element, t)); + + var extraTypes = BeanTypesPrism.getOptionalOn(element) + .map(BeanTypesPrism::registerTypes) + .orElse(List.of()); + this.typeReader = new TypeReader( - beanTypes.orElse(List.of()), genericType, returnElement, importTypes, element); + beanTypes.orElse(List.of()), extraTypes, genericType, returnElement, importTypes, element); typeReader.process(); MethodLifecycleReader lifecycleReader = new MethodLifecycleReader(returnElement, initMethod, destroyMethod); this.initMethod = lifecycleReader.initMethod(); @@ -475,6 +480,12 @@ void buildAddFor(Append writer) { } } writer.append(")) {").eol(); + if (typeReader != null) { + String extraTypesRegister = typeReader.extraTypesRegister(); + if (extraTypesRegister != null) { + writer.append(" builder.registerTypes(%s);", extraTypesRegister).eol(); + } + } } boolean methodThrows() { diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java index 58eebd7b1..73f47f6b5 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java @@ -46,8 +46,9 @@ private static Stream allComponentTypes(UType u) { Stream.of(u), componentTypes.stream().flatMap(TypeAppender::allComponentTypes)); } - void add(List sourceTypes) { + TypeAppender add(List sourceTypes) { sourceTypes.forEach(this::add); + return this; } void addSimpleType(String classType) { diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java index adc636dda..3d4b92a88 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java @@ -21,28 +21,33 @@ final class TypeReader { private final TypeAnnotationReader annotationReader; private Set genericTypes; private String typesRegister; + private String extraTypesRegister; private final List injectsTypes; + private final List extraTypes; TypeReader( List injectsTypes, + List extraTypes, UType genericType, TypeElement beanType, ImportTypeMap importTypes, boolean factory) { - this(injectsTypes, genericType, true, beanType, importTypes, factory, beanType); + this(injectsTypes, extraTypes, genericType, true, beanType, importTypes, factory, beanType); } TypeReader( List injectsTypes, + List extraTypes, UType genericType, TypeElement returnElement, ImportTypeMap importTypes, ExecutableElement source) { - this(injectsTypes, genericType, false, returnElement, importTypes, false, source); + this(injectsTypes, extraTypes, genericType, false, returnElement, importTypes, false, source); } private TypeReader( List injectsTypes, + List extraTypes, UType genericType, boolean forBean, TypeElement beanType, @@ -50,6 +55,7 @@ private TypeReader( boolean factory, Element source) { this.injectsTypes = injectsTypes.stream().map(UType::parse).collect(toList()); + this.extraTypes = extraTypes.stream().map(UType::parse).collect(toList()); this.forBean = forBean; this.beanType = beanType; this.importTypes = importTypes; @@ -63,6 +69,10 @@ String typesRegister() { return typesRegister; } + String extraTypesRegister() { + return extraTypesRegister; + } + List provides() { var provides = providedTypes(); provides.addAll(autoProvides()); @@ -156,6 +166,12 @@ private void initRegistrationTypes() { appender.add(extendsReader.provides()); } else { appender.add(injectsTypes); + if (!extraTypes.isEmpty()) { + this.extraTypesRegister = + new TypeAppender(importTypes) + .add(extraTypes) + .asString(); + } } this.genericTypes = appender.genericTypes(); this.typesRegister = appender.asString(); diff --git a/inject/src/main/java/io/avaje/inject/BeanTypes.java b/inject/src/main/java/io/avaje/inject/BeanTypes.java index fdcfded52..9b75b6d37 100644 --- a/inject/src/main/java/io/avaje/inject/BeanTypes.java +++ b/inject/src/main/java/io/avaje/inject/BeanTypes.java @@ -7,5 +7,14 @@ @Target({ElementType.METHOD, ElementType.TYPE}) public @interface BeanTypes { + /** The types the component will register to. */ Class[] value(); + + /** + * Extra types to register the component to that are not included in the + * isBeanAbsent() check. For testing purposes, when providing + * test doubles, these types are not checked. + */ + Class[] registerTypes() default {}; + } diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java index 1190a9fbd..ae36facc4 100644 --- a/inject/src/main/java/io/avaje/inject/spi/Builder.java +++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java @@ -57,6 +57,11 @@ default boolean isBeanAbsent(Type... types) { return isBeanAbsent(null, types); } + /** + * Register extra types that the next registered bean implements and provides. + */ + void registerTypes(Type... types); + /** * Register the next bean as having Primary priority. * Highest priority, wired over any other matching beans. diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java index 06a9ef6bb..fe82ac983 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java @@ -89,17 +89,25 @@ void register(Object bean) { var name = nextBean.name; qualifiers.add(name); var entryBean = DContextEntryBean.of(bean, name, nextBean.priority, currentModule); - for (Type type : nextBean.types) { - beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean); - } + registerTypes(entryBean); } void register(Provider provider) { qualifiers.add(nextBean.name); var entryBean = DContextEntryBean.provider(nextBean.prototype, provider, nextBean.name, nextBean.priority, currentModule); + registerTypes(entryBean); + } + + private void registerTypes(DContextEntryBean entryBean) { for (Type type : nextBean.types) { beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean); } + Type[] extraTypes = nextBean.extraTypes; + if (extraTypes != null) { + for (Type type : extraTypes) { + beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean); + } + } } /** Check if a non secondary entry exists */ @@ -132,8 +140,8 @@ T get(Type type, String name) { return (T) entry.get(name, currentModule); } - public List listByPriority(Type type) { - + @SuppressWarnings("unchecked") + List listByPriority(Type type) { DContextEntry entry = beans.get(type.getTypeName()); if (entry == null) { return List.of(); @@ -255,9 +263,14 @@ Set scopeAnnotations() { return forScopes; } - static class NextBean { + void nextBeanRegisterTypes(Type[] types) { + nextBean.registerTypes(types); + } + + static final class NextBean { final String name; final Type[] types; + Type[] extraTypes; int priority = BeanEntry.NORMAL; boolean prototype; @@ -265,5 +278,9 @@ static class NextBean { this.name = name; this.types = types; } + + private void registerTypes(Type[] extraTypes) { + this.extraTypes = extraTypes; + } } } diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java index 76aef5251..9ee44c1f6 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java @@ -85,6 +85,11 @@ public boolean isBeanAbsent(@Nullable String name, Type... types) { return true; } + @Override + public void registerTypes(Type... types) { + beanMap.nextBeanRegisterTypes(types); + } + /** * Return the types without any annotation types or raw generics. *