Repository: avaje/avaje-inject
Pull request #557 (2024-05-02 08:55:50)

Prefer intra-module beans when resolving type (v2)
Based on #549 but move current module into DBeanMap


Patch:
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/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/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) {
+    }
   }
 }