Repository: avaje/avaje-inject
Pull request #513 (2024-02-26 21:20:37)

Disable Module Validation when the Avaje Build Plugin is Detected
The new maven plugin can register services post compilation, so we can disable module-info validation when we detect it's presence.

also the new plugin solves #496 


Patch:
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 88eb1dee..c48f697b 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -3,7 +3,23 @@ updates:
 - package-ecosystem: maven
   directory: "/"
   schedule:
-    interval: "daily"
+    interval: "weekly"
   open-pull-requests-limit: 10
   ignore:
       - dependency-name: "jakarta.inject:jakarta.inject-api"
+  groups:
+      dev-deps:
+        dependency-type: "development"
+      prod-deps:
+        dependency-type: "production"
+
+- package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+    open-pull-requests-limit: 5
+    commit-message:
+      prefix: "[workflow]"
+    labels:
+      - "dependencies"
+    target-branch: "master"
diff --git a/README.md b/README.md
index c1ff50fa..b9807d19 100644
--- a/README.md
+++ b/README.md
@@ -143,9 +143,9 @@ public final class ExampleModule implements Module {
 
 ## Differences to Dagger
 
-- Specifically Aimed for server-side development (rather than Android)
+- Specifically aimed for server-side development (rather than Android)
 - Provides API to obtain all bean instances that implement an interface
-- Lifecycle methods with `@PostConstruct` and `@PreDestory`
+- Lifecycle methods with `@PostConstruct` and `@PreDestroy`
 - Spring-like factory classes with `@Factory` and `@Bean`
 - Conditional Wiring based on active profiles or existing beans/properties
 
diff --git a/blackbox-aspect/pom.xml b/blackbox-aspect/pom.xml
index e2d11752..2774d7d8 100644
--- a/blackbox-aspect/pom.xml
+++ b/blackbox-aspect/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <artifactId>avaje-inject-parent</artifactId>
     <groupId>io.avaje</groupId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/blackbox-other/pom.xml b/blackbox-other/pom.xml
index 9a457c78..201b43aa 100644
--- a/blackbox-other/pom.xml
+++ b/blackbox-other/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <artifactId>avaje-inject-parent</artifactId>
     <groupId>io.avaje</groupId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
 
   <artifactId>blackbox-other</artifactId>
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index 72707168..98ce0d6e 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -6,7 +6,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
 
   <artifactId>avaje-inject-generator</artifactId>
@@ -35,7 +35,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>junit</artifactId>
-      <version>1.4</version>
+      <version>1.5</version>
       <scope>test</scope>
     </dependency>
 
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 c892afdd..f014d64c 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
@@ -3,6 +3,7 @@ package io.avaje.inject.generator;
 import static io.avaje.inject.generator.APContext.logError;
 
 import javax.lang.model.element.Element;
+import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.TypeKind;
 
@@ -32,6 +33,7 @@ final class BeanReader {
   private final boolean prototype;
   private final boolean primary;
   private final boolean secondary;
+  private final boolean lazy;
   private final boolean proxy;
   private final BeanAspects aspects;
   private final BeanConditions conditions = new BeanConditions();
@@ -50,6 +52,7 @@ final class BeanReader {
     this.prototype = PrototypePrism.isPresent(beanType);
     this.primary = PrimaryPrism.isPresent(beanType);
     this.secondary = !primary && SecondaryPrism.isPresent(beanType);
+    this.lazy = !FactoryPrism.isPresent(beanType) && LazyPrism.isPresent(beanType);
     this.typeReader = new TypeReader(UType.parse(beanType.asType()), beanType, importTypes, factory);
 
     typeReader.process();
@@ -110,8 +113,12 @@ final class BeanReader {
     return aspects;
   }
 
-  boolean prototype() {
-    return prototype;
+  boolean registerProvider() {
+    return prototype || lazy;
+  }
+
+  boolean lazy() {
+    return lazy;
   }
 
   boolean importedComponent() {
@@ -261,7 +268,7 @@ final class BeanReader {
   }
 
   void buildRegister(Append writer) {
-    if (prototype) {
+    if (prototype || lazy) {
       return;
     }
     writer.indent("      ");
@@ -278,11 +285,12 @@ final class BeanReader {
   }
 
   void addLifecycleCallbacks(Append writer, String indent) {
-    if (postConstructMethod != null && !prototype) {
+
+    if (postConstructMethod != null && !registerProvider()) {
       writer.indent(indent).append(" builder.addPostConstruct($bean::%s);", postConstructMethod.getSimpleName()).eol();
     }
     if (preDestroyMethod != null) {
-      prototypeNotSupported("@PreDestroy");
+      lifeCycleNotSupported("@PreDestroy");
       var priority = preDestroyPriority == null || preDestroyPriority == 1000 ? "" : ", " + preDestroyPriority;
       writer.indent(indent).append(" builder.addPreDestroy($bean::%s%s);", preDestroyMethod.getSimpleName(), priority).eol();
     } else if (typeReader.isClosable() && !prototype) {
@@ -292,13 +300,24 @@ final class BeanReader {
 
   void prototypePostConstruct(Append writer, String indent) {
     if (postConstructMethod != null) {
-      writer.append("%s bean.%s();", indent, postConstructMethod.getSimpleName()).eol();
+      var postConstruct = (ExecutableElement) postConstructMethod;
+      writer.append("%s bean.%s(", indent, postConstructMethod.getSimpleName());
+      if (postConstruct.getParameters().isEmpty()) {
+        writer.append(");").eol();
+      } else {
+        writer.append("builder.get(io.avaje.inject.BeanScope.class));").eol();
+      }
+      writer.eol();
     }
   }
 
-  private void prototypeNotSupported(String lifecycle) {
-    if (prototype) {
-      logError(beanType, "@Prototype scoped bean does not support %s lifecycle method", lifecycle);
+  private void lifeCycleNotSupported(String lifecycle) {
+    if (registerProvider()) {
+      logError(
+        beanType,
+        "%s scoped bean does not support the %s lifecycle method",
+        prototype ? "@Prototype" : "@Lazy",
+        lifecycle);
     }
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
index 5934374a..325e38a2 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
@@ -1,6 +1,9 @@
 package io.avaje.inject.generator;
 
 import static java.util.Map.entry;
+
+import java.net.URI;
+import java.nio.file.Paths;
 import static java.util.List.of;
 
 import java.util.List;
@@ -9,6 +12,8 @@ import java.util.ServiceConfigurationError;
 import java.util.ServiceLoader;
 import java.util.Set;
 
+import javax.tools.StandardLocation;
+
 import io.avaje.inject.spi.Module;
 import io.avaje.inject.spi.Plugin;
 
@@ -43,13 +48,16 @@ final class ExternalProvider {
 
   static void registerModuleProvidedTypes(Set<String> providedTypes) {
     if (!injectAvailable) {
-      System.out.println("Unable to detect Avaje Inject in Annotation Processor ClassPath, use the Avaje Inject Maven/Gradle plugin for detecting Inject Modules from dependencies");
+      if (!pluginExists("build/avaje-plugin-exists.txt")
+        && !pluginExists("target/avaje-plugin-exists.txt")) {
+        APContext.logNote("Unable to detect Avaje Inject in Annotation Processor ClassPath, use the Avaje Inject Maven/Gradle plugin for detecting Inject Modules from dependencies");
+      }
       return;
     }
 
     final var iterator = ServiceLoader.load(Module.class, ExternalProvider.class.getClassLoader()).iterator();
     if (!iterator.hasNext()) {
-      System.out.println("No external modules detected");
+      APContext.logNote("No external modules detected");
       return;
     }
     while (iterator.hasNext()) {
@@ -78,7 +86,7 @@ final class ExternalProvider {
   static void registerPluginProvidedTypes(ScopeInfo defaultScope) {
     avajePlugins.forEach((k, v) -> {
       if (APContext.typeElement(k) != null) {
-        System.out.println("Loaded Plugin: " + k);
+        APContext.logNote("Loaded Plugin: " + k);
         v.forEach(defaultScope::pluginProvided);
       }
     });
@@ -90,7 +98,7 @@ final class ExternalProvider {
       if (avajePlugins.containsKey(name)) {
         continue;
       }
-      System.out.println("Loaded Plugin: " + plugin.getClass().getCanonicalName());
+      APContext.logNote("Loaded Plugin: " + plugin.getClass().getCanonicalName());
       for (final Class<?> provide : plugin.provides()) {
         defaultScope.pluginProvided(provide.getCanonicalName());
       }
@@ -99,4 +107,19 @@ final class ExternalProvider {
       }
     }
   }
+
+  private static boolean pluginExists(String relativeName) {
+    try {
+      final String resource =
+        APContext.filer()
+          .getResource(StandardLocation.CLASS_OUTPUT, "", relativeName)
+          .toUri()
+          .toString()
+          .replaceFirst("/target/classes", "")
+          .replaceFirst("/build/classes/java/main", "");
+      return Paths.get(new URI(resource)).toFile().exists();
+    } catch (final Exception e) {
+      return false;
+    }
+  }
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index d1062101..bff8848c 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -78,14 +78,17 @@ final class MetaData {
   }
 
   private String trimName(String name) {
-    return "".equals(name) ? null : name;
+    if (name == null || name.isEmpty()) {
+      return null;
+    }
+    return name.replace("\\\"", "'");
   }
 
   String buildName() {
     if (Util.isVoid(type)) {
       return "void_" + Util.trimMethod(method);
     } else {
-      final String trimType = Util.trimMethod(type);
+      final String trimType = Util.trimMethod(Util.unwrapProvider(type));
       if (name != null) {
         return trimType + "_" + name.replaceAll("[^a-zA-Z0-9_$]+", "_");
       } else {
@@ -243,7 +246,11 @@ final class MetaData {
     if (size > 1) {
       sb.eol().append("        ");
     }
+    var seen = new HashSet<String>();
     for (int i = 0; i < types.size(); i++) {
+      if (!seen.add(types.get(i))) {
+        continue;
+      }
       if (i > 0) {
         sb.append(",").eol().append("        ");
       }
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 328776ff..4855fc3a 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
@@ -21,6 +21,7 @@ final class MethodReader {
   private final boolean prototype;
   private final boolean primary;
   private final boolean secondary;
+  private final boolean lazy;
   private final String returnTypeRaw;
   private final UType genericType;
   private final String shortName;
@@ -46,11 +47,13 @@ final class MethodReader {
       prototype = PrototypePrism.isPresent(element);
       primary = PrimaryPrism.isPresent(element);
       secondary = SecondaryPrism.isPresent(element);
+      lazy = LazyPrism.isPresent(element) || LazyPrism.isPresent(element.getEnclosingElement());
       conditions.readAll(element);
     } else {
       prototype = false;
       primary = false;
       secondary = false;
+      lazy = false;
     }
     this.methodName = element.getSimpleName().toString();
     TypeMirror returnMirror = element.getReturnType();
@@ -87,6 +90,9 @@ final class MethodReader {
       this.initMethod = lifecycleReader.initMethod();
       this.destroyMethod = lifecycleReader.destroyMethod();
     }
+    if (lazy && prototype) {
+      APContext.logError("Cannot use both @Lazy and @Prototype");
+    }
   }
 
   @Override
@@ -181,20 +187,25 @@ final class MethodReader {
 
   void builderAddBeanProvider(Append writer) {
     if (isVoid) {
-      writer.append("Error - void @Prototype method ?").eol();
+      APContext.logError("Error - void @Prototype method ?", element);
       return;
     }
     if (optionalType) {
-      writer.append("Error - Optional type with @Prototype method is not supported").eol();
+      APContext.logError("Error - Optional type with @Prototype method is not supported", element);
       return;
     }
     String indent = "    ";
+    writer.indent(indent).append("  builder");
     if (prototype) {
-      writer.indent(indent).append("  builder.asPrototype().registerProvider(() -> {").eol();
-    } else {
-      writer.indent(indent).append("  builder.asSecondary().registerProvider(() -> {").eol();
+      writer.append(".asPrototype()");
+    } else if(secondary) {
+      writer.append(".asSecondary()");
     }
-    writer.indent(indent).append("    return ");
+
+    writer.indent(String.format(".%s(() -> {", lazy ? "registerLazy" : "registerProvider")).eol();
+
+    startTry(writer, "  ");
+    writer.indent(indent).append("  return ");
     writer.append("factory.%s(", methodName);
     for (int i = 0; i < params.size(); i++) {
       if (i > 0) {
@@ -203,6 +214,7 @@ final class MethodReader {
       params.get(i).builderGetDependency(writer, "builder");
     }
     writer.append(");").eol();
+    endTry(writer, "  ");
     writer.indent(indent).append("  });").eol();
     writer.indent(indent).append("}").eol();
   }
@@ -224,7 +236,11 @@ final class MethodReader {
       } else if (secondary) {
         writer.append(".asSecondary()");
       }
-      writer.append(".register(bean);").eol();
+      if (Util.isProvider(returnTypeRaw)) {
+        writer.append(".registerProvider(bean);").eol();
+      } else {
+        writer.append(".register(bean);").eol();
+      }
       if (notEmpty(initMethod)) {
         writer.indent(indent).append("builder.addPostConstruct($bean::%s);", initMethod).eol();
       }
@@ -289,18 +305,26 @@ final class MethodReader {
   }
 
   void startTry(Append writer) {
+    startTry(writer, "");
+  }
+
+  void startTry(Append writer, String indent) {
     if (methodThrows()) {
-      writer.append("      try {").eol();
-      writer.setExtraIndent("  ");
+      writer.append(indent).append("      try {").eol();
+      writer.setExtraIndent("  " + indent);
     }
   }
 
   void endTry(Append writer) {
+    endTry(writer, "");
+  }
+
+  void endTry(Append writer, String indent) {
     if (methodThrows()) {
       writer.setExtraIndent(null);
-      writer.append("      } catch (Throwable e) {").eol();
-      writer.append("        throw new RuntimeException(\"Error during wiring\", e);").eol();
-      writer.append("      }").eol();
+      writer.append(indent).append("      } catch (Throwable e) {").eol();
+      writer.append(indent).append("        throw new RuntimeException(\"Error during wiring\", e);").eol();
+      writer.append(indent).append("      }").eol();
     }
   }
 
@@ -331,7 +355,7 @@ final class MethodReader {
     }
   }
 
-  public void commentBuildMethod(Append writer) {
+  void commentBuildMethod(Append writer) {
     writer.append(CODE_COMMENT_BUILD_FACTORYBEAN, shortName, factoryShortName, methodName).eol();
   }
 
@@ -339,8 +363,12 @@ final class MethodReader {
     return prototype;
   }
 
+  boolean isLazy() {
+    return lazy;
+  }
+
   boolean isUseProviderForSecondary() {
-    return secondary && !optionalType;
+    return secondary && !optionalType && !Util.isProvider(returnTypeRaw);
   }
 
   boolean isPublic() {
@@ -424,8 +452,6 @@ final class MethodReader {
       writer.append(builderName).append(".").append(utilType.getMethod(nullable, isBeanMap));
       if (!genericType.isGeneric() || genericType.param0().kind() == TypeKind.WILDCARD) {
         writer.append(Util.shortName(genericType.mainType())).append(".class");
-      } else if (isProvider()) {
-        writer.append(providerParam()).append(".class");
       } else {
         writer.append("TYPE_").append(Util.shortName(genericType).replace(".", "_"));
       }
@@ -445,10 +471,6 @@ final class MethodReader {
       writer.append(")");
     }
 
-    private String providerParam() {
-      return Util.shortName(Util.unwrapProvider(paramType));
-    }
-
     String simpleName() {
       return simpleName;
     }
@@ -536,4 +558,5 @@ final class MethodReader {
       return element;
     }
   }
+
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
index 4169784e..b708b70c 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java
@@ -1,7 +1,6 @@
 package io.avaje.inject.generator;
 
 import javax.annotation.processing.FilerException;
-import javax.annotation.processing.ProcessingEnvironment;
 import javax.lang.model.element.TypeElement;
 import javax.lang.model.type.TypeMirror;
 import javax.tools.FileObject;
@@ -19,9 +18,9 @@ final class ProcessingContext {
 
   private static final ThreadLocal<Ctx> CTX = new ThreadLocal<>();
   private static boolean processingOver;
+  private static boolean moduleValidation;
 
-  private ProcessingContext() {
-  }
+  private ProcessingContext() {}
 
   static final class Ctx {
     private final Set<String> uniqueModuleNames = new HashSet<>();
@@ -31,21 +30,20 @@ final class ProcessingContext {
     private final List<TypeElement> delayQueue = new ArrayList<>();
     private boolean validated;
 
-    public Ctx(Set<String> moduleFileProvided) {
+    Ctx(Set<String> moduleFileProvided) {
       ExternalProvider.registerModuleProvidedTypes(providedTypes);
       providedTypes.addAll(moduleFileProvided);
     }
 
-    public Ctx() {
-    }
+    Ctx() {}
   }
 
-  public static void init(ProcessingEnvironment processingEnv, Set<String> moduleFileProvided) {
+  static void init(Set<String> moduleFileProvided, boolean performModuleValidation) {
+    ProcessingContext.moduleValidation = performModuleValidation;
     CTX.set(new Ctx(moduleFileProvided));
-    APContext.init(processingEnv);
   }
 
-  public static void testInit() {
+  static void testInit() {
     CTX.set(new Ctx());
   }
 
@@ -142,18 +140,13 @@ final class ProcessingContext {
 
   static void validateModule(String injectFQN) {
     var module = getProjectModuleElement();
-    if (module != null && !CTX.get().validated && !module.isUnnamed()) {
-
+    if (moduleValidation && module != null && !CTX.get().validated && !module.isUnnamed()) {
       CTX.get().validated = true;
-
       try (var reader = getModuleInfoReader()) {
-
         var noProvides = reader.lines().noneMatch(s -> s.contains(injectFQN));
-
         if (noProvides) {
           logError(module, "Missing \"provides io.avaje.inject.spi.Module with %s;\"", injectFQN);
         }
-
       } catch (Exception e) {
         // can't read module
       }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
index 316a63aa..c1f5cce9 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
@@ -13,10 +13,10 @@ import javax.lang.model.element.TypeElement;
 import javax.lang.model.util.ElementFilter;
 import javax.lang.model.util.Elements;
 import javax.tools.StandardLocation;
-import java.io.BufferedReader;
 import java.io.IOException;
-import java.io.InputStreamReader;
 import java.net.URI;
+import java.nio.file.Files;
+import java.nio.file.Paths;
 import java.util.*;
 import java.util.stream.Collectors;
 
@@ -47,6 +47,7 @@ public final class Processor extends AbstractProcessor {
   private boolean readModuleInfo;
   private final Set<String> pluginFileProvided = new HashSet<>();
   private final Set<String> moduleFileProvided = new HashSet<>();
+  private boolean performModuleValidation;
 
   @Override
   public SourceVersion getSupportedSourceVersion() {
@@ -56,8 +57,9 @@ public final class Processor extends AbstractProcessor {
   @Override
   public synchronized void init(ProcessingEnvironment processingEnv) {
     super.init(processingEnv);
-    loadProvidedFiles(processingEnv.getFiler());
-    ProcessingContext.init(processingEnv, moduleFileProvided);
+    APContext.init(processingEnv);
+    loadProvidedFiles();
+    ProcessingContext.init(moduleFileProvided, performModuleValidation);
     this.elementUtils = processingEnv.getElementUtils();
     this.allScopes = new AllScopes();
     this.defaultScope = allScopes.defaultScope();
@@ -65,36 +67,33 @@ public final class Processor extends AbstractProcessor {
     pluginFileProvided.forEach(defaultScope::pluginProvided);
   }
 
-  /**
-   * Loads provider files generated by avaje-inject-maven-plugin
-   */
-  void loadProvidedFiles(Filer filer) {
-    pluginFileProvided.addAll(lines(filer, "target/avaje-plugin-provides.txt", "/target/classes"));
-    moduleFileProvided.addAll(lines(filer, "target/avaje-module-provides.txt", "/target/classes"));
-    pluginFileProvided.addAll(lines(filer, "build/avaje-plugin-provides.txt", "/build/classes/java/main"));
-    moduleFileProvided.addAll(lines(filer, "build/avaje-module-provides.txt", "/build/classes/java/main"));
+  /** Loads provider files generated by avaje-inject-maven-plugin */
+  void loadProvidedFiles() {
+    this.performModuleValidation =
+        lines("target/avaje-plugin-exists.txt").isEmpty()
+            && lines("build/avaje-plugin-exists.txt").isEmpty();
+    pluginFileProvided.addAll(lines("target/avaje-plugin-provides.txt"));
+    moduleFileProvided.addAll(lines("target/avaje-module-provides.txt"));
+    pluginFileProvided.addAll(lines("build/avaje-plugin-provides.txt"));
+    moduleFileProvided.addAll(lines("build/avaje-module-provides.txt"));
   }
 
-  private static List<String> lines(Filer filer, String relativeName, String replace) {
+  private List<String> lines(String relativeName) {
     try {
-      final String resource = resource(filer, relativeName, replace);
-      try (var inputStream = new URI(resource).toURL().openStream();
-           var reader = new BufferedReader(new InputStreamReader(inputStream))) {
-        return reader.lines().collect(Collectors.toList());
-      }
+      final String resource =
+        processingEnv
+          .getFiler()
+          .getResource(StandardLocation.CLASS_OUTPUT, "", relativeName)
+          .toUri()
+          .toString()
+          .replaceFirst("/target/classes", "")
+          .replaceFirst("/build/classes/java/main", "");
+      return Files.readAllLines(Paths.get(new URI(resource)));
     } catch (final Exception e) {
       return Collections.emptyList();
     }
   }
 
-  private static String resource(Filer filer, String relativeName, String replace) throws IOException {
-    return filer
-      .getResource(StandardLocation.CLASS_OUTPUT, "", relativeName)
-      .toUri()
-      .toString()
-      .replace(replace, "");
-  }
-
   @Override
   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
     APContext.setProjectModuleElement(annotations, roundEnv);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index afa1bb7a..b4de2f78 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -2,6 +2,7 @@ package io.avaje.inject.generator;
 
 import static io.avaje.inject.generator.APContext.createSourceFile;
 import static io.avaje.inject.generator.APContext.logError;
+import static java.util.stream.Collectors.toSet;
 
 import java.io.IOException;
 import java.io.Writer;
@@ -69,13 +70,18 @@ final class SimpleBeanWriter {
 
   private void writeGenericTypeFields() {
     // collect all types to prevent duplicates
-    Set<UType> genericTypes = beanReader.allGenericTypes();
-    if (!genericTypes.isEmpty()) {
+    Set<UType> genericTypes =
+      beanReader.allGenericTypes().stream()
+        .map(Util::unwrapProvider)
+        .filter(UType::isGeneric)
+        .collect(toSet());
 
+    if (!genericTypes.isEmpty()) {
       final Map<String, String> seenShortNames = new HashMap<>();
       final Set<String> writtenFields = new HashSet<>();
 
-      for (final UType type : genericTypes) {
+      for (final UType utype : genericTypes) {
+        var type = Util.unwrapProvider(utype);
         final var fieldName = Util.shortName(type).replace(".", "_");
         final var components = type.componentTypes();
         if (components.size() == 1 && components.get(0).kind() == TypeKind.WILDCARD
@@ -86,7 +92,6 @@ final class SimpleBeanWriter {
 
         writer.append("  public static final Type TYPE_%s =", fieldName).eol()
           .append("      new GenericType<");
-
         writeGenericType(type, seenShortNames, writer);
         // use fully qualified types here rather than use type.writeShort(writer)
         writer.append(">(){}.type();").eol();
@@ -139,11 +144,10 @@ final class SimpleBeanWriter {
     method.buildConditional(writer);
     method.buildAddFor(writer);
     method.builderGetFactory(writer, beanReader.hasConditions());
-    method.startTry(writer);
-    if (method.isProtoType() || method.isUseProviderForSecondary()) {
+    if (method.isLazy() || method.isProtoType() || method.isUseProviderForSecondary()) {
       method.builderAddBeanProvider(writer);
-      method.endTry(writer);
     } else {
+      method.startTry(writer);
       method.builderBuildBean(writer);
       method.builderBuildAddBean(writer);
       method.endTry(writer);
@@ -170,9 +174,9 @@ final class SimpleBeanWriter {
   private void writeAddFor(MethodReader constructor) {
     beanReader.buildConditional(writer);
     beanReader.buildAddFor(writer);
-    if (beanReader.prototype()) {
+    if (beanReader.registerProvider()) {
       indent += "  ";
-      writer.append("      builder.asPrototype().registerProvider(() -> {", shortName, shortName).eol();
+      writer.append("      builder.%s(() -> {", beanReader.lazy() ? "registerLazy" : "asPrototype().registerProvider").eol();
     }
     constructor.startTry(writer);
     writeCreateBean(constructor);
@@ -181,7 +185,7 @@ final class SimpleBeanWriter {
     if (beanReader.isExtraInjectionRequired()) {
       writeExtraInjection();
     }
-    if (beanReader.prototype()) {
+    if (beanReader.registerProvider()) {
       beanReader.prototypePostConstruct(writer, indent);
       writer.indent("        return bean;").eol();
       writer.indent("      });").eol();
@@ -191,7 +195,7 @@ final class SimpleBeanWriter {
   }
 
   private void writeBuildMethodStart() {
-    if (beanReader.prototype()) {
+    if (beanReader.registerProvider()) {
       writer.append(CODE_COMMENT_BUILD_PROVIDER, shortName).eol();
     } else {
       writer.append(CODE_COMMENT_BUILD, shortName).eol();
@@ -208,20 +212,20 @@ final class SimpleBeanWriter {
   }
 
   private void writeExtraInjection() {
-    if (!beanReader.prototype()) {
+    if (!beanReader.registerProvider()) {
       writer.indent("      ").append("builder.addInjector(b -> {").eol();
       writer.indent("      ").append("  // field and method injection").eol();
     }
     injectFields();
     injectMethods();
-    if (!beanReader.prototype()) {
+    if (!beanReader.registerProvider()) {
       writer.indent("      });").eol();
     }
   }
 
   private void injectFields() {
-    String bean = beanReader.prototype() ? "bean" : "$bean";
-    String builder = beanReader.prototype() ? "builder" : "b";
+    String bean = beanReader.registerProvider() ? "bean" : "$bean";
+    String builder = beanReader.registerProvider() ? "builder" : "b";
     for (FieldReader fieldReader : beanReader.injectFields()) {
       String fieldName = fieldReader.fieldName();
       String getDependency = fieldReader.builderGetDependency(builder);
@@ -231,8 +235,8 @@ final class SimpleBeanWriter {
 
   private void injectMethods() {
     final var needsTry = beanReader.needsTryForMethodInjection();
-    final var bean = beanReader.prototype() ? "bean" : "$bean";
-    final var builder = beanReader.prototype() ? "builder" : "b";
+    final var bean = beanReader.registerProvider() ? "bean" : "$bean";
+    final var builder = beanReader.registerProvider() ? "builder" : "b";
     if (needsTry) {
       writer.indent("        try {").eol();
     }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
index 364bc6c8..775814af 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.generator;
 
+import static java.lang.annotation.ElementType.LOCAL_VARIABLE;
+
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Set;
@@ -19,7 +21,8 @@ final class TypeAppender {
     this.importTypes = importTypes;
   }
 
-  void add(UType type) {
+  void add(UType utype) {
+    var type = Util.unwrapProvider(utype);
     var components = type.componentTypes();
     if (isAddGenericType(type, components)) {
       addUType(type);
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 1e6d8363..cc02d3c1 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
@@ -118,11 +118,11 @@ final class TypeExtendsReader {
       return List.of();
     }
     if (baseTypeIsInterface) {
-      return List.of(Util.unwrapProvider(baseType.asType()));
+      return List.of(Util.unwrapProvider(baseUType));
     }
     var autoProvides = new ArrayList<>(interfaceTypes);
     autoProvides.addAll(extendsTypes);
-    autoProvides.add(Util.unwrapProvider(baseType.asType()));
+    autoProvides.add(Util.unwrapProvider(baseUType));
     return autoProvides;
   }
 
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 c94e48ea..721e78f5 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
@@ -91,6 +91,14 @@ final class Util {
     }
   }
 
+  static UType unwrapProvider(UType maybeProvider) {
+    if (isProvider(maybeProvider.mainType())) {
+      return maybeProvider.param0();
+    } else {
+      return maybeProvider;
+    }
+  }
+
   static String initLower(String name) {
     final StringBuilder sb = new StringBuilder(name.length());
     boolean upper = true;
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
index 9da596e4..eee6614b 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
@@ -1,31 +1,32 @@
+@GeneratePrism(AOPFallback.class)
+@GeneratePrism(Aspect.class)
+@GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism")
 @GeneratePrism(Assisted.class)
 @GeneratePrism(AssistFactory.class)
-@GeneratePrism(InjectModule.class)
-@GeneratePrism(Factory.class)
-@GeneratePrism(Singleton.class)
+@GeneratePrism(Bean.class)
 @GeneratePrism(Component.class)
 @GeneratePrism(Component.Import.class)
-@GeneratePrism(Prototype.class)
-@GeneratePrism(Scope.class)
-@GeneratePrism(Qualifier.class)
-@GeneratePrism(Named.class)
+@GeneratePrism(DependencyMeta.class)
+@GeneratePrism(Factory.class)
+@GeneratePrism(Generated.class)
 @GeneratePrism(Inject.class)
-@GeneratePrism(AOPFallback.class)
-@GeneratePrism(Aspect.class)
-@GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism")
+@GeneratePrism(InjectModule.class)
+@GeneratePrism(Lazy.class)
+@GeneratePrism(Named.class)
+@GeneratePrism(PreDestroy.class)
 @GeneratePrism(Primary.class)
-@GeneratePrism(Secondary.class)
+@GeneratePrism(Profile.class)
+@GeneratePrism(Prototype.class)
 @GeneratePrism(Proxy.class)
-@GeneratePrism(PreDestroy.class)
-@GeneratePrism(DependencyMeta.class)
-@GeneratePrism(Bean.class)
 @GeneratePrism(QualifiedMap.class)
-@GeneratePrism(Generated.class)
+@GeneratePrism(Qualifier.class)
 @GeneratePrism(RequiresBean.class)
 @GeneratePrism(RequiresProperty.class)
 @GeneratePrism(value = RequiresBean.Container.class, name = "RequiresBeanContainerPrism")
 @GeneratePrism(value = RequiresProperty.Container.class, name = "RequiresPropertyContainerPrism")
-@GeneratePrism(Profile.class)
+@GeneratePrism(Singleton.class)
+@GeneratePrism(Scope.class)
+@GeneratePrism(Secondary.class)
 package io.avaje.inject.generator;
 
 import io.avaje.inject.*;
diff --git a/inject-maven-plugin/pom.xml b/inject-maven-plugin/pom.xml
index 6e28f6af..a09f7a4a 100644
--- a/inject-maven-plugin/pom.xml
+++ b/inject-maven-plugin/pom.xml
@@ -22,7 +22,7 @@
     <dependency>
       <groupId>org.apache.maven.plugin-tools</groupId>
       <artifactId>maven-plugin-annotations</artifactId>
-      <version>3.11.0</version>
+      <version>3.12.0</version>
       <scope>provided</scope>
     </dependency>
 
@@ -38,7 +38,7 @@
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-plugin-plugin</artifactId>
-        <version>3.11.0</version>
+        <version>3.12.0</version>
         <configuration>
           <goalPrefix>provides</goalPrefix>
         </configuration>
diff --git a/inject/README.md b/inject/README.md
index 7872466a..eab0894f 100644
--- a/inject/README.md
+++ b/inject/README.md
@@ -25,7 +25,7 @@ module org.example {
 ## Differences to Dagger
 
 - Aimed specifically for server side development (rather than Andriod)
-- Supports lifecycle methods with `@PostConstruct` and `@PreDestory`
+- Supports lifecycle methods with `@PostConstruct` and `@PreDestroy`
 - Supports `@Factory` and `@Bean`
 - Provides API to obtain all bean instances that implement an interface
 - Provides API to obtain all bean instances that have an annotation
diff --git a/inject/pom.xml b/inject/pom.xml
index 55c6adfa..fe6ef5e7 100644
--- a/inject/pom.xml
+++ b/inject/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
 
   <artifactId>avaje-inject</artifactId>
@@ -33,14 +33,14 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-config</artifactId>
-      <version>3.11</version>
+      <version>3.13</version>
       <optional>true</optional>
     </dependency>
 
     <dependency>
       <groupId>org.mockito</groupId>
       <artifactId>mockito-core</artifactId>
-      <version>5.10.0</version>
+      <version>5.11.0</version>
       <optional>true</optional>
     </dependency>
 
@@ -49,7 +49,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>junit</artifactId>
-      <version>1.4</version>
+      <version>1.5</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/inject/src/main/java/io/avaje/inject/Lazy.java b/inject/src/main/java/io/avaje/inject/Lazy.java
new file mode 100644
index 00000000..b75140be
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/Lazy.java
@@ -0,0 +1,17 @@
+package io.avaje.inject;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Marks a Singleton, Component or Factory method beans to be initialised lazily.
+ * <p>
+ * When annotating a {@link Factory} as {@code @Lazy} it means that the factory
+ * itself is not lazy but all beans that it provides will have lazy initialisation.
+ */
+@Retention(RetentionPolicy.SOURCE)
+@Target({ElementType.METHOD, ElementType.TYPE})
+public @interface Lazy {
+}
diff --git a/inject/src/main/java/io/avaje/inject/aop/Fallback.java b/inject/src/main/java/io/avaje/inject/aop/Fallback.java
deleted file mode 100644
index 8609d164..00000000
--- a/inject/src/main/java/io/avaje/inject/aop/Fallback.java
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.avaje.inject.aop;
-
-import java.lang.reflect.Method;
-
-/**
- * @deprecated migrate to use {@link AOPFallback} to specify the fallback method.
- * <p>
- * A fallback or recovery method used with Aspects.
- * <p>
- * This isn't strictly required but more a helper to make it easier for aspects
- * that want to use a fallback or recovery method.
- */
-@Deprecated(forRemoval = true)
-@FunctionalInterface
-public interface Fallback {
-
-
-  /**
-   * Find and return the fallback given the name and original method.
-   * <p>
-   * This will first try and find the method for the name that has the same parameters as
-   * the method but additionally takes a {@code Throwable} as the last parameter. If no
-   * matching method is found it then looks for the method with the name that just has the
-   * matching parameters (and not the additional throwable).
-   *
-   * @param name   The name of the fallback method
-   * @param method The original method which we match to using argument types.
-   * @return The fallback
-   * @throws NoSuchMethodException When no matching fallback method is found
-   */
-  static Fallback find(String name, Method method) throws NoSuchMethodException {
-    return FallbackFinder.find(name, method);
-  }
-
-  /**
-   * Invoke the fallback method given the invocation and exception.
-   *
-   * @param call            The invocation being executed
-   * @param sourceException The exception thrown that triggers the use of the fallback method
-   */
-  Object invoke(Invocation call, Throwable sourceException);
-}
diff --git a/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java b/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java
deleted file mode 100644
index d120a75b..00000000
--- a/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java
+++ /dev/null
@@ -1,76 +0,0 @@
-package io.avaje.inject.aop;
-
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
-
-@Deprecated
-final class FallbackFinder {
-
-  static Fallback find(String name, Method method) throws NoSuchMethodException {
-    final Class<?> type = method.getDeclaringClass();
-    final Parameter[] parameters = method.getParameters();
-    try {
-      return new WithThrowable(type.getDeclaredMethod(name, paramTypesWithThrowable(parameters)));
-    } catch (NoSuchMethodException e) {
-      return new WithoutThrowable(type.getDeclaredMethod(name, paramTypes(parameters)));
-    }
-  }
-
-  private static Class<?>[] paramTypes(Parameter[] parameters) {
-    final Class<?>[] paramTypes = new Class[parameters.length];
-    for (int i = 0; i < parameters.length; i++) {
-      paramTypes[i] = parameters[i].getType();
-    }
-    return paramTypes;
-  }
-
-  private static Class<?>[] paramTypesWithThrowable(Parameter[] parameters) {
-    final Class<?>[] paramTypes = new Class[parameters.length + 1];
-    for (int i = 0; i < parameters.length; i++) {
-      paramTypes[i] = parameters[i].getType();
-    }
-    paramTypes[parameters.length] = Throwable.class;
-    return paramTypes;
-  }
-
-  static final class WithThrowable implements Fallback {
-
-    private final Method fallbackMethod;
-
-    WithThrowable(Method fallbackMethod) {
-      this.fallbackMethod = fallbackMethod;
-    }
-
-    @Override
-    public Object invoke(Invocation call, Throwable e) {
-       try {
-        final Object result = fallbackMethod.invoke(call.instance(), call.arguments(e));
-        call.result(result);
-        return result;
-      } catch (IllegalAccessException | InvocationTargetException ex) {
-        throw new InvocationException("Error invoking fallback method", ex);
-      }
-    }
-  }
-
-  static final class WithoutThrowable implements Fallback {
-
-    private final Method fallbackMethod;
-
-    WithoutThrowable(Method fallbackMethod) {
-      this.fallbackMethod = fallbackMethod;
-    }
-
-    @Override
-    public Object invoke(Invocation call, Throwable e) {
-      try {
-        final Object result = fallbackMethod.invoke(call.instance(), call.arguments());
-        call.result(result);
-        return result;
-      } catch (IllegalAccessException | InvocationTargetException ex) {
-        throw new InvocationException("Error invoking fallback method", ex);
-      }
-    }
-  }
-}
diff --git a/inject/src/main/java/io/avaje/inject/aop/Invocation.java b/inject/src/main/java/io/avaje/inject/aop/Invocation.java
index 2196badf..b46c5cc8 100644
--- a/inject/src/main/java/io/avaje/inject/aop/Invocation.java
+++ b/inject/src/main/java/io/avaje/inject/aop/Invocation.java
@@ -1,7 +1,6 @@
 package io.avaje.inject.aop;
 
 import java.lang.reflect.Method;
-import java.util.Arrays;
 import java.util.function.Consumer;
 import java.util.function.Function;
 
@@ -52,15 +51,6 @@ public interface Invocation {
    */
   Object[] arguments();
 
-  /**
-   * @deprecated migrate to using {@link AOPFallback} to specify fallback method.
-   * That then removes the need to use this method altogether.
-   * <p>
-   * Return the arguments additionally appending the throwable.
-   */
-  @Deprecated(forRemoval = true)
-  Object[] arguments(Throwable e);
-
   /**
    * Return the method being called for this invocation.
    */
@@ -126,17 +116,6 @@ public interface Invocation {
       return args;
     }
 
-    @Override
-    public Object[] arguments(Throwable e) {
-      if (args == null || args.length == 0) {
-        return new Object[]{e};
-      } else {
-        Object[] newArgs = Arrays.copyOf(args, args.length + 1);
-        newArgs[args.length] = e;
-        return newArgs;
-      }
-    }
-
     @Override
     public Method method() {
       return method;
diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java
index 87c17114..eb506576 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -76,6 +76,13 @@ public interface Builder {
    */
   <T> void registerProvider(Provider<T> provider);
 
+  /**
+   * Register a lazy provider into the context.
+   */
+  default <T> void registerLazy(Provider<T> original) {
+    registerProvider(new LazyProvider<>(original));
+  }
+
   /**
    * Register the bean instance into the context.
    *
@@ -276,5 +283,4 @@ public interface Builder {
    */
   BeanScope build(boolean withShutdownHook, long start);
 
-
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java b/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java
new file mode 100644
index 00000000..158f4665
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java
@@ -0,0 +1,35 @@
+package io.avaje.inject.spi;
+
+import jakarta.inject.Provider;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * Wrap a Provider for lazy initialisation.
+ */
+final class LazyProvider<T> implements Provider<T> {
+
+  private final ReentrantLock lock = new ReentrantLock();
+  private final Provider<T> original;
+  private T lazy;
+
+  LazyProvider(Provider<T> original) {
+    this.original = original;
+  }
+
+  @Override
+  public T get() {
+    if (lazy != null) {
+      return lazy;
+    }
+    lock.lock();
+    try {
+      if (lazy == null) {
+        lazy = original.get();
+      }
+      return lazy;
+    } finally {
+      lock.unlock();
+    }
+  }
+}
diff --git a/inject/src/main/java/io/avaje/inject/spi/ProviderPromise.java b/inject/src/main/java/io/avaje/inject/spi/ProviderPromise.java
index 3ae823bf..ae980fa0 100644
--- a/inject/src/main/java/io/avaje/inject/spi/ProviderPromise.java
+++ b/inject/src/main/java/io/avaje/inject/spi/ProviderPromise.java
@@ -28,6 +28,10 @@ final class ProviderPromise<T> implements Provider<T>, Consumer<Builder> {
 
   @Override
   public T get() {
+    if (provider == null) {
+      throw new IllegalStateException("Illegal to call Provider.get() method during DI wiring. " +
+        "Use a @PostConstruct method and call Provider.get() in there or perhaps use java.util.function.Supplier");
+    }
     return provider.get();
   }
 
diff --git a/pom.xml b/pom.xml
index adaf3bd4..32714726 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,7 +3,7 @@
   <modelVersion>4.0.0</modelVersion>
   <groupId>io.avaje</groupId>
   <artifactId>avaje-inject-parent</artifactId>
-  <version>9.12-SNAPSHOT</version>
+  <version>9.12-RC2</version>
   <packaging>pom</packaging>
   <name>avaje inject parent</name>
   <description>parent pom for avaje inject library</description>
@@ -11,7 +11,7 @@
   <parent>
     <groupId>org.avaje</groupId>
     <artifactId>java11-oss</artifactId>
-    <version>4.0</version>
+    <version>4.1</version>
   </parent>
   <scm>
     <developerConnection>scm:git:git@github.com:avaje/avaje-inject.git</developerConnection>


Test patch:
diff --git a/blackbox-test-inject/pom.xml b/blackbox-test-inject/pom.xml
index ecbc036c..69645c6b 100644
--- a/blackbox-test-inject/pom.xml
+++ b/blackbox-test-inject/pom.xml
@@ -5,7 +5,7 @@
   <parent>
     <artifactId>avaje-inject-parent</artifactId>
     <groupId>io.avaje</groupId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
@@ -39,7 +39,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-config</artifactId>
-      <version>3.11</version>
+      <version>3.13</version>
     </dependency>
 
     <dependency>
@@ -73,7 +73,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>junit</artifactId>
-      <version>1.4</version>
+      <version>1.5</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java b/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
index 0890a298..57631e30 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/config/AppConfig.java
@@ -48,6 +48,12 @@ public class AppConfig {
     return new Builder();
   }
 
+  @Prototype
+  @Bean
+  public BuilderThrows newBuilderThrows() throws Exception {
+    return new BuilderThrows();
+  }
+
   @Bean
   Generated newGenerated() {
     return new Generated();
@@ -59,6 +65,12 @@ public class AppConfig {
     return new MySecType();
   }
 
+  @Secondary
+  @Bean
+  public MySecTypeThrows generalSecondaryThrows() throws Exception {
+    return new MySecTypeThrows();
+  }
+
   @Secondary
   @Bean
   public Optional<MySecOptType> optionalSecondary() {
@@ -100,12 +112,18 @@ public class AppConfig {
   public static class Builder {
   }
 
+  public static class BuilderThrows {
+  }
+
   public static class Generated {
   }
 
   public static class MySecType {
   }
 
+  public static class MySecTypeThrows {
+  }
+
   public static class MySecOptType {
   }
 
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java
new file mode 100644
index 00000000..c6b4c816
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java
@@ -0,0 +1,26 @@
+package org.example.myapp.lazy;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.Lazy;
+import io.avaje.inject.PostConstruct;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+
+@Lazy
+@Singleton
+@Named("single")
+public class LazyBean {
+
+  @Inject @Nullable AtomicBoolean initialized;
+
+  @PostConstruct
+  void init(BeanScope scope) {
+
+    // note that nested test scopes will not be lazy
+    if (initialized != null) initialized.set(true);
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyFactory.java
new file mode 100644
index 00000000..986e46d9
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyFactory.java
@@ -0,0 +1,29 @@
+package org.example.myapp.lazy;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Lazy;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Named;
+
+@Lazy
+@Factory
+public class LazyFactory {
+
+  @Bean
+  @Named("factory")
+  LazyBean lazyInt(@Nullable AtomicBoolean initialized) {
+
+    // note that nested test scopes will not be lazy
+    if (initialized != null) initialized.set(true);
+    return new LazyBean();
+  }
+
+  @Bean
+  @Named("factoryThrows")
+  LazyBean lazyIntThrows(@Nullable AtomicBoolean initialized) throws Exception {
+    return lazyInt(initialized);
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupConsumer.java b/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupConsumer.java
new file mode 100644
index 00000000..97027015
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupConsumer.java
@@ -0,0 +1,26 @@
+package org.example.myapp.supplier;
+
+import io.avaje.inject.Component;
+
+import java.util.function.Supplier;
+
+@Component
+class MySupConsumer {
+
+  private final Supplier<MySupFactory.SupFoo> supplier;
+  private MySupFactory.SupFoo supFoo;
+
+  MySupConsumer(Supplier<MySupFactory.SupFoo> supplier) {
+    this.supplier = supplier;
+    supFoo = supplier.get();
+  }
+
+  MySupFactory.SupFoo current() {
+    return supFoo;
+  }
+
+  MySupFactory.SupFoo next() {
+    supFoo = supplier.get();
+    return supFoo;
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupFactory.java
new file mode 100644
index 00000000..18da3dab
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/supplier/MySupFactory.java
@@ -0,0 +1,31 @@
+package org.example.myapp.supplier;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.Supplier;
+
+@Factory
+public final class MySupFactory {
+
+  private final AtomicInteger counter = new AtomicInteger();
+
+  @Bean
+  Supplier<SupFoo> supplyIt() {
+    return () -> new SupFoo(counter.incrementAndGet());
+  }
+
+  public static class SupFoo {
+
+    private final int id;
+
+    SupFoo(int id) {
+      this.id = id;
+    }
+
+    public int id() {
+      return id;
+    }
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java
new file mode 100644
index 00000000..5095fe42
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java
@@ -0,0 +1,34 @@
+package org.example.myapp.lazy;
+
+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;
+
+class LazyTest {
+
+  @Test
+  void test() {
+    var initialized = new AtomicBoolean();
+    try (var scope = BeanScope.builder().beans(initialized).build()) {
+      assertThat(initialized).isFalse();
+      LazyBean prov = scope.get(LazyBean.class, "single");
+      assertThat(initialized).isTrue();
+      assertThat(prov).isNotNull();
+    }
+  }
+
+  @Test
+  void testFactory() {
+    var initialized = new AtomicBoolean();
+    try (var scope = BeanScope.builder().beans(initialized).build()) {
+      assertThat(initialized).isFalse();
+      LazyBean prov = scope.get(LazyBean.class, "factory");
+      assertThat(initialized).isTrue();
+      assertThat(prov).isNotNull();
+    }
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/supplier/MySupFactoryTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/supplier/MySupFactoryTest.java
new file mode 100644
index 00000000..6666a991
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/supplier/MySupFactoryTest.java
@@ -0,0 +1,31 @@
+package org.example.myapp.supplier;
+
+import io.avaje.inject.BeanScope;
+import org.example.external.aspect.sub.ExampleExternalAspectModule;
+import org.example.myapp.MyappModule;
+import org.junit.jupiter.api.Test;
+import org.other.one.OneModule;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class MySupFactoryTest {
+
+  @Test
+  void supplierOfFoo() {
+    try (var beanScope = BeanScope.builder()
+      .modules(new ExampleExternalAspectModule(), new OneModule(), new MyappModule())
+      .build()) {
+
+      MySupConsumer supConsumer = beanScope.get(MySupConsumer.class);
+      MySupFactory.SupFoo current = supConsumer.current();
+      assertThat(current.id()).isEqualTo(1);
+
+      MySupFactory.SupFoo next = supConsumer.next();
+      assertThat(next.id()).isEqualTo(2);
+
+      MySupFactory.SupFoo next2 = supConsumer.next();
+      assertThat(next2.id()).isEqualTo(3);
+    }
+
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBean.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBean.java
new file mode 100644
index 00000000..bae70111
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyBean.java
@@ -0,0 +1,12 @@
+package io.avaje.inject.generator.models.valid.lazy;
+
+import io.avaje.inject.Lazy;
+import jakarta.inject.Inject;
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+
+@Lazy
+@Singleton
+public class LazyBean {
+  @Inject Provider<Integer> intProvider;
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java
new file mode 100644
index 00000000..931e6723
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java
@@ -0,0 +1,15 @@
+package io.avaje.inject.generator.models.valid.lazy;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Lazy;
+
+@Lazy
+@Factory
+public class LazyFactory {
+
+  @Bean
+  Integer lazyInt() {
+    return 0;
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java
new file mode 100644
index 00000000..7d7c5bc2
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/FactoryProvider.java
@@ -0,0 +1,23 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+import java.util.function.Supplier;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Secondary;
+import jakarta.inject.Provider;
+
+@Factory
+public class FactoryProvider {
+
+  @Bean
+  @Secondary
+  Provider<Grinder> grinder() {
+    return Grinder::new;
+  }
+
+  @Bean
+  Provider<Supplier<String>> supply() {
+    return () -> () -> "";
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java
new file mode 100644
index 00000000..96b3a3dc
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/Grinder.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+public class Grinder {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java
new file mode 100644
index 00000000..2c46f571
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/provider/ProviderUser.java
@@ -0,0 +1,18 @@
+package io.avaje.inject.generator.models.valid.provider;
+
+import java.util.function.Supplier;
+
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ProviderUser {
+
+  Provider<Grinder> grinder;
+  Provider<Supplier<String>> supplier;
+
+  public ProviderUser(Provider<Grinder> grinder, Provider<Supplier<String>> supplier) {
+    this.grinder = grinder;
+    this.supplier = supplier;
+  }
+}
diff --git a/inject-test/pom.xml b/inject-test/pom.xml
index 3dfcb8ef..b12bad44 100644
--- a/inject-test/pom.xml
+++ b/inject-test/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>9.12-SNAPSHOT</version>
+    <version>9.12-RC2</version>
   </parent>
 
   <artifactId>avaje-inject-test</artifactId>
@@ -12,7 +12,7 @@
 
   <properties>
     <jupiter.version>5.10.2</jupiter.version>
-    <mockito.version>5.10.0</mockito.version>
+    <mockito.version>5.11.0</mockito.version>
   </properties>
 
   <dependencies>
@@ -45,12 +45,12 @@
     <dependency>
       <groupId>net.bytebuddy</groupId>
       <artifactId>byte-buddy</artifactId>
-      <version>1.14.12</version>
+      <version>1.14.13</version>
     </dependency>
     <dependency>
       <groupId>net.bytebuddy</groupId>
       <artifactId>byte-buddy-agent</artifactId>
-      <version>1.14.12</version>
+      <version>1.14.13</version>
     </dependency>
 
     <dependency>
@@ -105,7 +105,7 @@
     <dependency>
       <groupId>org.projectlombok</groupId>
       <artifactId>lombok</artifactId>
-      <version>1.18.30</version>
+      <version>1.18.32</version>
       <scope>test</scope>
     </dependency>
 
@@ -119,7 +119,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>junit</artifactId>
-      <version>1.4</version>
+      <version>1.5</version>
       <scope>test</scope>
     </dependency>
 
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
new file mode 100644
index 00000000..176d6da1
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
@@ -0,0 +1,25 @@
+package org.example.coffee.provider;
+
+import java.util.function.Supplier;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+import io.avaje.inject.Secondary;
+import jakarta.inject.Named;
+import jakarta.inject.Provider;
+
+@Factory
+public class FactoryProvider {
+
+  @Bean
+  @Secondary
+  @Named("second")
+  Provider<String> second() {
+    return () -> "Nah, I'd win";
+  }
+
+  @Bean
+  Provider<Supplier<String>> supply() {
+    return () -> () -> "Stand proud Provider, you were strong";
+  }
+}
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
new file mode 100644
index 00000000..b1868a14
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
@@ -0,0 +1,22 @@
+package org.example.coffee.provider;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import java.util.function.Supplier;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.BeanScope;
+import io.avaje.inject.spi.GenericType;
+
+class FactoryProviderTest {
+
+  @Test
+  void test() {
+    try (var scope = BeanScope.builder().build()) {
+      Supplier<String> prov = scope.get(new GenericType<Supplier<String>>() {});
+      assertThat(prov.get()).isEqualTo("Stand proud Provider, you were strong");
+      assertThat(scope.get(String.class, "second")).isEqualTo("Nah, I'd win");
+    }
+  }
+}