Repository: avaje/avaje-inject
Pull request #579 (2024-05-28 03:49:47)

Multi-register beans with `List` Factory Methods
Now can do:

```java
@Factory
public class ListFactory {

  @Bean
  List<String> test() {
    return List.of("test1", "test2");
  }

  @Bean
  String test3() {
    return "test3";
  }
}
```

and get `"test1", "test2","test3"` when injecting a `List<String>`. (Even Spring cannot do this currently)



Patch:
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 36a2c7b2..8f10416b 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
@@ -35,6 +35,7 @@ final class MethodReader {
   private final String name;
   private final TypeReader typeReader;
   private final boolean optionalType;
+  private final boolean multiRegister;
   private final BeanConditions conditions = new BeanConditions();
   private MethodParam observeParameter;
 
@@ -62,11 +63,18 @@ final class MethodReader {
     String raw = returnMirror.toString();
     if (Util.isOptional(raw)) {
       optionalType = true;
-      returnTypeRaw = Util.trimWildcard(Util.extractOptionalType(raw));
       uType = uType.param0();
+      returnTypeRaw = Util.trimWildcard(uType.full());
+      multiRegister = false;
+    } else if (raw.startsWith("java.util.List")) {
+      optionalType = false;
+      uType = uType.param0();
+      returnTypeRaw = Util.trimWildcard(uType.full());
+      multiRegister = true;
     } else {
       optionalType = false;
       returnTypeRaw = Util.trimWildcard(raw);
+      multiRegister = false;
     }
     this.genericType = uType;
     String mainType = genericType.mainType();
@@ -78,8 +86,10 @@ final class MethodReader {
     String destroyMethod = (bean == null) ? null : bean.destroyMethod();
     this.destroyPriority = (bean == null) ? null : bean.destroyPriority();
     this.beanCloseable = (bean != null) && bean.autoCloseable();
-    this.name = qualifierName;
-    TypeElement returnElement = asElement(returnMirror);
+    // for multiRegister we desire a qualifier name such that builder.isAddBeanFor() uses it and allows
+    // other beans of the same type to also register, otherwise it defaults to slightly confusing behaviour
+    this.name = multiRegister && qualifierName == null ? "multi" : qualifierName;
+    TypeElement returnElement = multiRegister ? APContext.typeElement(uType.mainType()) : asElement(returnMirror);
     if (returnElement == null) {
       this.typeReader = null;
       this.initMethod = initMethod;
@@ -196,6 +206,12 @@ final class MethodReader {
       APContext.logError("Error - Optional type with @Prototype method is not supported", element);
       return;
     }
+
+    if (multiRegister) {
+      APContext.logError("Error - List<Provider<>> type with @Prototype method is not supported", element);
+      return;
+    }
+
     String indent = "    ";
     writer.indent(indent).append("  builder");
     if (prototype) {
@@ -229,9 +245,16 @@ final class MethodReader {
       }
       String indent = optionalType ? "        " : "      ";
       writer.indent(indent);
-      if (hasLifecycleMethods()) {
+      var hasLifecycleMethods = hasLifecycleMethods();
+
+      if (hasLifecycleMethods && multiRegister) {
+        writer.append("bean.stream()").eol().indent(indent).append("    .map(");
+      } else if (hasLifecycleMethods) {
         writer.append("var $bean = ");
+      } else if (multiRegister) {
+        writer.append("bean.forEach(");
       }
+
       writer.append("builder");
       if (primary) {
         writer.append(".asPrimary()");
@@ -240,22 +263,51 @@ final class MethodReader {
       } else if (prototype) {
         writer.append(".asPrototype()");
       }
+
+      var lineEnd = hasLifecycleMethods ? "" : ";";
       if (Util.isProvider(returnTypeRaw)) {
-        writer.append(".registerProvider(bean);").eol();
+        var providerRegister = multiRegister ? "::registerProvider)" + lineEnd : ".registerProvider(bean);";
+        writer.append(providerRegister).eol();
       } else {
-        writer.append(".register(bean);").eol();
+        var beanRegister = multiRegister ? "::register)" + lineEnd : ".register(bean);";
+        writer.append(beanRegister).eol();
       }
-      if (notEmpty(initMethod)) {
-        writer.indent(indent).append("builder.addPostConstruct($bean::%s);", initMethod).eol();
+
+      final var hasInitMethod = notEmpty(initMethod);
+      if (hasInitMethod) {
+        var addPostConstruct =
+          multiRegister
+            ? "    .peek(b -> builder.addPostConstruct(b::%s))"
+            : "builder.addPostConstruct($bean::%s);";
+        writer.indent(indent).append(addPostConstruct, initMethod).eol();
       }
+
       var priority = destroyPriority == null || destroyPriority == 1000 ? "" : ", " + destroyPriority;
       if (notEmpty(destroyMethod)) {
-        writer.indent(indent).append("builder.addPreDestroy($bean::%s%s);", destroyMethod, priority).eol();
+        var addPreDestroy =
+          multiRegister
+            ? "    .forEach(b -> builder.addPreDestroy(b::%s%s));"
+            : "builder.addPreDestroy($bean::%s%s);";
+        writer.indent(indent).append(addPreDestroy, destroyMethod, priority).eol();
+
       } else if (typeReader != null && typeReader.isClosable()) {
-        writer.indent(indent).append("builder.addPreDestroy($bean::close%s);", priority).eol();
+        var addPreDestroy =
+          multiRegister
+            ? "    .forEach(b -> builder.addPreDestroy(b::close%s));"
+            : "builder.addPreDestroy($bean::close%s);";
+        writer.indent(indent).append(addPreDestroy, priority).eol();
+
       } else if (beanCloseable) {
-        writer.indent(indent).append("builder.addAutoClosable($bean);").eol();
+        var addAutoClosable =
+          multiRegister
+            ? "    .forEach(builder::addAutoClosable);"
+            : "builder.addAutoClosable(bean);";
+        writer.indent(indent).append(addAutoClosable).eol();
+
+      } else if (multiRegister && hasInitMethod) {
+        writer.indent(indent).append("    .forEach(x -> {});").eol();
       }
+
       if (optionalType) {
         writer.append("      }").eol();
       }


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/ListFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/ListFactory.java
new file mode 100644
index 00000000..7ec52917
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/ListFactory.java
@@ -0,0 +1,132 @@
+package org.example.myapp;
+
+import java.util.List;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.PostConstruct;
+import io.avaje.inject.PreDestroy;
+import jakarta.inject.Named;
+
+@Factory
+public class ListFactory {
+
+  @Bean
+  List<String> test() {
+    return List.of("test1", "test2");
+  }
+
+  @Bean
+  String test3() {
+    return "test3";
+  }
+
+  @Bean(initMethod = "initMe")
+  List<Some> multiple() {
+    return List.of(new Some("m1"), new Some("m2"), new Some("m3"));
+  }
+
+  @Bean(initMethod = "postInit")
+  Some one() {
+    return new Some("s1");
+  }
+
+  @Named("multipleVanilla")
+  @Bean
+  List<Some2> multipleVanilla() {
+    return List.of(new Some2("v1"), new Some2("v2"));
+  }
+
+  @Bean
+  Some2 oneVanilla() {
+    return new Some2("v3");
+  }
+
+  @Bean
+  List<Some3> multiple3() {
+    return List.of(new Some3("31"), new Some3("32"));
+  }
+
+  @Bean
+  List<Some4> multiple4() {
+    return List.of(new Some4("41"), new Some4("42"));
+  }
+
+  public static class Some {
+
+    public final String name;
+    public String other;
+
+    public Some(String name) {
+      this.name = name;
+    }
+
+    public String name() {
+      return name;
+    }
+
+    public String other() {
+      return other;
+    }
+
+    public void initMe() {
+      other = name + "initMe";
+    }
+
+    @PostConstruct
+    public void postInit() {
+      other = name + "postInit";
+    }
+
+    @PreDestroy
+    void shutdown() {
+      other = "stop";
+    }
+  }
+
+  public static class Some2 {
+
+    public final String name;
+
+    public Some2(String name) {
+      this.name = name;
+    }
+
+    public String name() {
+      return name;
+    }
+  }
+
+  public static class Some3 {
+
+    public final String name;
+
+    public Some3(String name) {
+      this.name = name;
+    }
+
+    public String name() {
+      return name;
+    }
+    @PreDestroy
+    void shutdown() {
+
+    }
+  }
+
+  public static class Some4 {
+
+    public final String name;
+
+    public Some4(String name) {
+      this.name = name;
+    }
+
+    public String name() {
+      return name;
+    }
+    @PostConstruct
+    void myInitMethod() {
+    }
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/ListService.java b/blackbox-test-inject/src/main/java/org/example/myapp/ListService.java
new file mode 100644
index 00000000..55bc4177
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/ListService.java
@@ -0,0 +1,25 @@
+package org.example.myapp;
+
+import java.util.List;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ListService {
+
+  private final List<String> strings;
+  private final List<ListFactory.Some> somes;
+
+  public ListService(List<String> strings, List<ListFactory.Some> somes) {
+    this.strings = strings;
+    this.somes = somes;
+  }
+
+  public List<String> strings() {
+    return strings;
+  }
+
+  public List<ListFactory.Some> somes() {
+    return somes;
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/ListFactoryTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/ListFactoryTest.java
new file mode 100644
index 00000000..b562ea12
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/ListFactoryTest.java
@@ -0,0 +1,32 @@
+package org.example.myapp;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+@InjectTest
+class ListFactoryTest {
+
+  @Inject ListService list;
+
+  @Test
+  void test() {
+
+    assertThat(list.strings()).contains("test1", "test1", "test3");
+
+    List<ListFactory.Some> somes = list.somes();
+    assertThat(somes).hasSize(4);
+
+    var names = somes.stream()
+      .map(ListFactory.Some::name)
+      .collect(Collectors.toList());
+
+    assertThat(names).contains("m1", "m2", "m3", "s1");
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/ListFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/ListFactory.java
new file mode 100644
index 00000000..3389b0c8
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/ListFactory.java
@@ -0,0 +1,25 @@
+package io.avaje.inject.generator.models.valid;
+
+import java.util.List;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+
+@Factory
+public class ListFactory {
+
+  @Bean(initMethod = "postConstruct")
+  List<Something> userId() {
+    return List.of(new Something());
+  }
+
+  public void close() throws Exception {}
+
+  public static class Something implements AutoCloseable {
+
+    public void postConstruct() {}
+
+    @Override
+    public void close() throws Exception {}
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/SomethingService.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/SomethingService.java
new file mode 100644
index 00000000..b9fc52de
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/SomethingService.java
@@ -0,0 +1,16 @@
+package io.avaje.inject.generator.models.valid;
+
+import java.util.List;
+
+import io.avaje.inject.generator.models.valid.ListFactory.Something;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class SomethingService {
+
+  List<Something> list;
+
+  public SomethingService(List<Something> list) {
+    this.list = list;
+  }
+}