diff --git a/README.md b/README.md index 066ef37f..fd5f24bf 100644 --- a/README.md +++ b/README.md @@ -164,6 +164,7 @@ public final class ExampleModule implements AvajeModule { | [@PreDestroy](https://avaje.io/inject/#pre-destroy) | - | @PreDestroy | [@Factory and @Bean](https://avaje.io/inject/#factory) | - | @Configuration and @Bean | [@RequiresBean and @RequiresProperty](https://avaje.io/inject/#conditional) | - | @Conditional +| [@Lazy](https://avaje.io/inject/#lazy) | - | @Lazy | [@Primary](https://avaje.io/inject/#primary) | - | @Primary | [@Secondary](https://avaje.io/inject/#secondary) | - | @Fallback | [@InjectTest](https://avaje.io/inject/#component-testing) | - | @SpringBootTest diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/GenericLazyFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/GenericLazyFactory.java new file mode 100644 index 00000000..df9d3d4d --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/GenericLazyFactory.java @@ -0,0 +1,35 @@ +package org.example.myapp.lazy.generic; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.jspecify.annotations.Nullable; + +import io.avaje.inject.Bean; +import io.avaje.inject.BeanTypes; +import io.avaje.inject.Factory; +import io.avaje.inject.Lazy; +import jakarta.inject.Named; + +@Lazy +@Factory +public class GenericLazyFactory { + + @Bean + @Named("factory") + LazyGenericInterface lazyInterFace(@Nullable AtomicBoolean initialized) throws Exception { + + // note that nested test scopes will not be lazy + if (initialized != null) initialized.set(true); + return new LazyGenericImpl(initialized); + } + + @Bean + @BeanTypes(LazyGenericInterface.class) + @Named("factoryBeanType") + LazyGenericImpl factoryBeanType(@Nullable AtomicBoolean initialized) throws Exception { + + // note that nested test scopes will not be lazy + if (initialized != null) initialized.set(true); + return new LazyGenericImpl(initialized); + } +} 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 new file mode 100644 index 00000000..a9011aee --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericImpl.java @@ -0,0 +1,36 @@ +package org.example.myapp.lazy.generic; + +import java.util.concurrent.atomic.AtomicBoolean; + +import io.avaje.inject.BeanScope; +import io.avaje.inject.BeanTypes; +import io.avaje.inject.Lazy; +import io.avaje.inject.PostConstruct; +import io.github.resilience4j.core.lang.Nullable; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Lazy +@Singleton +@Named("single") +@BeanTypes(LazyGenericInterface.class) +public class LazyGenericImpl implements LazyGenericInterface { + + AtomicBoolean initialized; + + public LazyGenericImpl(@Nullable AtomicBoolean initialized) { + this.initialized = initialized; + } + + @PostConstruct + void init(BeanScope scope) { + // note that nested test scopes will not be lazy + if (initialized != null) initialized.set(true); + } + + @Override + public void something() {} + + @Override + public void otherThing() {} +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericInterface.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericInterface.java new file mode 100644 index 00000000..b7885336 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/generic/LazyGenericInterface.java @@ -0,0 +1,8 @@ +package org.example.myapp.lazy.generic; + +public interface LazyGenericInterface { + + void something(); + + void otherThing(); +} diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/lazy/generic/LazyGenericTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/generic/LazyGenericTest.java new file mode 100644 index 00000000..d7947297 --- /dev/null +++ b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/generic/LazyGenericTest.java @@ -0,0 +1,49 @@ +package org.example.myapp.lazy.generic; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.concurrent.atomic.AtomicBoolean; + +import org.junit.jupiter.api.Test; + +import io.avaje.inject.BeanScope; +import io.avaje.inject.spi.GenericType; + +class LazyGenericTest { + + @Test + void testBeanTypes() { + var initialized = new AtomicBoolean(); + try (var scope = BeanScope.builder().beans(initialized).build()) { + assertThat(initialized).isFalse(); + var lazy = scope.get(LazyGenericInterface.class, "single"); + assertThat(lazy).isNotNull(); + assertThat(initialized).isTrue(); + } + } + + @Test + void testFactoryInterface() { + var initialized = new AtomicBoolean(); + try (var scope = BeanScope.builder().beans(initialized).build()) { + assertThat(initialized).isFalse(); + LazyGenericInterface prov = + scope.get(new GenericType>() {}.type(), "factory"); + assertThat(initialized).isFalse(); + prov.something(); + assertThat(initialized).isTrue(); + assertThat(prov).isNotNull(); + } + } + + @Test + void factoryBeanType() { + var initialized = new AtomicBoolean(); + try (var scope = BeanScope.builder().beans(initialized).build()) { + assertThat(initialized).isFalse(); + var lazy = scope.get(LazyGenericInterface.class, "factoryBeanType"); + assertThat(lazy).isNotNull(); + assertThat(initialized).isTrue(); + } + } +} 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 04788ce0..5b44ddc5 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 @@ -173,6 +173,9 @@ MethodReader read() { params.add(new MethodParam(p)); } observeParameter = params.stream().filter(MethodParam::observeEvent).findFirst().orElse(null); + if (proxyLazy) { + SimpleBeanLazyWriter.write(APContext.elements().getPackageOf(element), lazyProxyType); + } return this; } @@ -287,7 +290,14 @@ void builderAddBeanProvider(Append writer) { endTry(writer, " "); writer.indent(indent); if (proxyLazy) { - writer.append(" }, %s$Lazy::new);", Util.shortNameLazyProxy(lazyProxyType)).eol(); + String shortNameLazyProxy = Util.shortNameLazyProxy(lazyProxyType) + "$Lazy"; + writer.append(" }, "); + if (lazyProxyType.getTypeParameters().isEmpty()) { + writer.append("%s::new", shortNameLazyProxy); + } else { + writer.append("p -> new %s<>(p)", shortNameLazyProxy); + } + writer.append(");"); } else { writer.indent(indent).append(" });").eol(); } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanLazyWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanLazyWriter.java index 26ddd3e9..01d9a9b3 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanLazyWriter.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanLazyWriter.java @@ -13,6 +13,7 @@ import javax.lang.model.element.TypeElement; import javax.lang.model.element.TypeParameterElement; import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import javax.lang.model.util.ElementFilter; final class SimpleBeanLazyWriter { @@ -22,14 +23,14 @@ final class SimpleBeanLazyWriter { + "{1}" + "@Proxy\n" + "@Generated(\"avaje-inject-generator\")\n" - + "public final class {2}$Lazy {3} {2} '{'\n" + + "public final class {2}$Lazy{3} {4} {2}{3} '{'\n" + "\n" - + " private final Provider<{2}> onceProvider;\n" + + " private final Provider<{2}{3}> onceProvider;\n" + "\n" - + " public {2}$Lazy(Provider<{2}> onceProvider) '{'\n" + + " public {2}$Lazy(Provider<{2}{3}> onceProvider) '{'\n" + " this.onceProvider = onceProvider;\n" + " '}'\n\n" - + "{4}" + + "{5}" + "'}'\n"; private final String originName; @@ -64,9 +65,21 @@ void write() { try { final var writer = new Append(APContext.createSourceFile(originName, element).openWriter()); - var typeString = isInterface ? "implements" : "extends"; + var inheritance = isInterface ? "implements" : "extends"; String methodString = methods(); - writer.append(MessageFormat.format(TEMPLATE, packageName, imports(), shortName, typeString, methodString)); + + // Get type parameters + List typeParameters = element.getTypeParameters(); + String typeParametersDecl = buildTypeParametersDeclaration(typeParameters); + writer.append( + MessageFormat.format( + TEMPLATE, + packageName, + imports(), + shortName, + typeParametersDecl, + inheritance, + methodString)); writer.close(); } catch (Exception e) { logError("Failed to write Proxy class %s", e); @@ -165,7 +178,34 @@ private String methods() { sb.append(");\n"); sb.append(" }\n\n"); } + return sb.toString(); + } + + private String buildTypeParametersDeclaration(List typeParameters) { + if (typeParameters.isEmpty()) { + return ""; + } + StringBuilder sb = new StringBuilder("<"); + for (int i = 0; i < typeParameters.size(); i++) { + if (i > 0) { + sb.append(", "); + } + TypeParameterElement param = typeParameters.get(i); + sb.append(param.getSimpleName()); + + List bounds = param.getBounds(); + if (!bounds.isEmpty() && !"java.lang.Object".equals(bounds.get(0).toString())) { + sb.append(" extends "); + for (int j = 0; j < bounds.size(); j++) { + if (j > 0) { + sb.append(" & "); + } + sb.append(bounds.get(j).toString()); + } + } + } + sb.append(">"); return sb.toString(); } } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java index 6d7b9b86..2b7fa016 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java @@ -200,9 +200,7 @@ private void writeAddFor(MethodReader constructor) { writer.indent(" return bean;").eol(); if (!constructor.methodThrows()) { writer.append(" }"); - if (beanReader.proxyLazy()) { - writer.append(", %s$Lazy::new", Util.shortNameLazyProxy(beanReader.lazyProxyType())); - } + writeLazyRegister(); writer.append(");").eol(); } } @@ -211,9 +209,7 @@ private void writeAddFor(MethodReader constructor) { if (beanReader.registerProvider() && constructor.methodThrows()) { writer.append(" }"); - if (beanReader.proxyLazy()) { - writer.append(", %s$Lazy::new", Util.shortNameLazyProxy(beanReader.lazyProxyType())); - } + writeLazyRegister(); writer.append(");").eol(); } @@ -221,6 +217,18 @@ private void writeAddFor(MethodReader constructor) { writer.eol(); } + private void writeLazyRegister() { + if (beanReader.proxyLazy()) { + String shortNameLazyProxy = Util.shortNameLazyProxy(beanReader.lazyProxyType()) + "$Lazy"; + writer.append(", "); + if (beanReader.lazyProxyType().getTypeParameters().isEmpty()) { + writer.append("%s::new", shortNameLazyProxy); + } else { + writer.append("p -> new %s<>(p)", shortNameLazyProxy); + } + } + } + private void writeBuildMethodStart() { writer.append(" public static void build(%s builder) {", beanReader.builderType()).eol(); } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java index bc67f26a..7e7feb50 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java @@ -408,19 +408,17 @@ static TypeElement lazyProxy(Element element) { ? (TypeElement) element : APContext.asTypeElement(((ExecutableElement) element).getReturnType()); - if (!type.getTypeParameters().isEmpty() - || type.getModifiers().contains(Modifier.FINAL) - || !type.getKind().isInterface() && !Util.hasNoArgConstructor(type)) { + if (type.getModifiers().contains(Modifier.FINAL) + || !type.getKind().isInterface() && !Util.hasNoArgConstructor(type)) { return BeanTypesPrism.getOptionalOn(element) - .map(BeanTypesPrism::value) - .filter(v -> v.size() == 1) - .map(v -> APContext.asTypeElement(v.get(0))) - // figure out generics later - .filter(v -> - v.getTypeParameters().isEmpty() - && (v.getKind().isInterface() || hasNoArgConstructor(v))) - .orElse(null); + .map(BeanTypesPrism::value) + .filter(v -> v.size() == 1) + .map(v -> APContext.asTypeElement(v.get(0))) + // generics and beantypes don't mix + .filter(t -> t.getTypeParameters().isEmpty() || t.equals(element)) + .filter(v -> (v.getKind().isInterface() || hasNoArgConstructor(v))) + .orElse(null); } return type; diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBeanTypes.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBeanTypes.java index c6afc3b3..698131e7 100644 --- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBeanTypes.java +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBeanTypes.java @@ -22,7 +22,7 @@ public LazyBeanTypes(Provider intProvider) { public void something() {} @Override - public String somethingElse() { // TODO Auto-generated method stub + public String somethingElse() { return null; } } diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericBeanTypes.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericBeanTypes.java new file mode 100644 index 00000000..21c12c34 --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericBeanTypes.java @@ -0,0 +1,33 @@ +package io.avaje.inject.generator.models.valid.lazy.generic; + +import io.avaje.inject.BeanTypes; +import io.avaje.inject.Lazy; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Lazy +@Singleton +@BeanTypes(LazyGenericInterface.class) +public class LazyGenericBeanTypes implements LazyGenericInterface { + + Provider intProvider; + + @Inject + public LazyGenericBeanTypes(Provider intProvider) { + this.intProvider = intProvider; + } + + @Override + public void something() {} + + @Override + public String somethingElse() { + return null; + } + + @Override + public Object gen() { + return null; + } +} diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericFactory.java new file mode 100644 index 00000000..32b9d07f --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericFactory.java @@ -0,0 +1,15 @@ +package io.avaje.inject.generator.models.valid.lazy.generic; + +import io.avaje.inject.Bean; +import io.avaje.inject.Factory; +import io.avaje.inject.Lazy; + +@Factory +public class LazyGenericFactory { + + @Bean + @Lazy + LazyGenericInterface lazyInterface() { + return null; + } +} diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericInterface.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericInterface.java new file mode 100644 index 00000000..3990b2aa --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/generic/LazyGenericInterface.java @@ -0,0 +1,10 @@ +package io.avaje.inject.generator.models.valid.lazy.generic; + +public interface LazyGenericInterface { + + void something(); + + String somethingElse(); + + T gen(); +}