Skip to content

Make @Lazy work on a package/module level #884

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Aug 9, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* Use Lazy for all the beans in this package.
* <p>
* 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;
Original file line number Diff line number Diff line change
@@ -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 <init> 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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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);
}

Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()));
}
}
11 changes: 9 additions & 2 deletions inject/src/main/java/io/avaje/inject/Lazy.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}