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;
+}