Repository: avaje/avaje-inject
Pull request #752 (2025-01-05 02:02:32)

`PostConstruct` method Direct Injection
generates the lookup and fetches beans for `PostConstruct ` lifecycle methods. (Before we could only inject the `BeanScope`)

Given:

```java
@Singleton
public class Minos {

  @PostConstruct
  void prepareThyself(Serpent serpent, Consumer<String> c, BeanScope b) {}
}
```

the following is generated:

```java
@Generated("io.avaje.inject.generator")
public final class Minos$DI  {

  public static final Type TYPE_ConsumerString =
      new GenericType<Consumer<String>>(){}.type();

  public static void build(Builder builder) {
    if (builder.isBeanAbsent(Minos.class)) {
      var bean = new Minos();
      var $bean = builder.register(bean);
      builder.addPostConstruct(b -> $bean.prepareThyself(b.get(Serpent.class,"!serpent"), b.get(TYPE_ConsumerString), b));
    }
  }
}


Patch:
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 d261f4fd..32b66f2b 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
@@ -1,7 +1,6 @@
 package io.avaje.inject.generator;
 
 import static io.avaje.inject.generator.APContext.logError;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.LinkedHashSet;
@@ -11,7 +10,6 @@ import java.util.Set;
 import java.util.stream.Stream;
 
 import javax.lang.model.element.Element;
-import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.TypeKind;
 
@@ -30,7 +28,7 @@ final class BeanReader {
   private final List<MethodReader> injectMethods;
   private final List<MethodReader> factoryMethods;
   private final List<MethodReader> observerMethods;
-  private final Element postConstructMethod;
+  private final Optional<MethodReader> postConstructMethod;
   private final Element preDestroyMethod;
 
   private final ImportTypeMap importTypes = new ImportTypeMap();
@@ -165,6 +163,8 @@ final class BeanReader {
       factoryMethod.addImports(importTypes);
     }
 
+    postConstructMethod.ifPresent(m -> m.addImports(importTypes));
+
     conditions.addImports(importTypes);
     return this;
   }
@@ -242,6 +242,12 @@ final class BeanReader {
     for (MethodReader factoryMethod : factoryMethods()) {
       factoryMethod.addDependsOnGeneric(allUTypes);
     }
+
+    postConstructMethod.ifPresent(m ->
+      m.params().stream()
+        .filter(MethodParam::isGenericParam)
+        .map(MethodParam::getFullUType)
+        .forEach(allUTypes::add));
     return allUTypes;
   }
 
@@ -266,7 +272,7 @@ final class BeanReader {
    * Return true if lifecycle via annotated methods is required.
    */
   boolean hasLifecycleMethods() {
-    return (postConstructMethod != null || preDestroyMethod != null || typeReader.isClosable());
+    return (postConstructMethod.isPresent() || preDestroyMethod != null || typeReader.isClosable());
   }
 
   List<MetaData> createFactoryMethodMeta() {
@@ -327,9 +333,10 @@ final class BeanReader {
   }
 
   void addLifecycleCallbacks(Append writer, String indent) {
-    if (postConstructMethod != null && !registerProvider()) {
-      writer.indent(indent).append(" builder.addPostConstruct($bean::%s);", postConstructMethod.getSimpleName()).eol();
+    if (postConstructMethod.isPresent() && !registerProvider()) {
+      writePostConstruct(writer, indent, postConstructMethod.get());
     }
+
     if (preDestroyMethod != null) {
       lifeCycleNotSupported("@PreDestroy");
       var priority = preDestroyPriority == null || preDestroyPriority == 1000 ? "" : ", " + preDestroyPriority;
@@ -339,17 +346,46 @@ final class BeanReader {
     }
   }
 
+  private void writePostConstruct(Append writer, String indent, MethodReader postConstruct) {
+    writer.indent(indent).append(" builder.addPostConstruct(");
+    final var methodName = postConstruct.name();
+    final var params = postConstruct.params();
+    if (params.isEmpty() || Constants.BEANSCOPE.equals(params.get(0).getFullUType().shortType())) {
+      writer.append("$bean::%s);", methodName).eol();
+    } else {
+      writer.append("beanScope -> $bean.%s(", methodName);
+      writeLifeCycleGet(writer, params, "beanScope", "beanScope");
+      writer.append(");").eol();
+    }
+  }
+
   void prototypePostConstruct(Append writer, String indent) {
-    if (postConstructMethod != null) {
-      var postConstruct = (ExecutableElement) postConstructMethod;
-      writer.indent(indent).append(" bean.%s(", postConstructMethod.getSimpleName());
-      if (postConstruct.getParameters().isEmpty()) {
+    postConstructMethod.ifPresent(m -> {
+      writer.indent(indent).append(" bean.%s(", m.name());
+      if (m.params().isEmpty()) {
         writer.append(");").eol();
       } else {
-        writer.append("builder.get(io.avaje.inject.BeanScope.class));").eol();
+        writeLifeCycleGet(writer, m.params(), "builder", "builder.get(io.avaje.inject.BeanScope.class)");
+        writer.append(";").eol();
       }
       writer.eol();
+    });
+  }
+
+  private void writeLifeCycleGet(Append writer, List<MethodParam> params, String builderName, String beanScopeString) {
+    final var size = params.size();
+    for (int i = 0; i < size; i++) {
+      if (i > 0) {
+        writer.append(", ");
+      }
+      final var param = params.get(i);
+      if (Constants.BEANSCOPE.equals(param.getFullUType().fullWithoutAnnotations())) {
+        writer.append(beanScopeString);
+      } else {
+        param.builderGetDependency(writer, builderName);
+      }
     }
+    writer.append(")");
   }
 
   private void lifeCycleNotSupported(String lifecycle) {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
index 5f3e6718..073cd15b 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsInjection.java
@@ -30,7 +30,7 @@ final class TypeExtendsInjection {
   private final TypeElement baseType;
   private final boolean factory;
   private final List<AspectPair> typeAspects;
-  private Element postConstructMethod;
+  private Optional<MethodReader> postConstructMethod = Optional.empty();
   private Element preDestroyMethod;
   private Integer preDestroyPriority;
 
@@ -125,7 +125,7 @@ final class TypeExtendsInjection {
       notInjectMethods.add(methodKey);
     }
     if (AnnotationUtil.hasAnnotationWithName(element, "PostConstruct")) {
-      postConstructMethod = element;
+      postConstructMethod = Optional.of(new MethodReader(methodElement, type, importTypes).read());
       checkAspect = false;
     }
     if (AnnotationUtil.hasAnnotationWithName(element, "PreDestroy")) {
@@ -206,7 +206,7 @@ final class TypeExtendsInjection {
     return observerMethods;
   }
 
-  Element postConstructMethod() {
+  Optional<MethodReader> postConstructMethod() {
     return postConstructMethod;
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
index 732e216a..f1cb9166 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
@@ -7,6 +7,7 @@ import static io.avaje.inject.generator.ProcessingContext.asElement;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
@@ -101,7 +102,7 @@ final class TypeExtendsReader {
     return extendsInjection.observerMethods();
   }
 
-  Element postConstructMethod() {
+  Optional<MethodReader> postConstructMethod() {
     return extendsInjection.postConstructMethod();
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
index ab90f310..e7e14975 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
@@ -1,14 +1,14 @@
 package io.avaje.inject.generator;
 
-import javax.lang.model.element.Element;
-import javax.lang.model.element.TypeElement;
-import javax.lang.model.type.TypeKind;
-import javax.lang.model.type.TypeMirror;
+import static java.util.stream.Collectors.toList;
 
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
-import static java.util.stream.Collectors.toList;
+
+import javax.lang.model.element.Element;
+import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
 
 final class TypeReader {
 
@@ -107,7 +107,7 @@ final class TypeReader {
     return extendsReader.observerMethods();
   }
 
-  Element postConstructMethod() {
+  Optional<MethodReader> postConstructMethod() {
     return extendsReader.postConstructMethod();
   }
 


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeFour.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeFour.java
new file mode 100644
index 00000000..e055fb0e
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeFour.java
@@ -0,0 +1,18 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.PostConstruct;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class LifeFour {
+
+  public String _state;
+
+  @PostConstruct
+  void post(@Named("foo") LifeOne one, LifeTwo two) {
+    _state = "post|"
+      + (one != null ? "one|" : "")
+      + (two != null ? "two" : "");
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeOne.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeOne.java
new file mode 100644
index 00000000..94ec1bbe
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeOne.java
@@ -0,0 +1,17 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.PostConstruct;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class LifeOne {
+
+  public String _state;
+
+  @PostConstruct
+  void post(BeanScope scope) {
+    _state = "post|"
+      + (scope != null ? "scope" : "");
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeProtoTwo.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeProtoTwo.java
new file mode 100644
index 00000000..d7df00b8
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeProtoTwo.java
@@ -0,0 +1,18 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.PostConstruct;
+import io.avaje.inject.Prototype;
+
+@Prototype
+public class LifeProtoTwo {
+
+  public String _state;
+
+  @PostConstruct
+  void post(LifeOne one, BeanScope scope) {
+    _state = "post|"
+      + (one != null ? "one|" : "")
+      + (scope != null ? "scope" : "");
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeThree.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeThree.java
new file mode 100644
index 00000000..91d9b393
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeThree.java
@@ -0,0 +1,16 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.PostConstruct;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class LifeThree {
+
+  public String _state;
+
+  @PostConstruct
+  void post(LifeOne one) {
+    _state = "post|"
+      + (one != null ? "one" : "");
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeTwo.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeTwo.java
new file mode 100644
index 00000000..ebe8ec74
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/LifeTwo.java
@@ -0,0 +1,18 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.PostConstruct;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class LifeTwo {
+
+  public String _state;
+
+  @PostConstruct
+  void post(LifeOne one, BeanScope scope) {
+    _state = "post|"
+      + (one != null ? "one|" : "")
+      + (scope != null ? "scope" : "");
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/config/PostConstructParametersTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/config/PostConstructParametersTest.java
new file mode 100644
index 00000000..816956fa
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/config/PostConstructParametersTest.java
@@ -0,0 +1,27 @@
+package org.example.myapp.config;
+
+import io.avaje.inject.BeanScope;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class PostConstructParametersTest {
+
+  @Test
+  void factorySecondaryAsProvider() {
+    try (BeanScope testScope = BeanScope.builder().build()) {
+      var one = testScope.get(LifeOne.class);
+      var two = testScope.get(LifeTwo.class);
+      var three = testScope.get(LifeThree.class);
+      var four = testScope.get(LifeFour.class);
+      var protoTwo = testScope.get(LifeProtoTwo.class);
+
+      assertThat(one._state).isEqualTo("post|scope");
+      assertThat(two._state).isEqualTo("post|one|scope");
+      assertThat(three._state).isEqualTo("post|one");
+      assertThat(four._state).isEqualTo("post|one|two");
+      assertThat(protoTwo._state).isEqualTo("post|one|scope");
+    }
+  }
+
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Minos.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Minos.java
new file mode 100644
index 00000000..8b811862
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Minos.java
@@ -0,0 +1,18 @@
+package io.avaje.inject.generator.models.valid.lifecycle;
+
+import java.util.function.Consumer;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.PostConstruct;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class Minos {
+
+  @PostConstruct
+  void prepareThyself(Serpent serpent, Consumer<String> c, BeanScope b) {}
+
+  //  @PreDestroy
+  //  void thyEndIsNow() {
+  //  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Serpent.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Serpent.java
new file mode 100644
index 00000000..05eda842
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lifecycle/Serpent.java
@@ -0,0 +1,15 @@
+package io.avaje.inject.generator.models.valid.lifecycle;
+
+import java.util.function.Consumer;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.PostConstruct;
+import io.avaje.inject.Prototype;
+
+@Prototype
+public class Serpent {
+
+  @PostConstruct
+  void hiss(Consumer<String> c, BeanScope b) {}
+
+}