Repository: avaje/avaje-inject
Pull request #248 (2023-01-03 04:01:58)

#243 - Fix for ClassCastException with generic types
The core fix is with DBeanMap. In detail, when wiring mocks/spy this part of DBeanMap additionally registers the mock with the generic type (which is in the generated code).

Prior to this fix, this process was too loose and registered the mock with other types like Model2 in the test case.


Patch:
diff --git a/README.md b/README.md
index 424e877e..c9fd3071 100644
--- a/README.md
+++ b/README.md
@@ -3,80 +3,11 @@
 [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://github.com/avaje/avaje-inject/blob/master/LICENSE)
 [![Maven Central : avaje-inject](https://maven-badges.herokuapp.com/maven-central/io.avaje/avaje-inject/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.avaje/avaje-inject)
 
-# [Avaje-Inject](https://avaje.io/inject)
+# [avaje-inject](https://avaje.io/inject)
 APT based dependency injection for server side developers - https://avaje.io/inject
 
-## Quick Start
-#### 1. Add avaje-inject as a dependency.
-```xml
-<dependency>
-  <groupId>io.avaje</groupId>
-  <artifactId>avaje-inject</artifactId>
-  <version>${avaje.inject.version}</version>
-</dependency>
-```
-#### 2. Add avaje-inject-generator annotation processor as a dependency with provided scope.
-```xml
-<dependency>
-  <groupId>io.avaje</groupId>
-  <artifactId>avaje-inject-generator</artifactId>
-  <version>${avaje.inject.version}</version>
-  <scope>provided</scope>
-</dependency>
-```
-If there are other annotation processors and they are specified via `maven-compiler-plugin`, then we add avaje-inject-generator there instead.
-```xml
-<plugin>
-  <groupId>org.apache.maven.plugins</groupId>
-  <artifactId>maven-compiler-plugin</artifactId>
-  <configuration>
-    <annotationProcessorPaths> <!-- All annotation processors specified here -->
-      <path>
-          <groupId>io.avaje</groupId>
-          <artifactId>avaje-inject-generator</artifactId>
-          <version>${avaje.inject.version}</version>
-      </path>
-      <path>
-          ... other annotation processor ...
-      </path>
-    </annotationProcessorPaths>
-  </configuration>
-</plugin>
-```
-#### 3. Create a Bean Class annotated with @Singleton
-```java
-@Singleton
-public class Example {
-
- private DependencyClass d1;
- private DependencyClass2 d2;
-  
-  // Dependencies must be annotated with singleton,
-  // or else be provided from another class annotated with @Factory
-  public Example(DependencyClass d1, DependencyClass2 d2) {
-    this.d1 = d1;
-    this.d2 = d2;
-  }
-}
-```
-Example factory class:
-```java
-@Factory
-public class ExampleFactory {
-  @Bean
-  public DependencyClass2() {
-    return new DependencyClass2();
-  }
-}
-```
-
-#### 4. Use BeanScope to wire and retrieve the beans and use however you wish.
-```java
-BeanScope beanScope = BeanScope.builder().build()
-Example ex = beanScope.get(Example.class);
-```
-
 ### Example module use
+
 ```java
 module org.example {
 
@@ -91,7 +22,7 @@ module org.example {
 - Uses Java annotation processing for dependency injection
 - Generates source code
 - Avoids any use of reflection or classpath scanning (so low overhead and fast startup)
-- A `Library only` (a DI library and that's it. ~25k in size)
+- A `Library only` (a DI library and that's it ~25k in size)
 
 
 ## Differences to Dagger
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index 75fb4f8f..7a0d9a23 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>8.10</version>
+    <version>8.11-SNAPSHOT</version>
   </parent>
 
   <artifactId>avaje-inject-generator</artifactId>
@@ -16,7 +16,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-inject</artifactId>
-      <version>8.10</version>
+      <version>8.11-SNAPSHOT</version>
     </dependency>
 
     <!-- test dependencies -->
diff --git a/inject/pom.xml b/inject/pom.xml
index 17aabee0..97c53248 100644
--- a/inject/pom.xml
+++ b/inject/pom.xml
@@ -4,7 +4,7 @@
   <parent>
     <groupId>io.avaje</groupId>
     <artifactId>avaje-inject-parent</artifactId>
-    <version>8.10</version>
+    <version>8.11-SNAPSHOT</version>
   </parent>
 
   <artifactId>avaje-inject</artifactId>
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 e7490d43..607b88bb 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -3,6 +3,7 @@ package io.avaje.inject.spi;
 import io.avaje.inject.BeanScope;
 import jakarta.inject.Provider;
 
+import java.lang.reflect.ParameterizedType;
 import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.LinkedHashMap;
@@ -145,8 +146,10 @@ class DBeanMap {
         DContextEntry entry = beans.get(type.getTypeName());
         if (entry != null) {
           DContextEntryBean suppliedBean = entry.supplied(qualifierName);
-          if (suppliedBean != null && types.length > 1) {
-            addSuppliedFor(type, types, suppliedBean);
+          if (suppliedBean != null) {
+            if (types.length > 1) {
+              addSuppliedFor(type, types, suppliedBean);
+            }
             return true;
           }
         }
@@ -156,12 +159,12 @@ class DBeanMap {
   }
 
   /**
-   * Register the suppliedBean entry with other types (like other interfaces)
-   * IF those types don't already have a registered entry.
+   * Register the suppliedBean entry with parameterized types IF those types
+   * don't already have a registered entry.
    */
   private void addSuppliedFor(Type matchType, Type[] types, DContextEntryBean suppliedBean) {
     for (Type type : types) {
-      if (type != matchType) {
+      if (type != matchType && type instanceof ParameterizedType) {
         beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(suppliedBean);
       }
     }
diff --git a/pom.xml b/pom.xml
index c6b44389..e78a796a 100644
--- a/pom.xml
+++ b/pom.xml
@@ -9,7 +9,7 @@
 
   <groupId>io.avaje</groupId>
   <artifactId>avaje-inject-parent</artifactId>
-  <version>8.10</version>
+  <version>8.11-SNAPSHOT</version>
   <packaging>pom</packaging>
   <name>avaje inject parent</name>
   <description>parent pom for avaje inject library</description>


Test patch:
diff --git a/blackbox-test-inject/pom.xml b/blackbox-test-inject/pom.xml
index 372e1231..c4f56e61 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>8.10</version>
+    <version>8.11-SNAPSHOT</version>
   </parent>
   <modelVersion>4.0.0</modelVersion>
 
diff --git a/inject-test/pom.xml b/inject-test/pom.xml
index 5a7466d0..2ceb1a2f 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>8.10</version>
+    <version>8.11-SNAPSHOT</version>
   </parent>
 
   <artifactId>avaje-inject-test</artifactId>
@@ -124,7 +124,7 @@
             <path>
               <groupId>io.avaje</groupId>
               <artifactId>avaje-inject-generator</artifactId>
-              <version>8.10</version>
+              <version>8.11-SNAPSHOT</version>
             </path>
           </annotationProcessorPaths>
         </configuration>
diff --git a/inject-test/src/test/java/org/example/circular/CupholderTest.java b/inject-test/src/test/java/org/example/circular/CupholderTest.java
index 8984ef0c..cdfe49de 100644
--- a/inject-test/src/test/java/org/example/circular/CupholderTest.java
+++ b/inject-test/src/test/java/org/example/circular/CupholderTest.java
@@ -1,12 +1,29 @@
 package org.example.circular;
 
+import io.avaje.inject.BeanScope;
 import io.avaje.inject.xtra.ApplicationScope;
 import org.junit.jupiter.api.Test;
 
+import java.util.List;
+
 import static org.assertj.core.api.Assertions.assertThat;
 
 public class CupholderTest {
 
+  @Test
+  void mockOne() {
+    try (BeanScope beanScope = BeanScope.builder().forTesting()
+      .mock(Cupholder.class)
+      .build()) {
+
+      Cupholder cupholder = beanScope.get(Cupholder.class);
+      assertThat(cupholder.hello()).isNull();
+
+      List<Cupholder> all = beanScope.list(Cupholder.class);
+      assertThat(all).hasSize(1);
+    }
+  }
+
   @Test
   void circularDependency_via_providerInterface() {
 
diff --git a/inject-test/src/test/java/org/example/generic/repo/AbstractRepo.java b/inject-test/src/test/java/org/example/generic/repo/AbstractRepo.java
new file mode 100644
index 00000000..e23c1f0d
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/AbstractRepo.java
@@ -0,0 +1,6 @@
+package org.example.generic.repo;
+
+public abstract class AbstractRepo<T> {
+
+  public abstract T get();
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/GenericWithMockTest.java b/inject-test/src/test/java/org/example/generic/repo/GenericWithMockTest.java
new file mode 100644
index 00000000..c97ed823
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/GenericWithMockTest.java
@@ -0,0 +1,35 @@
+package org.example.generic.repo;
+
+import io.avaje.inject.BeanScope;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class GenericWithMockTest {
+
+  @Test
+  void withMock() {
+    try (BeanScope beanScope = BeanScope.builder().forTesting()
+      .mock(MapRepo1.class)
+      .build()) {
+
+      ServiceClass serviceClass = beanScope.get(ServiceClass.class);
+      MapRepo1 mapRepo1 = beanScope.get(MapRepo1.class);
+      assertThat(mapRepo1.get()).isNull();
+
+      MapRepo2 mapRepo2 = beanScope.get(MapRepo2.class);
+      assertThat(mapRepo2.get()).isNotNull();
+      assertThat(serviceClass.map2.get()).isNotNull();
+    }
+  }
+
+  @Test
+  void wireNoMocks() {
+    try (BeanScope beanScope = BeanScope.builder().build()) {
+      var serviceClass = beanScope.get(ServiceClass.class);
+      String value = serviceClass.process();
+
+      assertThat(value).isEqualTo("{=Model1}|{=Model2}");
+    }
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/MapRepo1.java b/inject-test/src/test/java/org/example/generic/repo/MapRepo1.java
new file mode 100644
index 00000000..6476b477
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/MapRepo1.java
@@ -0,0 +1,12 @@
+package org.example.generic.repo;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class MapRepo1 extends AbstractRepo<Model1> {
+
+  @Override
+  public Model1 get() {
+    return new Model1();
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/MapRepo2.java b/inject-test/src/test/java/org/example/generic/repo/MapRepo2.java
new file mode 100644
index 00000000..1268bb4c
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/MapRepo2.java
@@ -0,0 +1,12 @@
+package org.example.generic.repo;
+
+import jakarta.inject.Singleton;
+
+@Singleton
+public class MapRepo2 extends AbstractRepo<Model2> {
+
+  @Override
+  public Model2 get() {
+    return new Model2();
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/MapService.java b/inject-test/src/test/java/org/example/generic/repo/MapService.java
new file mode 100644
index 00000000..d80686fd
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/MapService.java
@@ -0,0 +1,6 @@
+package org.example.generic.repo;
+
+public interface MapService<T> {
+
+  T get();
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/MapService1.java b/inject-test/src/test/java/org/example/generic/repo/MapService1.java
new file mode 100644
index 00000000..d212b803
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/MapService1.java
@@ -0,0 +1,20 @@
+package org.example.generic.repo;
+
+import jakarta.inject.Singleton;
+
+import java.util.Map;
+
+@Singleton
+public class MapService1 implements MapService<Map<String, Model1>> {
+
+  final MapRepo1 repo;
+
+  public MapService1(MapRepo1 repo) {
+    this.repo = repo;
+  }
+
+  @Override
+  public Map<String, Model1> get() {
+    return Map.of("", repo.get());
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/MapService2.java b/inject-test/src/test/java/org/example/generic/repo/MapService2.java
new file mode 100644
index 00000000..e289097f
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/MapService2.java
@@ -0,0 +1,20 @@
+package org.example.generic.repo;
+
+import jakarta.inject.Singleton;
+
+import java.util.Map;
+
+@Singleton
+public class MapService2 implements MapService<Map<String, Model2>> {
+
+  final MapRepo2 repo;
+
+  public MapService2(MapRepo2 repo) {
+    this.repo = repo;
+  }
+
+  @Override
+  public Map<String, Model2> get() {
+    return Map.of("", repo.get());
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/Model1.java b/inject-test/src/test/java/org/example/generic/repo/Model1.java
new file mode 100644
index 00000000..1b5d1c1e
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/Model1.java
@@ -0,0 +1,9 @@
+package org.example.generic.repo;
+
+public class Model1 {
+
+  @Override
+  public String toString() {
+    return "Model1";
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/Model2.java b/inject-test/src/test/java/org/example/generic/repo/Model2.java
new file mode 100644
index 00000000..b20281e0
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/Model2.java
@@ -0,0 +1,9 @@
+package org.example.generic.repo;
+
+public class Model2 {
+
+  @Override
+  public String toString() {
+    return "Model2";
+  }
+}
diff --git a/inject-test/src/test/java/org/example/generic/repo/ServiceClass.java b/inject-test/src/test/java/org/example/generic/repo/ServiceClass.java
new file mode 100644
index 00000000..a646dc99
--- /dev/null
+++ b/inject-test/src/test/java/org/example/generic/repo/ServiceClass.java
@@ -0,0 +1,23 @@
+package org.example.generic.repo;
+
+import jakarta.inject.Singleton;
+
+import java.util.Map;
+
+@Singleton
+public class ServiceClass {
+
+  final MapService<Map<String, Model1>> map1;
+  final MapService<Map<String, Model2>> map2;
+
+  public ServiceClass(MapService<Map<String, Model1>> map1, MapService<Map<String, Model2>> map2) {
+    this.map1 = map1;
+    this.map2 = map2;
+  }
+
+  public String process() {
+    Map<String, Model1> m1 = map1.get();
+    Map<String, Model2> m2 = map2.get();
+    return m1.toString() + "|" + m2.toString();
+  }
+}