diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneA.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneA.java new file mode 100644 index 00000000..f4d753e7 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneA.java @@ -0,0 +1,26 @@ +package org.example.myapp.lazy2; + +import io.avaje.inject.PostConstruct; +import jakarta.inject.Singleton; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Singleton +public class LazyOneA { + + public static final AtomicBoolean AINIT = new AtomicBoolean(); + public static final AtomicBoolean A_POST_CONSTRUCT = new AtomicBoolean(); + + LazyOneA() { + AINIT.set(true); + } + + @PostConstruct + void postConstruct() { + A_POST_CONSTRUCT.set(true); + } + + public String oneA() { + return "oneA"; + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneB.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneB.java new file mode 100644 index 00000000..10c39976 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneB.java @@ -0,0 +1,34 @@ +package org.example.myapp.lazy2; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; +import org.example.myapp.HelloService; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Singleton +public class LazyOneB { + + public static final AtomicBoolean BINIT = new AtomicBoolean(); + + final HelloService helloService; + + @Inject + LazyOneB(HelloService helloService) { + this.helloService = helloService; // non-lazy dependency + BINIT.set(true); + } + + /** Required by Lazy proxy */ + LazyOneB() { + this.helloService = null; + } + + public String oneB() { + return "oneB"; + } + + public HelloService helloService() { + return helloService; + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyTwo.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyTwo.java new file mode 100644 index 00000000..2df5d409 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyTwo.java @@ -0,0 +1,44 @@ +package org.example.myapp.lazy2; + +import jakarta.inject.Inject; +import jakarta.inject.Singleton; + +import java.util.concurrent.atomic.AtomicBoolean; + +@Singleton +public class LazyTwo { + + public static final AtomicBoolean INIT = new AtomicBoolean(); + + private final LazyOneB oneB; + final LazyOneA oneA; + + @Inject + LazyTwo(LazyOneA oneA, LazyOneB oneB) { + this.oneA = oneA; + this.oneB = oneB; + INIT.set(true); + } + + /** Required by Lazy proxy */ + LazyTwo() { + this.oneA = null; + this.oneB = null; + } + + String something() { + return "two-" + oneA.oneA() + "-" + oneB.oneB(); + } + + String description() { + return this.getClass() + "|" + oneA.getClass() + "|" + oneB.getClass(); + } + + public LazyOneA oneA() { + return oneA; + } + + public LazyOneB oneB() { + return oneB; + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/package-info.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/package-info.java new file mode 100644 index 00000000..133f6dc7 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy2/package-info.java @@ -0,0 +1,9 @@ +/** + * Use Lazy for all the beans in this package. + *

+ * Use {@code enforceProxy = true} to fail compilation if there is no default constructor/lazy not supported. + */ +@Lazy(enforceProxy = true) +package org.example.myapp.lazy2; + +import io.avaje.inject.Lazy; diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/lazy2/LazyTwoTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/lazy2/LazyTwoTest.java new file mode 100644 index 00000000..4c0ab883 --- /dev/null +++ b/blackbox-test-inject/src/test/java/org/example/myapp/lazy2/LazyTwoTest.java @@ -0,0 +1,50 @@ +package org.example.myapp.lazy2; + +import io.avaje.inject.BeanScope; +import org.example.myapp.HelloService; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +class LazyTwoTest { + + @Test + void test() { + try (var scope = BeanScope.builder().build()) { + assertThat(LazyTwo.INIT).isFalse(); + assertThat(LazyOneA.AINIT).describedAs("Only 1 constructor, so called by LazyOneA$$Lazy()").isTrue(); + assertThat(LazyOneA.A_POST_CONSTRUCT).isFalse(); + assertThat(LazyOneB.BINIT).isFalse(); + + var lazyOneA = scope.get(LazyOneA.class); + assertThat(LazyOneA.A_POST_CONSTRUCT).describedAs("Only got the proxy").isFalse(); + + var lazy = scope.get(LazyTwo.class); + assertThat(lazy.getClass().toString()).describedAs("got the proxy").contains("LazyTwo$Lazy"); + assertThat(LazyTwo.INIT).isFalse(); + assertThat(LazyOneB.BINIT).isFalse(); + assertThat(LazyOneA.A_POST_CONSTRUCT).describedAs("Only got the proxy").isFalse(); + + assertThat(lazy.oneA()).describedAs("same proxy instance").isSameAs(lazyOneA); + + // invocation will initialize the lazy beans + String value = lazy.something(); + assertThat(value).isEqualTo("two-oneA-oneB"); + assertThat(LazyTwo.INIT).isTrue(); + assertThat(LazyOneA.A_POST_CONSTRUCT).isTrue(); + assertThat(LazyOneB.BINIT).isTrue(); + + // the graph is of Lazy beans + String description = lazy.description(); + assertThat(description).describedAs("this is the underlying real instance").doesNotContain("LazyTwo$Lazy"); + assertThat(description).contains("LazyOneA$Lazy"); + assertThat(description).contains("LazyOneB$Lazy"); + + assertThat(scope.get(LazyTwo.class)).isSameAs(lazy); + + HelloService nonLazyDependency = lazy.oneB().helloService(); + HelloService helloService = scope.get(HelloService.class); + assertThat(nonLazyDependency).isSameAs(helloService); + } + } +} 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 beefe5aa..6ed4a6e3 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 @@ -85,10 +85,10 @@ final class BeanReader { factory); typeReader.process(); - this.lazy = - !FactoryPrism.isPresent(actualType) - && (LazyPrism.isPresent(actualType) - || importedComponent && ProcessingContext.isImportedLazy(actualType)); + var lazyPrism = Util.isLazy(actualType); + this.lazy = !FactoryPrism.isPresent(actualType) + && (lazyPrism != null + || importedComponent && ProcessingContext.isImportedLazy(actualType)); this.requestParams = new BeanRequestParams(type); this.name = typeReader.name(); @@ -105,6 +105,14 @@ final class BeanReader { this.delayed = shouldDelay(); this.lazyProxyType = !lazy || delayed ? null : Util.lazyProxy(actualType); this.proxyLazy = lazy && lazyProxyType != null; + if (lazy && !proxyLazy) { + if (lazyPrism != null && lazyPrism.enforceProxy()) { + logError(beanType, "Lazy beans must have an additional no-arg constructor"); + } else { + logWarn(beanType, "Lazy beans should have an additional no-arg constructor"); + } + } + conditions.readAll(actualType); } @@ -197,8 +205,6 @@ BeanReader read() { conditions.addImports(importTypes); if (proxyLazy) { SimpleBeanLazyWriter.write(APContext.elements().getPackageOf(beanType), lazyProxyType); - } else if (lazy) { - logWarn(beanType, "Lazy beans should have a no-arg constructor"); } return this; } 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 7c6ff2e0..e092d96e 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 @@ -1,5 +1,6 @@ package io.avaje.inject.generator; +import static io.avaje.inject.generator.APContext.logError; import static io.avaje.inject.generator.APContext.logWarn; import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY; import static io.avaje.inject.generator.ProcessingContext.asElement; @@ -71,10 +72,18 @@ final class MethodReader { primary = PrimaryPrism.isPresent(element); secondary = SecondaryPrism.isPresent(element); priority = Util.priority(element); - lazy = LazyPrism.isPresent(element) || LazyPrism.isPresent(element.getEnclosingElement()); + var lazyPrism = Util.isLazy(element); + lazy = lazyPrism != null; conditions.readAll(element); this.lazyProxyType = lazy ? Util.lazyProxy(element) : null; this.proxyLazy = lazy && lazyProxyType != null; + if (lazy && !proxyLazy) { + if (lazyPrism.enforceProxy()) { + logError(element, "Lazy return type must be abstract or have a no-arg constructor"); + } else { + logWarn(element, "Lazy return type should be abstract or have a no-arg constructor"); + } + } } else { prototype = false; primary = false; @@ -181,8 +190,6 @@ MethodReader read() { observeParameter = params.stream().filter(MethodParam::observeEvent).findFirst().orElse(null); if (proxyLazy) { SimpleBeanLazyWriter.write(APContext.elements().getPackageOf(element), lazyProxyType); - } else if (lazy) { - logWarn(element, "Lazy return types should be abstract or have a no-arg constructor"); } return this; } 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 93edfb71..232a7f32 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 @@ -442,4 +442,11 @@ static Integer priority(Element element) { private static boolean isPriorityAnnotation(AnnotationMirror mirror) { return mirror.getAnnotationType().asElement().getSimpleName().toString().contains("Priority"); } + + static LazyPrism isLazy(Element element) { + if (element == null) { + return null; + } + return LazyPrism.getOptionalOn(element).orElseGet(() -> isLazy(element.getEnclosingElement())); + } } diff --git a/inject/src/main/java/io/avaje/inject/Lazy.java b/inject/src/main/java/io/avaje/inject/Lazy.java index f962e93d..f5766e06 100644 --- a/inject/src/main/java/io/avaje/inject/Lazy.java +++ b/inject/src/main/java/io/avaje/inject/Lazy.java @@ -15,5 +15,12 @@ * constructor, a generated proxy bean will be wired for ultimate laziness. */ @Retention(RetentionPolicy.SOURCE) -@Target({ElementType.METHOD, ElementType.TYPE}) -public @interface Lazy {} +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PACKAGE, ElementType.MODULE}) +public @interface Lazy { + + /** + * Ensures that a compile-time proxy is generated, will fail compilation if missing conditions for + * generation + */ + boolean enforceProxy() default false; +}