Skip to content

Support Lazy proxy generation for generic factory methods #838

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
Jun 22, 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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Original file line number Diff line number Diff line change
@@ -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<String> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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<String> {

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() {}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package org.example.myapp.lazy.generic;

public interface LazyGenericInterface<T> {

void something();

void otherThing();
}
Original file line number Diff line number Diff line change
@@ -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<String> prov =
scope.get(new GenericType<LazyGenericInterface<String>>() {}.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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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;
Expand Down Expand Up @@ -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<? extends TypeParameterElement> 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);
Expand Down Expand Up @@ -165,7 +178,34 @@ private String methods() {
sb.append(");\n");
sb.append(" }\n\n");
}
return sb.toString();
}

private String buildTypeParametersDeclaration(List<? extends TypeParameterElement> 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<? extends TypeMirror> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
Expand All @@ -211,16 +209,26 @@ 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();
}

writer.append(" }");
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();
}
Expand Down
20 changes: 9 additions & 11 deletions inject-generator/src/main/java/io/avaje/inject/generator/Util.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ public LazyBeanTypes(Provider<Integer> intProvider) {
public void something() {}

@Override
public String somethingElse() { // TODO Auto-generated method stub
public String somethingElse() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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<Integer> intProvider;

@Inject
public LazyGenericBeanTypes(Provider<Integer> intProvider) {
this.intProvider = intProvider;
}

@Override
public void something() {}

@Override
public String somethingElse() {
return null;
}

@Override
public Object gen() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -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<String> lazyInterface() {
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.avaje.inject.generator.models.valid.lazy.generic;

public interface LazyGenericInterface<T> {

void something();

String somethingElse();

T gen();
}