Repository: avaje/avaje-inject (java)

Issue:
Injection of parameterized types doesn't work as expected
Hi everyone, I expericene a problem with injecting parameterized types. It seems avaje-inject is not able to find beans, if they have a type parameter even though the bean is present in the bean scope. For example I tried to define a bean of [MongoCollection](https://mongodb.github.io/mongo-java-driver/4.11/apidocs/mongodb-driver-sync/com/mongodb/client/MongoCollection.html) in a `@Factory` annotated configuration class via a `@Bean` annotated factory method:

```java
@Factory
class MyConfig {

    @Bean
    public MongoCollection<MyDocument> buildCollection() {
        // omitted for brevity
    }
}
```

Then I tried to inject the bean into my component test (I tried both using `@InjectTest` as well as programmatically creating the `BeanScope`).

The run fails with: `No bean found for type: ...` even though I can see the bean in the BeanScop, when debugging.

I was able to work around this issue by using the raw type in the definition of the factory method as well as when injecting the bean, but I'd rather not be forced to cast the object everytime:

```java
@Factory
class MyConfig {

    @Bean
    public MongoCollection buildCollection() {
        // omitted for brevity
    }
}
```

Did I miss something? Is this really the intended behavior?

Pull request description:
Fix Generic Junit Injection
- Fix situations where generic types were not being properly injected into certain types of test
- Adds new default methods to make retrieving a generic bean more intuitive

resolves #461  

Patch:
diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java
index 9904a574..899210c9 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScope.java
@@ -1,5 +1,6 @@
 package io.avaje.inject;
 
+import io.avaje.inject.spi.Types;
 import io.avaje.lang.NonNullApi;
 import io.avaje.lang.Nullable;
 
@@ -140,7 +141,51 @@ public interface BeanScope extends AutoCloseable {
   <T> T get(Class<T> type, @Nullable String name);
 
   /**
-   * Return a single bean given the generic type and name.
+   * Return a single bean given its type and generic components.
+   *
+   * <pre>{@code
+   * Map<String, CoffeeMaker> coffeeMaker = beanScope.get(Map.class, String.class, CoffeeMaker.class);
+   *
+   * }</pre>
+   *
+   * @param base the base generic type
+   * @param type component types of the generic type
+   * @throws java.util.NoSuchElementException When no matching bean is found
+   */
+  default <T> T get(Type base, Type... types) {
+    return get(null, base, types);
+  }
+
+  /**
+   * Return a single bean given it's name, type, and generic components.
+   *
+   * <pre>{@code
+   * Heater heater = beanScope.get("electric", Set.class,  );
+   * heater.heat();
+   *
+   * }</pre>
+   *
+   * @param type an interface or bean type
+   * @param name the name qualifier of a specific bean
+   * @throws java.util.NoSuchElementException When no matching bean is found
+   */
+  default <T> T get(@Nullable String name, Type type, Type... types) {
+
+    return get(Types.parameterizedType(type, types), name);
+  }
+
+  /**
+   * Return a single bean given the full generic type.
+   *
+   * @param type The generic type
+   * @throws java.util.NoSuchElementException When no matching bean is found
+   */
+  default <T> T get(Type type) {
+    return get(type, (String) null);
+  }
+
+  /**
+   * Return a single bean given the full generic type and name.
    *
    * @param type The generic type
    * @param name the name qualifier of a specific bean
diff --git a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
index 1470c27c..f0904434 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
@@ -325,7 +325,7 @@ public interface BeanScopeBuilder {
      *
      * }</pre>
      */
-    BeanScopeBuilder.ForTesting mock(Class<?> type);
+    BeanScopeBuilder.ForTesting mock(Type type);
 
     /**
      * Register as a Mockito mock with a qualifier name.
@@ -343,7 +343,7 @@ public interface BeanScopeBuilder {
      *
      * }</pre>
      */
-    BeanScopeBuilder.ForTesting mock(Class<?> type, String name);
+    BeanScopeBuilder.ForTesting mock(Type type, String name);
 
     /**
      * Use a mockito mock when injecting this bean type additionally
@@ -398,7 +398,7 @@ public interface BeanScopeBuilder {
      *
      * }</pre>
      */
-    BeanScopeBuilder.ForTesting spy(Class<?> type);
+    BeanScopeBuilder.ForTesting spy(Type type);
 
     /**
      * Register a Mockito spy with a qualifier name.
@@ -416,7 +416,7 @@ public interface BeanScopeBuilder {
      *
      * }</pre>
      */
-    BeanScopeBuilder.ForTesting spy(Class<?> type, String name);
+    BeanScopeBuilder.ForTesting spy(Type type, String name);
 
     /**
      * Use a mockito spy when injecting this bean type additionally
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 1168d3ba..f9d8fff1 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -157,13 +157,14 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
   }
 
   @Override
-  public BeanScopeBuilder.ForTesting mock(Class<?> type) {
-    return mock(type, null, null);
+  public BeanScopeBuilder.ForTesting mock(Type type) {
+    return mock(type, null);
   }
 
   @Override
-  public BeanScopeBuilder.ForTesting mock(Class<?> type, String name) {
-    return mock(type, name, null);
+  public BeanScopeBuilder.ForTesting mock(Type type, String name) {
+    suppliedBeans.add(SuppliedBean.ofType(name, type, null));
+    return this;
   }
 
   @Override
@@ -177,12 +178,12 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
   }
 
   @Override
-  public BeanScopeBuilder.ForTesting spy(Class<?> type) {
+  public BeanScopeBuilder.ForTesting spy(Type type) {
     return spy(type, null, null);
   }
 
   @Override
-  public BeanScopeBuilder.ForTesting spy(Class<?> type, String name) {
+  public BeanScopeBuilder.ForTesting spy(Type type, String name) {
     return spy(type, name, null);
   }
 
@@ -196,6 +197,12 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     return this;
   }
 
+  private <D> BeanScopeBuilder.ForTesting spy(
+      Type type, @Nullable String name, @Nullable Consumer<D> consumer) {
+    enrichBeans.add(new EnrichBean<>(type, name, consumer));
+    return this;
+  }
+
   private void initClassLoader() {
     if (classLoader == null) {
       classLoader = Thread.currentThread().getContextClassLoader();
diff --git a/inject/src/main/java/io/avaje/inject/spi/EnrichBean.java b/inject/src/main/java/io/avaje/inject/spi/EnrichBean.java
index 7e374016..942ba7f7 100644
--- a/inject/src/main/java/io/avaje/inject/spi/EnrichBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/EnrichBean.java
@@ -2,6 +2,7 @@ package io.avaje.inject.spi;
 
 import org.mockito.Mockito;
 
+import java.lang.reflect.Type;
 import java.util.function.Consumer;
 
 /**
@@ -9,11 +10,11 @@ import java.util.function.Consumer;
  */
 public final class EnrichBean<B> {
 
-  private final Class<B> type;
+  private final Type type;
   private final String name;
   private final Consumer<B> consumer;
 
-  public EnrichBean(Class<B> type, String name, Consumer<B> consumer) {
+  public EnrichBean(Type type, String name, Consumer<B> consumer) {
     this.type = type;
     this.name = KeyUtil.lower(name);
     this.consumer = consumer;
diff --git a/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java b/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java
index 66dcc514..089a298b 100644
--- a/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java
+++ b/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java
@@ -1,17 +1,17 @@
 package io.avaje.inject.spi;
 
+import java.lang.annotation.Annotation;
 import java.lang.reflect.*;
 import java.util.Arrays;
+import java.util.Set;
 import java.util.Stack;
 
-/**
- * Cut down version of GenericTypeUtil from Helidon project Apache 2 license.
- */
+/** Cut down version of GenericTypeUtil from Helidon project Apache 2 license. */
 final class GenericTypeUtil {
 
-  /**
-   * Return the Type for type parameter of {@code GenericType<T>}.
-   */
+  private static final Type[] EMPTY_TYPE_ARRAY = {};
+
+  /** Return the Type for type parameter of {@code GenericType<T>}. */
   static Type typeArgument(Class<?> clazz) {
     // collect superclasses
     Stack<Type> superclasses = new Stack<>();
@@ -25,10 +25,7 @@ final class GenericTypeUtil {
       } else if (currentType instanceof ParameterizedType) {
         currentClass = (Class<?>) ((ParameterizedType) currentType).getRawType();
       }
-      if (currentClass.equals(Object.class)) {
-        break;
-      }
-      if (currentClass.equals(GenericType.class)) {
+      if (currentClass.equals(Object.class) || currentClass.equals(GenericType.class)) {
         break;
       }
     } while (true);
@@ -44,7 +41,8 @@ final class GenericTypeUtil {
         if (argIndex > -1) {
           Type typeArg = pt.getActualTypeArguments()[argIndex];
           if (typeArg instanceof TypeVariable) {
-            // type argument is another type variable - look for the value of that variable in subclasses
+            // type argument is another type variable - look for the value of that variable in
+            // subclasses
             tv = (TypeVariable<?>) typeArg;
             continue;
           } else {
@@ -56,7 +54,323 @@ final class GenericTypeUtil {
       // needed type argument not supplied - break and throw exception
       break;
     }
-    throw new IllegalArgumentException(currentType + " does not specify the type parameter T of GenericType<T>");
+    throw new IllegalArgumentException(
+        currentType + " does not specify the type parameter T of GenericType<T>");
+  }
+
+  /** Returns an array type whose elements are all instances of {@code componentType}. */
+  public static GenericArrayType arrayOf(Type elementType) {
+    return new GenericArrayTypeImpl(elementType);
+  }
+
+  /**
+   * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
+   * method if {@code rawType} is not enclosed in another type.
+   */
+  public static ParameterizedType newParameterizedType(Type rawType, Type... typeArguments) {
+    if (typeArguments.length == 0) {
+      throw new IllegalArgumentException("Missing type arguments for " + rawType);
+    }
+    return new ParameterizedTypeImpl(null, rawType, typeArguments);
+  }
+
+  static Type canonicalizeClass(Class<?> cls) {
+    return cls.isArray() ? new GenericArrayTypeImpl(canonicalize(cls.getComponentType())) : cls;
+  }
+
+  /**
+   * Returns a type that is functionally equal but not necessarily equal according to {@link
+   * Object#equals(Object) Object.equals()}.
+   */
+  static Type canonicalize(Type type) {
+    if (type instanceof Class) {
+      Class<?> c = (Class<?>) type;
+      return c.isArray() ? new GenericArrayTypeImpl(canonicalize(c.getComponentType())) : c;
+
+    } else if (type instanceof ParameterizedType) {
+      if (type instanceof ParameterizedTypeImpl) return type;
+      ParameterizedType p = (ParameterizedType) type;
+      return new ParameterizedTypeImpl(
+          p.getOwnerType(), p.getRawType(), p.getActualTypeArguments());
+
+    } else if (type instanceof GenericArrayType) {
+      if (type instanceof GenericArrayTypeImpl) return type;
+      GenericArrayType g = (GenericArrayType) type;
+      return new GenericArrayTypeImpl(g.getGenericComponentType());
+
+    } else if (type instanceof WildcardType) {
+      if (type instanceof WildcardTypeImpl) return type;
+      WildcardType w = (WildcardType) type;
+      return new WildcardTypeImpl(w.getUpperBounds(), w.getLowerBounds());
+
+    } else {
+      return type; // This type is unsupported!
+    }
+  }
+
+  static int hashCodeOrZero(Object o) {
+    return o != null ? o.hashCode() : 0;
+  }
+
+  static String typeToString(Type type) {
+    return type instanceof Class ? ((Class<?>) type).getName() : type.toString();
+  }
+
+  static void checkNotPrimitive(Type type) {
+    if ((type instanceof Class<?>) && ((Class<?>) type).isPrimitive()) {
+      throw new IllegalArgumentException("Unexpected primitive " + type + ". Use the boxed type.");
+    }
+  }
+
+  static final class ParameterizedTypeImpl implements ParameterizedType {
+    private final Type ownerType;
+    private final Type rawType;
+    public final Type[] typeArguments;
+
+    public ParameterizedTypeImpl(Type ownerType, Type rawType, Type... typeArguments) {
+      // Require an owner type if the raw type needs it.
+      if (ownerType != null && rawType instanceof Class<?>) {
+        Class<?> enclosingClass = ((Class<?>) rawType).getEnclosingClass();
+        if (enclosingClass == null || Types.rawType(ownerType) != enclosingClass) {
+          throw new IllegalArgumentException(
+              "unexpected owner type for " + rawType + ": " + ownerType);
+
+        } else if (enclosingClass != null) {
+          throw new IllegalArgumentException("unexpected owner type for " + rawType + ": null");
+        }
+      }
+
+      this.ownerType = ownerType == null ? null : canonicalize(ownerType);
+      this.rawType = canonicalize(rawType);
+      this.typeArguments = typeArguments.clone();
+      for (int t = 0; t < this.typeArguments.length; t++) {
+        if (this.typeArguments[t] == null) throw new NullPointerException();
+        checkNotPrimitive(this.typeArguments[t]);
+        this.typeArguments[t] = canonicalize(this.typeArguments[t]);
+      }
+    }
+
+    @Override
+    public Type[] getActualTypeArguments() {
+      return typeArguments.clone();
+    }
+
+    @Override
+    public Type getRawType() {
+      return rawType;
+    }
+
+    @Override
+    public Type getOwnerType() {
+      return ownerType;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof ParameterizedType
+          && GenericTypeUtil.equals(this, (ParameterizedType) other);
+    }
+
+    @Override
+    public int hashCode() {
+      return Arrays.hashCode(typeArguments) ^ rawType.hashCode() ^ hashCodeOrZero(ownerType);
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder result = new StringBuilder(30 * (typeArguments.length + 1));
+      result.append(typeToString(rawType));
+
+      if (typeArguments.length == 0) {
+        return result.toString();
+      }
+
+      result.append("<").append(typeToString(typeArguments[0]));
+      for (int i = 1; i < typeArguments.length; i++) {
+        result.append(", ").append(typeToString(typeArguments[i]));
+      }
+      return result.append(">").toString();
+    }
   }
 
+  static final class GenericArrayTypeImpl implements GenericArrayType {
+    private final Type componentType;
+
+    GenericArrayTypeImpl(Type componentType) {
+      this.componentType = canonicalize(componentType);
+    }
+
+    @Override
+    public Type getGenericComponentType() {
+      return componentType;
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof GenericArrayType && GenericTypeUtil.equals(this, (GenericArrayType) o);
+    }
+
+    @Override
+    public int hashCode() {
+      return componentType.hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return typeToString(componentType) + "[]";
+    }
+  }
+
+  /**
+   * The WildcardType interface supports multiple upper bounds and multiple lower bounds. We only
+   * support what the Java 6 language needs - at most one bound. If a lower bound is set, the upper
+   * bound must be Object.class.
+   */
+  static final class WildcardTypeImpl implements WildcardType {
+    private final Type upperBound;
+    private final Type lowerBound;
+
+    WildcardTypeImpl(Type[] upperBounds, Type[] lowerBounds) {
+      if ((lowerBounds.length > 1) || (upperBounds.length != 1))
+        throw new IllegalArgumentException();
+
+      if (lowerBounds.length == 1) {
+        if (lowerBounds[0] == null) throw new NullPointerException();
+        checkNotPrimitive(lowerBounds[0]);
+        if (upperBounds[0] != Object.class) throw new IllegalArgumentException();
+        this.lowerBound = canonicalize(lowerBounds[0]);
+        this.upperBound = Object.class;
+
+      } else {
+        if (upperBounds[0] == null) throw new NullPointerException();
+        checkNotPrimitive(upperBounds[0]);
+        this.lowerBound = null;
+        this.upperBound = canonicalize(upperBounds[0]);
+      }
+    }
+
+    @Override
+    public Type[] getUpperBounds() {
+      return new Type[] {upperBound};
+    }
+
+    @Override
+    public Type[] getLowerBounds() {
+      return lowerBound != null ? new Type[] {lowerBound} : EMPTY_TYPE_ARRAY;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other instanceof WildcardType && GenericTypeUtil.equals(this, (WildcardType) other);
+    }
+
+    @Override
+    public int hashCode() {
+      // This equals Arrays.hashCode(getLowerBounds()) ^ Arrays.hashCode(getUpperBounds()).
+      return (lowerBound != null ? 31 + lowerBound.hashCode() : 1) ^ (31 + upperBound.hashCode());
+    }
+
+    @Override
+    public String toString() {
+      if (lowerBound != null) {
+        return "? super " + typeToString(lowerBound);
+      } else if (upperBound == Object.class) {
+        return "?";
+      } else {
+        return "? extends " + typeToString(upperBound);
+      }
+    }
+  }
+
+  static String typeAnnotatedWithAnnotations(Type type, Set<? extends Annotation> annotations) {
+    return type + (annotations.isEmpty() ? " (with no annotations)" : " annotated " + annotations);
+  }
+
+  /**
+   * Returns a type that represents an unknown type that extends {@code bound}. For example, if
+   * {@code bound} is {@code CharSequence.class}, this returns {@code ? extends CharSequence}. If
+   * {@code bound} is {@code Object.class}, this returns {@code ?}, which is shorthand for {@code ?
+   * extends Object}.
+   */
+  static WildcardType subtypeOf(Type bound) {
+    Type[] upperBounds;
+    if (bound instanceof WildcardType) {
+      upperBounds = ((WildcardType) bound).getUpperBounds();
+    } else {
+      upperBounds = new Type[] {bound};
+    }
+    return new WildcardTypeImpl(upperBounds, EMPTY_TYPE_ARRAY);
+  }
+
+  /**
+   * Returns a type that represents an unknown supertype of {@code bound}. For example, if {@code
+   * bound} is {@code String.class}, this returns {@code ? super String}.
+   */
+  static WildcardType supertypeOf(Type bound) {
+    Type[] lowerBounds;
+    if (bound instanceof WildcardType) {
+      lowerBounds = ((WildcardType) bound).getLowerBounds();
+    } else {
+      lowerBounds = new Type[] {bound};
+    }
+    return new WildcardTypeImpl(new Type[] {Object.class}, lowerBounds);
+  }
+
+  /** Returns true if {@code a} and {@code b} are equal. */
+  static boolean equals(Type a, Type b) {
+    if (a == b) {
+      return true; // Also handles (a == null && b == null).
+
+    } else if (a instanceof Class) {
+      if (b instanceof GenericArrayType) {
+        return equals(
+            ((Class<?>) a).getComponentType(), ((GenericArrayType) b).getGenericComponentType());
+      }
+      return a.equals(b); // Class already specifies equals().
+
+    } else if (a instanceof ParameterizedType) {
+      if (!(b instanceof ParameterizedType)) return false;
+      ParameterizedType pa = (ParameterizedType) a;
+      ParameterizedType pb = (ParameterizedType) b;
+      Type[] aTypeArguments =
+          pa instanceof ParameterizedTypeImpl
+              ? ((ParameterizedTypeImpl) pa).typeArguments
+              : pa.getActualTypeArguments();
+      Type[] bTypeArguments =
+          pb instanceof ParameterizedTypeImpl
+              ? ((ParameterizedTypeImpl) pb).typeArguments
+              : pb.getActualTypeArguments();
+      return equals(pa.getOwnerType(), pb.getOwnerType())
+          && pa.getRawType().equals(pb.getRawType())
+          && Arrays.equals(aTypeArguments, bTypeArguments);
+
+    } else if (a instanceof GenericArrayType) {
+      if (b instanceof Class) {
+        return equals(
+            ((Class<?>) b).getComponentType(), ((GenericArrayType) a).getGenericComponentType());
+      }
+      if (!(b instanceof GenericArrayType)) return false;
+      GenericArrayType ga = (GenericArrayType) a;
+      GenericArrayType gb = (GenericArrayType) b;
+      return equals(ga.getGenericComponentType(), gb.getGenericComponentType());
+
+    } else if (a instanceof WildcardType) {
+      if (!(b instanceof WildcardType)) return false;
+      WildcardType wa = (WildcardType) a;
+      WildcardType wb = (WildcardType) b;
+      return Arrays.equals(wa.getUpperBounds(), wb.getUpperBounds())
+          && Arrays.equals(wa.getLowerBounds(), wb.getLowerBounds());
+
+    } else if (a instanceof TypeVariable) {
+      if (!(b instanceof TypeVariable)) return false;
+      TypeVariable<?> va = (TypeVariable<?>) a;
+      TypeVariable<?> vb = (TypeVariable<?>) b;
+      return va.getGenericDeclaration() == vb.getGenericDeclaration()
+          && va.getName().equals(vb.getName());
+
+    } else {
+      // This isn't a supported type.
+      return false;
+    }
+  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java b/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
index 694490fb..2705f3d2 100644
--- a/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
@@ -1,12 +1,13 @@
 package io.avaje.inject.spi;
 
-import io.avaje.inject.BeanEntry;
-import jakarta.inject.Named;
-import org.mockito.Mockito;
-
 import java.lang.reflect.Type;
 import java.util.function.Consumer;
 
+import org.mockito.Mockito;
+
+import io.avaje.inject.BeanEntry;
+import jakarta.inject.Named;
+
 /**
  * Holds beans supplied to the dependency injection.
  * <p>
@@ -47,7 +48,7 @@ public class SuppliedBean {
    * Create a supplied bean for a generic type.
    */
   public static SuppliedBean ofType(String name, Type type, Object source) {
-    return new SuppliedBean(BeanEntry.SUPPLIED, name, type, source);
+    return new SuppliedBean.ForType(name, type, source);
   }
 
   /**
@@ -136,4 +137,24 @@ public class SuppliedBean {
       return source;
     }
   }
+
+  /** Class based supplied bean. */
+  private static final class ForType extends SuppliedBean {
+
+    private final Type classType;
+
+    ForType(String name, Type type, Object source) {
+      super(BeanEntry.SUPPLIED, name, type, source);
+      this.classType = type;
+    }
+
+    @Override
+    public Object source() {
+      if (source == null) {
+        source = Mockito.mock(Types.rawType(classType));
+      }
+      return source;
+    }
+  }
+
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/Types.java b/inject/src/main/java/io/avaje/inject/spi/Types.java
new file mode 100644
index 00000000..acb25ac2
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/Types.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright (C) 2014 Square, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *    https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.avaje.inject.spi;
+
+import java.lang.reflect.Array;
+import java.lang.reflect.GenericArrayType;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+
+/** Factory methods for types. */
+public class Types {
+
+  private Types() {}
+
+  /** Returns an array type whose elements are all instances of {@code componentType}. */
+  public static GenericArrayType arrayOf(Type elementType) {
+    return GenericTypeUtil.arrayOf(elementType);
+  }
+
+  /**
+   * Returns a new parameterized type, applying {@code typeArguments} to {@code rawType}. Use this
+   * method if {@code rawType} is not enclosed in another type.
+   */
+  public static ParameterizedType parameterizedType(Type rawType, Type... typeArguments) {
+    return GenericTypeUtil.newParameterizedType(rawType, typeArguments);
+  }
+
+  /** Return the raw type for the given potentially generic type. */
+  public static Class<?> rawType(Type type) {
+    if (type instanceof Class<?>) {
+      // type is a normal class.
+      return (Class<?>) type;
+
+    } else if (type instanceof ParameterizedType) {
+      ParameterizedType parameterizedType = (ParameterizedType) type;
+
+      Type rawType = parameterizedType.getRawType();
+      return (Class<?>) rawType;
+
+    } else if (type instanceof GenericArrayType) {
+      Type componentType = ((GenericArrayType) type).getGenericComponentType();
+      return Array.newInstance(rawType(componentType), 0).getClass();
+
+    } else if (type instanceof TypeVariable) {
+      return Object.class;
+
+    } else if (type instanceof WildcardType) {
+      return rawType(((WildcardType) type).getUpperBounds()[0]);
+
+    } else {
+
+      String className = type == null ? "null" : type.getClass().getName();
+      throw new IllegalArgumentException(
+          "Expected a Class, ParameterizedType, or  GenericArrayType, but <"
+              + type
+              + "> is of type "
+              + className);
+    }
+  }
+}


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/generic/Generic.java b/blackbox-test-inject/src/main/java/org/example/myapp/generic/Generic.java
new file mode 100644
index 00000000..19ecdf0a
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/generic/Generic.java
@@ -0,0 +1,6 @@
+package org.example.myapp.generic;
+
+public interface Generic<T> {
+
+
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/generic/GenericFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/generic/GenericFactory.java
new file mode 100644
index 00000000..34cc4e56
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/generic/GenericFactory.java
@@ -0,0 +1,22 @@
+package org.example.myapp.generic;
+
+import io.avaje.inject.Bean;
+import io.avaje.inject.Factory;
+
+@Factory
+public class GenericFactory {
+
+  @Bean
+  Generic<Integer> one() {
+
+    return new Generic<>() {};
+  }
+
+  @Bean
+  Stringy two() {
+
+    return new Stringy() {};
+  }
+
+  public interface Stringy extends Generic<String> {}
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/generic/GenericFactoryTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/generic/GenericFactoryTest.java
new file mode 100644
index 00000000..6b5a83a5
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/generic/GenericFactoryTest.java
@@ -0,0 +1,20 @@
+package org.example.myapp.generic;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+
+@InjectTest
+class GenericFactoryTest {
+
+  @Inject Generic<Integer> intymcintface;
+  @Inject Generic<String> stringy;
+
+  @Test
+  void test() {
+    assertThat(intymcintface).isNotNull();
+  }
+}
diff --git a/inject-test/pom.xml b/inject-test/pom.xml
index 5987b6dd..a1a54ebe 100644
--- a/inject-test/pom.xml
+++ b/inject-test/pom.xml
@@ -23,6 +23,13 @@
       <version>${project.version}</version>
     </dependency>
 
+    <dependency>
+      <groupId>io.avaje</groupId>
+      <artifactId>avaje-inject-generator</artifactId>
+      <version>${project.version}</version>
+      <scope>test</scope>
+    </dependency>
+
     <dependency>
       <groupId>org.junit.jupiter</groupId>
       <artifactId>junit-jupiter-api</artifactId>
diff --git a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
index 1f0d11ae..ebf7b23d 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/MetaReader.java
@@ -15,6 +15,7 @@ import org.mockito.internal.util.reflection.GenericMaster;
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Field;
 import java.lang.reflect.Modifier;
+import java.lang.reflect.Type;
 import java.util.ArrayList;
 import java.util.List;
 
@@ -53,14 +54,14 @@ final class MetaReader {
 
   @Override
   public String toString() {
-    String s = toStringAppend("mocks:", mocks);
-    s += toStringAppend("spies:", spies);
-    s += toStringAppend("inject:", injection);
-    s += toStringAppend("captors:", captors);
-    s += toStringAppend("staticMocks:", staticMocks);
-    s += toStringAppend("staticSpies:", staticSpies);
-    s += toStringAppend("staticInjection:", staticInjection);
-    return s;
+    StringBuilder s = new StringBuilder().append(toStringAppend("mocks:", mocks));
+    s.append(toStringAppend("spies:", spies));
+    s.append(toStringAppend("inject:", injection));
+    s.append(toStringAppend("captors:", captors));
+    s.append(toStringAppend("staticMocks:", staticMocks));
+    s.append(toStringAppend("staticSpies:", staticSpies));
+    s.append(toStringAppend("staticInjection:", staticInjection));
+    return s.toString();
   }
 
   private String toStringAppend(String key, List<?> entries) {
@@ -282,8 +283,8 @@ final class MetaReader {
       return field.getName();
     }
 
-    Class<?> type() {
-      return field.getType();
+    Type type() {
+      return field.getGenericType();
     }
 
     String name() {
diff --git a/inject-test/src/main/java/io/avaje/inject/test/Plugin.java b/inject-test/src/main/java/io/avaje/inject/test/Plugin.java
index 346f55a7..c3f7834d 100644
--- a/inject-test/src/main/java/io/avaje/inject/test/Plugin.java
+++ b/inject-test/src/main/java/io/avaje/inject/test/Plugin.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.test;
 
+import java.lang.reflect.Type;
+
 import io.avaje.inject.BeanScope;
 
 /**
@@ -13,7 +15,7 @@ public interface Plugin {
   /**
    * Return true if this plugin should inject for this class.
    */
-  boolean forType(Class<?> type);
+  boolean forType(Type type);
 
   /**
    * Create a plugin scope for either EACH or ALL (per test or per class)
@@ -32,7 +34,7 @@ public interface Plugin {
      * <p>
      * For example, avaje-jex-test will create a http client.
      */
-    Object create(Class<?> type);
+    Object create(Type type);
 
     /**
      * Close resources that are in this scope - for example, http server.
diff --git a/inject-test/src/test/java/io/avaje/inject/test/MetaReaderTest.java b/inject-test/src/test/java/io/avaje/inject/test/MetaReaderTest.java
index 0d5769b5..21332250 100644
--- a/inject-test/src/test/java/io/avaje/inject/test/MetaReaderTest.java
+++ b/inject-test/src/test/java/io/avaje/inject/test/MetaReaderTest.java
@@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test;
 import org.mockito.Mock;
 import org.mockito.Mockito;
 
+import java.lang.reflect.Type;
 import java.net.http.HttpClient;
 
 import static org.assertj.core.api.Assertions.assertThat;
@@ -49,7 +50,7 @@ class MetaReaderTest {
 
   static class MyPlugin implements Plugin {
 
-    public boolean forType(Class<?> type) {
+  public boolean forType(Type type) {
       return HttpClient.class.equals(type);
     }
 
@@ -63,7 +64,7 @@ class MetaReaderTest {
       HttpClient httpClient;
 
       @Override
-      public Object create(Class<?> type) {
+      public Object create(Type type) {
         this.httpClient = HttpClient.newBuilder().build();
         return httpClient;
       }
diff --git a/inject-test/src/test/java/org/example/coffee/provider/BProvProviderTest.java b/inject-test/src/test/java/org/example/coffee/provider/BProvProviderTest.java
index 3d8185e7..2e5e80ba 100644
--- a/inject-test/src/test/java/org/example/coffee/provider/BProvProviderTest.java
+++ b/inject-test/src/test/java/org/example/coffee/provider/BProvProviderTest.java
@@ -11,10 +11,10 @@ class BProvProviderTest {
   void providerOfGenericType() {
     try (BeanScope beanScope = BeanScope.builder().build()) {
 
-      BProv<String> bProv = beanScope.get(BProvProvider$DI.TYPE_BProvString, null);
+      BProv<String> bProv = beanScope.get(BProvProvider$DI.TYPE_BProvString);
       assertThat(bProv.get()).isEqualTo("Hello BProv1");
 
-      BProv<String> bProv2 = beanScope.get(BProvProvider$DI.TYPE_BProvString, null);
+      BProv<String> bProv2 = beanScope.get(BProvProvider$DI.TYPE_BProvString);
       assertThat(bProv).isNotSameAs(bProv2);
       assertThat(bProv2.get()).isEqualTo("Hello BProv2");
     }
diff --git a/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java b/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java
index a8f936db..60b07513 100644
--- a/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java
+++ b/inject-test/src/test/java/org/example/generic/MzCrudServiceTest.java
@@ -12,8 +12,8 @@ class MzCrudServiceTest {
   void test() {
     try (BeanScope beanScope = BeanScope.builder().build()) {
       MzCrudService crud = beanScope.get(MzCrudService.class);
-      ReadService<MzObj,Integer> read = beanScope.get(MzCrudService$DI.TYPE_ReadServiceMzObjInteger, null);
-      CreateService<MzObj,Integer> create = beanScope.get(MzCrudService$DI.TYPE_CreateServiceMzObjInteger, null);
+      ReadService<MzObj,Integer> read = beanScope.get(MzCrudService$DI.TYPE_ReadServiceMzObjInteger);
+      CreateService<MzObj,Integer> create = beanScope.get(MzCrudService$DI.TYPE_CreateServiceMzObjInteger);
 
       assertNotNull(crud);
       assertThat(crud).isSameAs(read);
diff --git a/inject-test/src/test/java/org/example/generic/PathBuildTaskTest.java b/inject-test/src/test/java/org/example/generic/PathBuildTaskTest.java
index 3d461a9e..5e54219f 100644
--- a/inject-test/src/test/java/org/example/generic/PathBuildTaskTest.java
+++ b/inject-test/src/test/java/org/example/generic/PathBuildTaskTest.java
@@ -18,7 +18,7 @@ class PathBuildTaskTest {
       // PathBuildTask is registered as providing PathBuildTask.class, BuildTask.class and generic TYPE_BuildTaskWithPath
       assertThat(pathBuildTask).isSameAs(buildTask);
 
-      Object viaType = beanScope.get(PathBuildTask$DI.TYPE_BuildTaskWithPath, null);
+      Object viaType = beanScope.get(PathBuildTask$DI.TYPE_BuildTaskWithPath);
       assertThat(viaType).isSameAs(buildTask);
     }
   }