Repository: avaje/avaje-inject
Pull request #553 (2024-04-25 19:03:48)

Adds `@External` annotation
Adds a similar annotation to `@Nullable` such that compile time validations can be disabled for beans not managed by Inject, but will still fail at runtime if the bean is not present. An example use case is beans provided by Jooby

- Adds an `@External` marker annotation 
- updates generator to use this new annotation
- exempts Nullable/Optional beans from strict wiring calculus
- renames the processor

rbygrave: > renames the processor

Any reason for that?
SentryMan: > Any reason for that?

Aesthetics
SentryMan: hmm wait a sec
SentryMan: Okay, I'm good with this

Patch:
diff --git a/blackbox-other/src/main/java/org/other/one/package-info.java b/blackbox-other/src/main/java/org/other/one/package-info.java
index 84da2bd5..45a440c3 100644
--- a/blackbox-other/src/main/java/org/other/one/package-info.java
+++ b/blackbox-other/src/main/java/org/other/one/package-info.java
@@ -1,6 +1,1 @@
-@InjectModule(provides = {SomeOptionalDep.class, AtomicBoolean.class})
 package org.other.one;
-
-import java.util.concurrent.atomic.AtomicBoolean;
-
-import io.avaje.inject.InjectModule;
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 d2e19121..2dcd677c 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
@@ -116,7 +116,7 @@ final class ExternalProvider {
     if (!injectAvailable) {
       return;
     }
-    for (final Plugin plugin : ServiceLoader.load(Plugin.class, Processor.class.getClassLoader())) {
+    for (final Plugin plugin : ServiceLoader.load(Plugin.class, InjectProcessor.class.getClassLoader())) {
       var name = plugin.getClass().getTypeName();
       if (avajePlugins.containsKey(name)) {
         continue;
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/FactoryOrder.java b/inject-generator/src/main/java/io/avaje/inject/generator/FactoryOrder.java
index 0710e762..827b556e 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/FactoryOrder.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/FactoryOrder.java
@@ -109,7 +109,9 @@ class FactoryOrder {
 
   private boolean notProvided(String dependency) {
     final FactoryList factoryList = providesMap.get(dependency);
-    return (factoryList == null || !factoryList.allPushed()) && !pluginProvided.contains(dependency);
+    return (factoryList == null || !factoryList.allPushed())
+      && !pluginProvided.contains(dependency)
+      && !ProcessingContext.externallyProvided(dependency);
   }
 
   /**
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
similarity index 94%
rename from inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
rename to inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
index e0656853..744de519 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Processor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
@@ -29,21 +29,22 @@ import static io.avaje.inject.generator.ProcessingContext.*;
 @GenerateAPContext
 @GenerateModuleInfoReader
 @SupportedAnnotationTypes({
+  ApplicationEventPrism.PRISM_TYPE,
+  AspectImportPrism.PRISM_TYPE,
   AssistFactoryPrism.PRISM_TYPE,
-  InjectModulePrism.PRISM_TYPE,
-  FactoryPrism.PRISM_TYPE,
-  SingletonPrism.PRISM_TYPE,
   ComponentPrism.PRISM_TYPE,
-  PrototypePrism.PRISM_TYPE,
-  ScopePrism.PRISM_TYPE,
   Constants.TESTSCOPE,
   Constants.CONTROLLER,
+  ExternalPrism.PRISM_TYPE,
+  FactoryPrism.PRISM_TYPE,
   ImportPrism.PRISM_TYPE,
-  AspectImportPrism.PRISM_TYPE,
-  ApplicationEventPrism.PRISM_TYPE,
-  QualifierPrism.PRISM_TYPE
+  InjectModulePrism.PRISM_TYPE,
+  PrototypePrism.PRISM_TYPE,
+  QualifierPrism.PRISM_TYPE,
+  ScopePrism.PRISM_TYPE,
+  SingletonPrism.PRISM_TYPE,
 })
-public final class Processor extends AbstractProcessor {
+public final class InjectProcessor extends AbstractProcessor {
 
   private Elements elementUtils;
   private ScopeInfo defaultScope;
@@ -72,7 +73,9 @@ public final class Processor extends AbstractProcessor {
     pluginFileProvided.forEach(defaultScope::pluginProvided);
   }
 
-  /** Loads provider files generated by avaje-inject-maven-plugin */
+  /**
+   * Loads provider files generated by avaje-inject-maven-plugin
+   */
   void loadProvidedFiles() {
     this.performModuleValidation =
       lines("target/avaje-plugin-exists.txt").isEmpty()
@@ -83,13 +86,18 @@ public final class Processor extends AbstractProcessor {
     moduleFileProvided.addAll(lines("build/avaje-module-provides.txt"));
   }
 
-  /** Loads order files generated by avaje-inject-maven-plugin */
+  /**
+   * Loads order files generated by avaje-inject-maven-plugin
+   */
   private void loadOrderFiles() {
     Stream.concat(
         lines("target/avaje-module-dependencies.csv").stream().skip(1),
         lines("build/avaje-module-dependencies.csv").stream().skip(1))
+      .filter(s -> !s.startsWith("External Module Type"))
+      .distinct()
       .map(l -> l.split("\\|"))
       .map(AvajeModule::new)
+      .peek(m -> APContext.logNote(m.toString()))
       .forEach(ProcessingContext::addAvajeModule);
   }
 
@@ -142,6 +150,13 @@ public final class Processor extends AbstractProcessor {
       .flatMap(Set::stream)
       .forEach(EventPublisherWriter::new);
 
+    maybeElements(roundEnv, ExternalPrism.PRISM_TYPE).stream()
+      .flatMap(Set::stream)
+      .map(Element::asType)
+      .map(UType::parse)
+      .map(u -> "java.util.List".equals(u.mainType()) ? u.param0() : u)
+      .map(UType::fullWithoutAnnotations)
+      .forEach(ProcessingContext::addOptionalType);
     allScopes.readBeans(roundEnv);
     defaultScope.write(processingOver);
     allScopes.write(processingOver);
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 0d5087d3..76947300 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
@@ -8,6 +8,7 @@
 @GeneratePrism(Component.class)
 @GeneratePrism(Component.Import.class)
 @GeneratePrism(DependencyMeta.class)
+@GeneratePrism(External.class)
 @GeneratePrism(Factory.class)
 @GeneratePrism(Generated.class)
 @GeneratePrism(Inject.class)
diff --git a/inject-generator/src/main/java/module-info.java b/inject-generator/src/main/java/module-info.java
index ef1d9628..cbea9940 100644
--- a/inject-generator/src/main/java/module-info.java
+++ b/inject-generator/src/main/java/module-info.java
@@ -7,5 +7,5 @@ module io.avaje.inject.generator {
   uses io.avaje.inject.spi.Plugin;
   uses io.avaje.inject.spi.Module;
 
-  provides javax.annotation.processing.Processor with io.avaje.inject.generator.Processor;
+  provides javax.annotation.processing.Processor with io.avaje.inject.generator.InjectProcessor;
 }
diff --git a/inject-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors b/inject-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors
index 71ec82ce..5bdcf52a 100644
--- a/inject-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors
+++ b/inject-generator/src/main/resources/META-INF/gradle/incremental.annotation.processors
@@ -1,1 +1,1 @@
-io.avaje.inject.generator.Processor,aggregating
+io.avaje.inject.generator.InjectProcessor,aggregating
diff --git a/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java b/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
index 7af3294d..50e13493 100644
--- a/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
+++ b/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
@@ -93,7 +93,7 @@ public class AutoProvidesMojo extends AbstractMojo {
   }
 
   private FileWriter createFileWriter(String string) throws IOException {
-    return new FileWriter(new File(project.getBuild().getDirectory(), string));
+    return new FileWriter(new File(project.getBuild().getDirectory(), string), true);
   }
 
   private void writeProvidedPlugins(URLClassLoader newClassLoader, FileWriter pluginWriter) throws IOException {
@@ -160,7 +160,7 @@ public class AutoProvidesMojo extends AbstractMojo {
   }
 
   private void writeModuleCSV(FileWriter moduleWriter) throws IOException {
-    moduleWriter.write("External Module Type|Provides|Requires");
+    moduleWriter.write("\nExternal Module Type|Provides|Requires");
     for (AvajeModule avajeModule : modules) {
       moduleWriter.write("\n");
       moduleWriter.write(avajeModule.name());
diff --git a/inject/pom.xml b/inject/pom.xml
index cc88f527..341733e4 100644
--- a/inject/pom.xml
+++ b/inject/pom.xml
@@ -33,7 +33,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-config</artifactId>
-      <version>3.13</version>
+      <version>3.14</version>
       <optional>true</optional>
     </dependency>
 
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 16b4a49c..3a63e297 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -285,6 +285,7 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
     final Builder builder = Builder.newBuilder(profiles, propertyRequiresPlugin, suppliedBeans, enrichBeans, parent, parentOverride);
     for (final Module factory : factoryOrder.factories()) {
+      builder.currentModule(factory.getClass());
       factory.build(builder);
     }
 
diff --git a/inject/src/main/java/io/avaje/inject/External.java b/inject/src/main/java/io/avaje/inject/External.java
new file mode 100644
index 00000000..4eed781b
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/External.java
@@ -0,0 +1,22 @@
+package io.avaje.inject;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.PARAMETER;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Documented;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+import java.lang.reflect.Type;
+
+/**
+ * Marks this dependency as an external bean not managed by avaje inject.
+ * Compile-time validation will be disabled for this type.
+ * <p>
+ * The external dependency is expected to be provided by
+ * {@link BeanScopeBuilder#bean(String, Type, Object)}.
+ */
+@Documented
+@Retention(RUNTIME)
+@Target({FIELD, PARAMETER})
+public @interface External {}
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 489574fb..6b4d9ed7 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -279,4 +279,8 @@ public interface Builder {
    */
   BeanScope build(boolean withShutdownHook, long start);
 
+  /**
+   * Set the current module being wired.
+   */
+  void currentModule(Class<? extends Module> currentModule);
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
index 65ea0b05..49d83ddb 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -24,10 +24,15 @@ final class DBeanMap {
   private final Set<String> qualifiers = new HashSet<>();
 
   private NextBean nextBean;
+  private Class<? extends Module> currentModule;
 
   DBeanMap() {
   }
 
+  void currentModule(Class<? extends Module> currentModule) {
+    this.currentModule = currentModule;
+  }
+
   @Override
   public String toString() {
     return "BeanMap{" + beans + '}';
@@ -69,7 +74,7 @@ final class DBeanMap {
     }
     var name = nextBean.name;
     qualifiers.add(name);
-    DContextEntryBean entryBean = DContextEntryBean.of(bean, name, nextBean.priority);
+    var entryBean = DContextEntryBean.of(bean, name, nextBean.priority, currentModule);
     for (Type type : nextBean.types) {
       beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean);
     }
@@ -77,7 +82,7 @@ final class DBeanMap {
 
   void register(Provider<?> provider) {
     qualifiers.add(nextBean.name);
-    DContextEntryBean entryBean = DContextEntryBean.provider(nextBean.prototype, provider, nextBean.name, nextBean.priority);
+    var entryBean = DContextEntryBean.provider(nextBean.prototype, provider, nextBean.name, nextBean.priority, currentModule);
     for (Type type : nextBean.types) {
       beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean);
     }
@@ -112,7 +117,7 @@ final class DBeanMap {
     if (entry == null) {
       return null;
     }
-    return (T) entry.get(name);
+    return (T) entry.get(name, currentModule);
   }
 
   @SuppressWarnings("unchecked")
@@ -121,7 +126,7 @@ final class DBeanMap {
     if (entry == null) {
       return null;
     }
-    return (Provider<T>) entry.provider(name);
+    return (Provider<T>) entry.provider(name, currentModule);
   }
 
   /**
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
index e5f6a6c1..372d21f8 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -45,6 +45,11 @@ class DBuilder implements Builder {
     this.profiles = profiles;
   }
 
+  @Override
+  public void currentModule(Class<? extends Module> currentModule) {
+    beanMap.currentModule(currentModule);
+  }
+
   @Override
   public final boolean isAddBeanFor(Type... types) {
     return isAddBeanFor(null, types);
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
index 7025e393..1dbb2bd9 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntry.java
@@ -29,11 +29,11 @@ final class DContextEntry {
     entries.add(entryBean);
   }
 
-  Provider<?> provider(String name) {
+  Provider<?> provider(String name, Class<? extends Module> currentModule) {
     if (entries.size() == 1) {
       return entries.get(0).provider();
     }
-    return new EntryMatcher(name).provider(entries);
+    return new EntryMatcher(name, currentModule).provider(entries);
   }
 
   /**
@@ -43,14 +43,14 @@ final class DContextEntry {
     if (entries.size() == 1) {
       return entries.get(0).beanIfNameMatch(name);
     }
-    return new EntryMatcher(name).match(entries);
+    return new EntryMatcher(name, null).match(entries);
   }
 
-  Object get(String name) {
+  Object get(String name, Class<? extends Module> currentModule) {
     if (entries.size() == 1) {
       return entries.get(0).bean();
     }
-    return new EntryMatcher(name).match(entries);
+    return new EntryMatcher(name, currentModule).match(entries);
   }
 
   /**
@@ -96,10 +96,12 @@ final class DContextEntry {
 
     private final String name;
     private final boolean impliedName;
+    private final Class<? extends Module> currentModule;
     private DContextEntryBean match;
     private DContextEntryBean ignoredSecondaryMatch;
 
-    EntryMatcher(String name) {
+    EntryMatcher(String name, Class<? extends Module> currentModule) {
+      this.currentModule = currentModule;
       if (name != null && name.startsWith("!")) {
         this.name = name.substring(1);
         this.impliedName = true;
@@ -178,6 +180,15 @@ final class DContextEntry {
         match = entry;
         return;
       }
+      // if all else fails use the one provided by the current module
+      if (entry.sourceModule() != currentModule) {
+        ignoredSecondaryMatch = entry;
+        return;
+      } else if (entry.sourceModule() == currentModule && match.sourceModule() != currentModule) {
+        // match on module
+        match = entry;
+        return;
+      }
       throw new IllegalStateException("Expecting only 1 bean match but have multiple matching beans " + match.bean()
         + " and " + entry.bean() + ". Maybe need a rebuild is required after adding a @Named qualifier?");
     }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
index 17215478..41fd5059 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -3,7 +3,6 @@ package io.avaje.inject.spi;
 import io.avaje.inject.BeanEntry;
 
 import jakarta.inject.Provider;
-import java.util.Objects;
 import java.util.concurrent.locks.ReentrantLock;
 
 /**
@@ -14,11 +13,11 @@ class DContextEntryBean {
   /**
    * Create taking into account if it is a Provider or the bean itself.
    */
-  static DContextEntryBean of(Object source, String name, int flag) {
+  static DContextEntryBean of(Object source, String name, int flag, Class<? extends Module> currentModule) {
     if (source instanceof Provider) {
-      return new ProtoProvider((Provider<?>)source, name, flag);
+      return new ProtoProvider((Provider<?>)source, name, flag, currentModule);
     } else {
-      return new DContextEntryBean(source, name, flag);
+      return new DContextEntryBean(source, name, flag, currentModule);
     }
   }
 
@@ -27,24 +26,26 @@ class DContextEntryBean {
    */
   static DContextEntryBean supplied(Object source, String name, int flag) {
     if (source instanceof Provider) {
-      return new OnceProvider((Provider<?>)source, name, flag);
+      return new OnceProvider((Provider<?>)source, name, flag, null);
     } else {
-      return new DContextEntryBean(source, name, flag);
+      return new DContextEntryBean(source, name, flag, null);
     }
   }
 
-  static DContextEntryBean provider(boolean prototype, Provider<?> provider, String name, int flag) {
-    return prototype ? new ProtoProvider(provider, name, flag) : new OnceProvider(provider, name, flag);
+  static DContextEntryBean provider(boolean prototype, Provider<?> provider, String name, int flag, Class<? extends Module> currentModule) {
+    return prototype ? new ProtoProvider(provider, name, flag, currentModule) : new OnceProvider(provider, name, flag, currentModule);
   }
 
   protected final Object source;
   protected final String name;
+  protected final Class<? extends Module> sourceModule;
   private final int flag;
 
-  private DContextEntryBean(Object source, String name, int flag) {
+  private DContextEntryBean(Object source, String name, int flag, Class<? extends Module> currentModule) {
     this.source = source;
     this.name = name;
     this.flag = flag;
+    this.sourceModule = currentModule;
   }
 
   @Override
@@ -53,6 +54,7 @@ class DContextEntryBean {
       "source=" + source +
       ", name='" + name + '\'' +
       ", flag=" + flag +
+      ", sourceModule=" + sourceModule +
       '}';
   }
 
@@ -74,6 +76,10 @@ class DContextEntryBean {
     return qualifierName == null ? name == null : qualifierName.equalsIgnoreCase(name);
   }
 
+  final Class<? extends Module> sourceModule() {
+    return sourceModule;
+  }
+
   /**
    * Return the bean if name matches and otherwise null.
    */
@@ -116,8 +122,8 @@ class DContextEntryBean {
 
     private final Provider<?> provider;
 
-    private ProtoProvider(Provider<?> provider, String name, int flag) {
-      super(provider, name, flag);
+    private ProtoProvider(Provider<?> provider, String name, int flag, Class<? extends Module> currentModule) {
+      super(provider, name, flag, currentModule);
       this.provider = provider;
     }
 
@@ -141,8 +147,8 @@ class DContextEntryBean {
     private final Provider<?> provider;
     private Object bean;
 
-    private OnceProvider(Provider<?> provider, String name, int flag) {
-      super(provider, name, flag);
+    private OnceProvider(Provider<?> provider, String name, int flag, Class<? extends Module> currentModule) {
+      super(provider, name, flag, currentModule);
       this.provider = provider;
     }
 


Test patch:
diff --git a/blackbox-test-inject/pom.xml b/blackbox-test-inject/pom.xml
index 77f21e2f..bff911c9 100644
--- a/blackbox-test-inject/pom.xml
+++ b/blackbox-test-inject/pom.xml
@@ -39,7 +39,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-config</artifactId>
-      <version>3.13</version>
+      <version>3.14</version>
     </dependency>
 
     <dependency>
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
index 585a4b07..7602e6eb 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
@@ -36,7 +36,6 @@ class InjectProcessorTest {
           .forEach(File::delete);
       Paths.get("io.avaje.inject.spi.Module").toAbsolutePath().toFile().delete();
       Paths.get("io.avaje.inject.spi.ModuleOrdering").toAbsolutePath().toFile().delete();
-
     } catch (final Exception e) {
     }
   }
@@ -66,7 +65,7 @@ class InjectProcessorTest {
             List.of("--release=" + Integer.getInteger("java.specification.version")),
             null,
             files);
-    task.setProcessors(Arrays.asList(new Processor()));
+    task.setProcessors(Arrays.asList(new InjectProcessor()));
 
     assertThat(task.call()).isTrue();
   }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java
index fd9273e6..2a596308 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java
@@ -2,12 +2,12 @@ package io.avaje.inject.generator.models.valid;
 
 import java.lang.annotation.Annotation;
 
-import io.avaje.lang.Nullable;
+import io.avaje.inject.External;
 import jakarta.inject.Inject;
 import jakarta.inject.Singleton;
 
 @Singleton
 public class LangInject {
 
-  @Inject @Nullable Annotation classLoadingMXBean;
+  @Inject @External Annotation classLoadingMXBean;
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/external/ExternalDeps.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/external/ExternalDeps.java
new file mode 100644
index 00000000..daa1e556
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/external/ExternalDeps.java
@@ -0,0 +1,14 @@
+package io.avaje.inject.generator.models.valid.external;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import io.avaje.inject.External;
+import io.avaje.lang.Nullable;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ExternalDeps {
+
+  public ExternalDeps(@External AtomicBoolean bool, @Nullable AtomicInteger inty) {}
+}
diff --git a/inject/src/test/java/io/avaje/inject/spi/DContextEntryTest.java b/inject/src/test/java/io/avaje/inject/spi/DContextEntryTest.java
index e7388c52..03e61d80 100644
--- a/inject/src/test/java/io/avaje/inject/spi/DContextEntryTest.java
+++ b/inject/src/test/java/io/avaje/inject/spi/DContextEntryTest.java
@@ -3,8 +3,7 @@ package io.avaje.inject.spi;
 import io.avaje.inject.BeanEntry;
 import org.junit.jupiter.api.Test;
 
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.*;
 
 @SuppressWarnings("all")
 class DContextEntryTest {
@@ -13,22 +12,22 @@ class DContextEntryTest {
   void get_when_onePrimary() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("P", null, BeanEntry.PRIMARY));
-    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL));
-    entry.add(DContextEntryBean.of("S", null, BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("P", null, BeanEntry.PRIMARY, null));
+    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("S", null, BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get(null), "P");
+    assertEquals(entry.get(null, null), "P");
   }
 
   @Test
   void get_when_twoPrimary() {
     assertThrows(IllegalStateException.class, () -> {
       DContextEntry entry = new DContextEntry();
-      entry.add(DContextEntryBean.of("P", null, BeanEntry.PRIMARY));
-      entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL));
-      entry.add(DContextEntryBean.of("S", null, BeanEntry.PRIMARY));
+      entry.add(DContextEntryBean.of("P", null, BeanEntry.PRIMARY, null));
+      entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL, null));
+      entry.add(DContextEntryBean.of("S", null, BeanEntry.PRIMARY, null));
 
-      entry.get(null);
+      entry.get(null, null);
     });
   }
 
@@ -36,10 +35,10 @@ class DContextEntryTest {
   void get_when_oneNormal() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL));
-    entry.add(DContextEntryBean.of("S", null, BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("S", null, BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get(null), "N");
+    assertEquals(entry.get(null, null), "N");
   }
 
 
@@ -47,11 +46,11 @@ class DContextEntryTest {
   void get_when_oneNormal2() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL));
-    entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY));
-    entry.add(DContextEntryBean.of("S2", null, BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("N", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY, null));
+    entry.add(DContextEntryBean.of("S2", null, BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get(null), "N");
+    assertEquals(entry.get(null, null), "N");
   }
 
   @Test
@@ -59,10 +58,10 @@ class DContextEntryTest {
 
     assertThrows(IllegalStateException.class, () -> {
       DContextEntry entry = new DContextEntry();
-      entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY));
-      entry.add(DContextEntryBean.of("S2", null, BeanEntry.SECONDARY));
+      entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY, null));
+      entry.add(DContextEntryBean.of("S2", null, BeanEntry.SECONDARY, null));
 
-      entry.get(null);
+      entry.get(null, null);
     });
   }
 
@@ -70,29 +69,121 @@ class DContextEntryTest {
   void get_when_multiSecondary_butNamed() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("S1", "a", BeanEntry.SECONDARY));
-    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("S1", "a", BeanEntry.SECONDARY, null));
+    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get("b"), "S2");
+    assertEquals(entry.get("b", null), "S2");
   }
 
   @Test
   void get_when_multiSecondary_butNamed2() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY));
-    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("S1", null, BeanEntry.SECONDARY, null));
+    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get("b"), "S2");
+    assertEquals(entry.get("b", null), "S2");
   }
 
   @Test
   void get_when_secondary_butNamed() {
 
     DContextEntry entry = new DContextEntry();
-    entry.add(DContextEntryBean.of("S1", null, BeanEntry.PRIMARY));
-    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY));
+    entry.add(DContextEntryBean.of("S1", null, BeanEntry.PRIMARY, null));
+    entry.add(DContextEntryBean.of("S2", "b", BeanEntry.SECONDARY, null));
 
