From 48709916cdc54965b857b669700b709aadb30f7c Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:25:40 -0400 Subject: [PATCH 1/5] Make `@Lazy` work on a package/module level Given that we generate proxies now, this technically works. --- .../java/io/avaje/inject/generator/BeanReader.java | 13 ++++++++++++- inject/src/main/java/io/avaje/inject/Lazy.java | 2 +- 2 files changed, 13 insertions(+), 2 deletions(-) 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 beefe5aaa..d14c2517c 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 @@ -87,7 +87,7 @@ final class BeanReader { typeReader.process(); this.lazy = !FactoryPrism.isPresent(actualType) - && (LazyPrism.isPresent(actualType) + && (isLazy(actualType) || importedComponent && ProcessingContext.isImportedLazy(actualType)); this.requestParams = new BeanRequestParams(type); @@ -108,6 +108,17 @@ final class BeanReader { conditions.readAll(actualType); } + private boolean isLazy(Element element) { + if (element == null) { + return false; + } + if (LazyPrism.isPresent(element)) { + return true; + } + + return isLazy(element.getEnclosingElement()); + } + /** * delay until next round if types cannot be resolved */ diff --git a/inject/src/main/java/io/avaje/inject/Lazy.java b/inject/src/main/java/io/avaje/inject/Lazy.java index f962e93d7..baaa150e3 100644 --- a/inject/src/main/java/io/avaje/inject/Lazy.java +++ b/inject/src/main/java/io/avaje/inject/Lazy.java @@ -15,5 +15,5 @@ * constructor, a generated proxy bean will be wired for ultimate laziness. */ @Retention(RetentionPolicy.SOURCE) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD, ElementType.TYPE, ElementType.PACKAGE, ElementType.MODULE}) public @interface Lazy {} From 688097b384c8533b8dbb4d71be899fc9537e5392 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Fri, 8 Aug 2025 19:34:27 -0400 Subject: [PATCH 2/5] add a flag to fail compilation if missing no arg constructor --- .../io/avaje/inject/generator/BeanReader.java | 26 ++++++++----------- .../avaje/inject/generator/MethodReader.java | 13 +++++++--- .../java/io/avaje/inject/generator/Util.java | 8 ++++++ .../src/main/java/io/avaje/inject/Lazy.java | 9 ++++++- 4 files changed, 37 insertions(+), 19 deletions(-) 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 d14c2517c..acc17c139 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,11 @@ final class BeanReader { factory); typeReader.process(); + var lazyPrism = Util.isLazy(actualType); this.lazy = - !FactoryPrism.isPresent(actualType) - && (isLazy(actualType) - || importedComponent && ProcessingContext.isImportedLazy(actualType)); + !FactoryPrism.isPresent(actualType) + && (lazyPrism != null + || importedComponent && ProcessingContext.isImportedLazy(actualType)); this.requestParams = new BeanRequestParams(type); this.name = typeReader.name(); @@ -105,18 +106,15 @@ final class BeanReader { this.delayed = shouldDelay(); this.lazyProxyType = !lazy || delayed ? null : Util.lazyProxy(actualType); this.proxyLazy = lazy && lazyProxyType != null; - conditions.readAll(actualType); - } - - private boolean isLazy(Element element) { - if (element == null) { - return false; - } - if (LazyPrism.isPresent(element)) { - return true; + if (lazy && !proxyLazy) { + if (lazyPrism != null && lazyPrism.forceProxy()) { + logError(beanType, "Lazy beans must have an additional no-arg constructor"); + } else { + logWarn(beanType, "Lazy beans should have an additional no-arg constructor"); + } } - return isLazy(element.getEnclosingElement()); + conditions.readAll(actualType); } /** @@ -208,8 +206,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 7c6ff2e0f..f00878953 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.forceProxy()) { + 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 93edfb710..432987a1c 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,12 @@ 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 baaa150e3..9a190a350 100644 --- a/inject/src/main/java/io/avaje/inject/Lazy.java +++ b/inject/src/main/java/io/avaje/inject/Lazy.java @@ -16,4 +16,11 @@ */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD, ElementType.TYPE, ElementType.PACKAGE, ElementType.MODULE}) -public @interface Lazy {} +public @interface Lazy { + + /** + * Ensures that a compile-time proxy is generated, will fail compilation if missing conditions for + * generation + */ + boolean forceProxy() default false; +} From ba2d141f1db268824c0e71a4f1cf892b0187f6b1 Mon Sep 17 00:00:00 2001 From: Josiah Noel <32279667+SentryMan@users.noreply.github.com> Date: Fri, 8 Aug 2025 23:48:28 -0400 Subject: [PATCH 3/5] change name --- .../src/main/java/io/avaje/inject/generator/BeanReader.java | 2 +- .../src/main/java/io/avaje/inject/generator/MethodReader.java | 2 +- inject/src/main/java/io/avaje/inject/Lazy.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 acc17c139..8b23acae7 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 @@ -107,7 +107,7 @@ final class BeanReader { this.lazyProxyType = !lazy || delayed ? null : Util.lazyProxy(actualType); this.proxyLazy = lazy && lazyProxyType != null; if (lazy && !proxyLazy) { - if (lazyPrism != null && lazyPrism.forceProxy()) { + 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"); 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 f00878953..e092d96e9 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 @@ -78,7 +78,7 @@ final class MethodReader { this.lazyProxyType = lazy ? Util.lazyProxy(element) : null; this.proxyLazy = lazy && lazyProxyType != null; if (lazy && !proxyLazy) { - if (lazyPrism.forceProxy()) { + 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"); diff --git a/inject/src/main/java/io/avaje/inject/Lazy.java b/inject/src/main/java/io/avaje/inject/Lazy.java index 9a190a350..f5766e064 100644 --- a/inject/src/main/java/io/avaje/inject/Lazy.java +++ b/inject/src/main/java/io/avaje/inject/Lazy.java @@ -22,5 +22,5 @@ * Ensures that a compile-time proxy is generated, will fail compilation if missing conditions for * generation */ - boolean forceProxy() default false; + boolean enforceProxy() default false; } From b4553867b0b038dee9cbfb96e3b4bdd5a9ad98bb Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sat, 9 Aug 2025 16:37:11 +1200 Subject: [PATCH 4/5] Add more tests --- .../org/example/myapp/lazy2/LazyOneA.java | 26 ++++++++++ .../org/example/myapp/lazy2/LazyOneB.java | 34 +++++++++++++ .../java/org/example/myapp/lazy2/LazyTwo.java | 44 ++++++++++++++++ .../org/example/myapp/lazy2/package-info.java | 9 ++++ .../org/example/myapp/lazy2/LazyTwoTest.java | 50 +++++++++++++++++++ 5 files changed, 163 insertions(+) create mode 100644 blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneA.java create mode 100644 blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyOneB.java create mode 100644 blackbox-test-inject/src/main/java/org/example/myapp/lazy2/LazyTwo.java create mode 100644 blackbox-test-inject/src/main/java/org/example/myapp/lazy2/package-info.java create mode 100644 blackbox-test-inject/src/test/java/org/example/myapp/lazy2/LazyTwoTest.java 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 000000000..f4d753e78 --- /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 000000000..10c399760 --- /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 000000000..2df5d4094 --- /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 000000000..133f6dc74 --- /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 000000000..4c0ab883f --- /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); + } + } +} From 614a314cefece92df6a1231c18bdc07a62ebb6e2 Mon Sep 17 00:00:00 2001 From: Rob Bygrave Date: Sat, 9 Aug 2025 16:51:44 +1200 Subject: [PATCH 5/5] Format only --- .../main/java/io/avaje/inject/generator/BeanReader.java | 7 +++---- .../src/main/java/io/avaje/inject/generator/Util.java | 1 - 2 files changed, 3 insertions(+), 5 deletions(-) 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 8b23acae7..6ed4a6e3d 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 @@ -86,10 +86,9 @@ final class BeanReader { typeReader.process(); var lazyPrism = Util.isLazy(actualType); - this.lazy = - !FactoryPrism.isPresent(actualType) - && (lazyPrism != null - || importedComponent && ProcessingContext.isImportedLazy(actualType)); + this.lazy = !FactoryPrism.isPresent(actualType) + && (lazyPrism != null + || importedComponent && ProcessingContext.isImportedLazy(actualType)); this.requestParams = new BeanRequestParams(type); this.name = typeReader.name(); 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 432987a1c..232a7f322 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 @@ -447,7 +447,6 @@ static LazyPrism isLazy(Element element) { if (element == null) { return null; } - return LazyPrism.getOptionalOn(element).orElseGet(() -> isLazy(element.getEnclosingElement())); } }