-    assertEquals(entry.get("b"), "S2");
+    assertEquals(entry.get("b", null), "S2");
+  }
+
+  @Test
+  void getWithName_when_currentModule() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", "same", BeanEntry.NORMAL, Module.class));
+    entry.add(DContextEntryBean.of("N2", "same", BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("N3", "same", BeanEntry.NORMAL, OtherModule.class));
+
+    assertEquals(entry.get("same", null), "N2");
+    assertEquals(entry.get("same", Module.class), "N1");
+    assertEquals(entry.get("same", OtherModule.class), "N3");
+
+    assertEquals(entry.get(null, null), "N2");
+    assertEquals(entry.get(null, Module.class), "N1");
+    assertEquals(entry.get(null, OtherModule.class), "N3");
+  }
+
+  @Test
+  void getWithoutName_when_currentModule() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", null, BeanEntry.NORMAL, Module.class));
+    entry.add(DContextEntryBean.of("N2", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("N3", null, BeanEntry.NORMAL, OtherModule.class));
+
+    assertEquals(entry.get(null, null), "N2");
+    assertEquals(entry.get(null, Module.class), "N1");
+    assertEquals(entry.get(null, OtherModule.class), "N3");
+
+    assertNull(entry.get("notThere", null));
+    assertNull(entry.get("notThere", Module.class));
+    assertNull(entry.get("notThere", OtherModule.class));
+  }
+
+  @Test
+  void getWithName_expect_matchOnName() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", "same", BeanEntry.NORMAL, Module.class));
+    entry.add(DContextEntryBean.of("N2", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("N3", null, BeanEntry.NORMAL, OtherModule.class));
+
+    assertEquals(entry.get("same", null), "N1");
+    assertEquals(entry.get("same", Module.class), "N1");
+    assertEquals(entry.get("same", OtherModule.class), "N1");
+  }
+
+  @Test
+  void getWithoutName_expect_matchOnNoName() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", null, BeanEntry.NORMAL, Module.class));
+    entry.add(DContextEntryBean.of("N2", "same", BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("N3", "same", BeanEntry.NORMAL, OtherModule.class));
+
+    assertEquals(entry.get(null, null), "N1");
+    assertEquals(entry.get(null, Module.class), "N1");
+    assertEquals(entry.get(null, OtherModule.class), "N1");
+  }
+
+  @Test
+  void when_noQualifier_expect_matchedToNoQualider() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", null, BeanEntry.NORMAL, Module.class));
+    entry.add(DContextEntryBean.of("N2", "myName", BeanEntry.NORMAL, Module.class));
+
+    assertEquals(entry.get("myName", Module.class), "N2");
+    assertEquals(entry.get(null, Module.class), "N1");
+  }
+
+  @Test
+  void when_noQualifier_expect_matchedToNoQualider2() {
+
+    DContextEntry entry = new DContextEntry();
+    entry.add(DContextEntryBean.of("N1", null, BeanEntry.NORMAL, null));
+    entry.add(DContextEntryBean.of("N2", "myName", BeanEntry.NORMAL, null));
+
+    assertEquals(entry.get("myName", Module.class), "N2");
+    assertEquals(entry.get(null, Module.class), "N1");
+  }
+
+  class OtherModule implements Module {
+    @Override
+    public Class<?>[] classes() {
+      return new Class[0];
+    }
+    @Override
+    public void build(Builder builder) {
+    }
   }
 }