Repository: avaje/avaje-inject
Pull request #136 (2021-07-26 05:48:29)

Support registration and supply of external dependencies using generic types


Patch:
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
index 5d4eaa50..d3dfc90b 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/BeanReader.java
@@ -96,6 +96,10 @@ class BeanReader {
     return typeReader.getInterfaces();
   }
 
+  Set<GenericType> getGenericTypes() {
+    return typeReader.getGenericTypes();
+  }
+
   /**
    * Return the short name of the element.
    */
@@ -176,6 +180,7 @@ class BeanReader {
     if (Util.validImportType(type)) {
       importTypes.add(type);
     }
+    typeReader.extraImports(importTypes);
     requestParams.addImports(importTypes);
     return importTypes;
   }
@@ -254,42 +259,6 @@ class BeanReader {
     writer.append("  }").eol();
   }
 
-  void buildReq(Append writer) {
-    writer.append("    builder.requestScope(%s.class, new RequestScopeProvider<%s>() {", shortName, shortName).eol();
-    writer.append("      @Override").eol();
-    writer.append("      public %s provide(RequestScope scope) {", shortName).eol();
-  }
-
-  void buildReqEnd(Append writer) {
-    writer.append("        return bean;").eol();
-    writer.append("      }").eol();
-    writer.append("    }");
-
-    final String ifaceTypes = typeReader.getTypesRegister();
-    if (ifaceTypes != null) {
-      if (name != null && !name.isEmpty()) {
-        writer.append(", \"%s\"", name);
-        writer.append(", ");
-        writer.append(ifaceTypes);
-      }
-    }
-    writer.append(");").eol();
-  }
-
-  void writePostConstruct(Append writer) {
-    if (postConstructMethod != null) {
-      writer.append("        bean.%s();", postConstructMethod.getSimpleName()).eol();
-    }
-  }
-
-  void writePreDestroy(Append writer) {
-    if (preDestroyMethod != null) {
-      writer.append("        scope.addClosable(bean::%s);", preDestroyMethod.getSimpleName()).eol();
-    } else if (typeReader.isClosable()) {
-      writer.append("        scope.addClosable(bean);").eol();
-    }
-  }
-
   List<FieldReader> getInjectFields() {
     return typeReader.getInjectFields();
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
index ce9ff010..6c730445 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Constants.java
@@ -8,6 +8,7 @@ class Constants {
   static final String AUTO_CLOSEABLE = "java.lang.AutoCloseable";
   static final String OPTIONAL = "java.util.Optional";
   static final String KOTLIN_METADATA = "kotlin.Metadata";
+  static final String TYPE = "java.lang.reflect.Type";
 
   static final String PROVIDER = "jakarta.inject.Provider";
   static final String SINGLETON = "jakarta.inject.Singleton";
@@ -30,4 +31,6 @@ class Constants {
   static final String BUILDER = "io.avaje.inject.spi.Builder";
   static final String DEPENDENCYMETA = "io.avaje.inject.spi.DependencyMeta";
   static final String MODULE = "io.avaje.inject.spi.Module";
+  static final String GENERICTYPE = "io.avaje.inject.spi.GenericType";
+
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java b/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
index 85a7582a..1130f66a 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/GenericType.java
@@ -9,10 +9,25 @@ import java.util.Set;
  */
 class GenericType {
 
+  private final String raw;
   private String mainType;
 
   private final List<GenericType> params = new ArrayList<>();
 
+  /**
+   * Create for top level type.
+   */
+  GenericType(String raw) {
+    this.raw = raw;
+  }
+
+  /**
+   * Create for parameter type.
+   */
+  GenericType() {
+    this.raw = null;
+  }
+
   /**
    * Return true if this is a generic type.
    */
@@ -34,6 +49,10 @@ class GenericType {
     return isGeneric(paramType) ? parse(paramType) : null;
   }
 
+  @Override
+  public String toString() {
+    return raw != null ? raw : mainType + '<' + params + '>';
+  }
 
   void addImports(Set<String> importTypes) {
     final String type = trimExtends();
@@ -70,6 +89,19 @@ class GenericType {
     }
   }
 
+  String shortName() {
+    StringBuilder sb = new StringBuilder();
+    shortName(sb);
+    return sb.toString();
+  }
+
+  void shortName(StringBuilder sb) {
+    sb.append(Util.shortName(trimExtends()));
+    for (GenericType param : params) {
+      param.shortName(sb);
+    }
+  }
+
   private String trimExtends() {
     if (mainType.startsWith("? extends ")) {
       return mainType.substring(10);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/GenericTypeParser.java b/inject-generator/src/main/java/io/avaje/inject/generator/GenericTypeParser.java
index 44682b80..7e331c18 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/GenericTypeParser.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/GenericTypeParser.java
@@ -12,7 +12,7 @@ class GenericTypeParser {
 
   GenericTypeParser(String raw) {
     this.raw = raw;
-    stack.push(new GenericType());
+    stack.push(new GenericType(raw));
   }
 
   GenericType parse() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
index 7193d828..ed02a33d 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MetaData.java
@@ -145,7 +145,6 @@ class MetaData {
   }
 
   String buildMethod(MetaDataOrdering ordering) {
-
     StringBuilder sb = new StringBuilder(200);
     sb.append("  @DependencyMeta(type=\"").append(type).append("\"");
     if (name != null) {
@@ -172,16 +171,16 @@ class MetaData {
       if (GenericType.isGeneric(depend) && !Util.isProvider(depend)) {
         // provide implementation of generic interface as a parameter to the build method
         final MetaData providerMeta = findProviderOf(depend, ordering);
-        String prov = (providerMeta == null) ? "UnknownProvider" : Util.shortName(providerMeta.type);
-        sb.append(", () -> {").append(NEWLINE);
-        sb.append("      return builder.get(").append(prov).append(".class);").append(NEWLINE);
-        sb.append("    }");
+        if (providerMeta != null) {
+          final GenericType type = GenericType.parse(depend);
+          sb.append(", ").append(providerMeta.shortType).append("$DI.provider").append(type.shortName()).append("(builder)");
+        } else {
+          sb.append("UnknownProvider??");
+        }
       }
     }
-
     sb.append(");").append(NEWLINE);
     sb.append("  }").append(NEWLINE);
-
     return sb.toString();
   }
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
index d0fb279c..cea4df21 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java
@@ -163,7 +163,10 @@ class ScopeInfo {
     if (emptyModule) {
       // typically nothing in the default scope, only custom scopes
       if (meta.size() > 0) {
-        context.logWarn("Empty module but meta is not empty? " + meta);
+        String msg = meta + " is being ignored by avaje-inject as there is no 'default' module. This is expected when " +
+          meta + " is a @Singleton being generated by an annotation processor but there is no 'default' module " +
+          "(only custom modules are being used).";
+        context.logWarn(msg);
       }
       return;
     }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index 76b7f806..c25713cb 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -5,6 +5,7 @@ import javax.tools.JavaFileObject;
 import java.io.IOException;
 import java.io.Writer;
 import java.util.List;
+import java.util.Set;
 
 /**
  * Write the source code for the bean.
@@ -46,6 +47,8 @@ class SimpleBeanWriter {
     if (isRequestScopedController()) {
       writeRequestCreate();
     } else {
+      writeGenericTypeFields();
+      writeGenericProviders();
       writeStaticFactoryMethod();
       writeStaticFactoryBeanMethods();
     }
@@ -53,6 +56,33 @@ class SimpleBeanWriter {
     writer.close();
   }
 
+  private void writeGenericProviders() {
+    final Set<GenericType> genericTypes = beanReader.getGenericTypes();
+    if (genericTypes != null && !genericTypes.isEmpty()) {
+      for (GenericType type : genericTypes) {
+        final String sn = type.shortName();
+        writer.append("  public static Provider<");
+        type.writeShort(writer);
+        writer.append("> provider%s(Builder builder) {",  sn).eol();
+        writer.append("    return builder.getProviderFor(%s.class, TYPE_%s);", shortName, sn).eol();
+        writer.append("  }").eol();
+      }
+      writer.eol();
+    }
+  }
+
+  private void writeGenericTypeFields() {
+    final Set<GenericType> genericTypes = beanReader.getGenericTypes();
+    if (genericTypes != null && !genericTypes.isEmpty()) {
+      for (GenericType type : genericTypes) {
+        writer.append("  public static final Type TYPE_%s = new GenericType<", type.shortName());
+        type.writeShort(writer);
+        writer.append(">(){};").eol();
+      }
+      writer.eol();
+    }
+  }
+
   private void writeRequestCreate() {
     beanReader.writeRequestCreate(writer);
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
index cd435303..8f8b7b85 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeAppender.java
@@ -11,32 +11,45 @@ class TypeAppender {
 
   private final Set<String> importTypes;
   private final Set<String> types = new LinkedHashSet<>();
+  private final Set<GenericType> genericTypes = new LinkedHashSet<>();
 
   TypeAppender(Set<String> importTypes) {
     this.importTypes = importTypes;
   }
 
-  void add(String type) {
-    types.add(type);
-    importTypes.add(type);
+  void add(String classType) {
+    importTypes.add(classType);
+    types.add(Util.shortName(classType) + ".class");
   }
 
   void add(List<String> types) {
     for (String type : types) {
-      if (!GenericType.isGeneric(type)) {
+      if (GenericType.isGeneric(type)) {
+        genericTypes.add(GenericType.parse(type));
+      } else {
         add(type);
       }
     }
   }
 
+  Set<GenericType> genericTypes() {
+    return genericTypes;
+  }
+
   String asString() {
+    if (!genericTypes.isEmpty()) {
+      for (GenericType genericType : genericTypes) {
+        types.add("TYPE_" + genericType.shortName());
+      }
+    }
+
     int count = 0;
     final StringBuilder sb = new StringBuilder();
     for (String type : types) {
       if (count++ > 0) {
         sb.append(", ");
       }
-      sb.append(Util.shortName(type)).append(".class");
+      sb.append(type);
     }
     return sb.toString();
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
index 6dc7ca82..d6d608f6 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeReader.java
@@ -15,6 +15,7 @@ class TypeReader {
   private final Set<String> importTypes;
   private final TypeExtendsReader extendsReader;
   private final TypeAnnotationReader annotationReader;
+  private Set<GenericType> genericTypes;
   private String typesRegister;
 
   TypeReader(TypeElement beanType, ProcessingContext context, Set<String> importTypes, boolean factory) {
@@ -73,6 +74,10 @@ class TypeReader {
     return extendsReader.getConstructor();
   }
 
+  Set<GenericType> getGenericTypes() {
+    return genericTypes;
+  }
+
   void process() {
     extendsReader.process(forBean);
     if (forBean) {
@@ -89,11 +94,7 @@ class TypeReader {
     if (annotationReader.hasQualifierName()) {
       return annotationReader.getQualifierName();
     }
-    String derivedName = extendsReader.getQualifierName();
-    if (derivedName != null) {
-      return derivedName;
-    }
-    return derivedName;
+    return extendsReader.getQualifierName();
   }
 
   private void initRegistrationTypes() {
@@ -104,7 +105,18 @@ class TypeReader {
     if (forBean) {
       appender.add(annotationReader.getAnnotationTypes());
     }
+    this.genericTypes = appender.genericTypes();
     this.typesRegister = appender.asString();
   }
 
+  void extraImports(Set<String> importTypes) {
+    if (!genericTypes.isEmpty()) {
+      importTypes.add(Constants.TYPE);
+      importTypes.add(Constants.GENERICTYPE);
+      importTypes.add(Constants.PROVIDER);
+      for (GenericType type : genericTypes) {
+        type.addImports(importTypes);
+      }
+    }
+  }
 }
diff --git a/inject/src/main/java/io/avaje/inject/BeanScope.java b/inject/src/main/java/io/avaje/inject/BeanScope.java
index b5631c22..985483cc 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScope.java
@@ -1,6 +1,7 @@
 package io.avaje.inject;
 
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
 import java.util.List;
 
 /**
@@ -130,6 +131,14 @@ public interface BeanScope extends AutoCloseable {
    */
   <T> T get(Class<T> type, String name);
 
+  /**
+   * Return a single bean given the generic type and name.
+   *
+   * @param type The generic type
+   * @param name the name qualifier of a specific bean
+   */
+  <T> T get(Type type, String name);
+
   /**
    * Return the list of beans that have an annotation.
    *
diff --git a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
index e1fc90df..dfe3af5d 100644
--- a/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/BeanScopeBuilder.java
@@ -2,6 +2,7 @@ package io.avaje.inject;
 
 import io.avaje.inject.spi.Module;
 
+import java.lang.reflect.Type;
 import java.util.function.Consumer;
 
 /**
@@ -96,7 +97,8 @@ public interface BeanScopeBuilder {
    *
    * }</pre>
    *
-   * @param beans Externally provided beans used when injecting a dependency for the bean or the interface(s) it implements
+   * @param beans Externally provided beans used when injecting a dependency
+   *              for the bean or the interface(s) it implements
    * @return This BeanScopeBuilder
    */
   BeanScopeBuilder withBeans(Object... beans);
@@ -122,7 +124,7 @@ public interface BeanScopeBuilder {
    * }</pre>
    *
    * @param type The dependency injection type this bean is target for
-   * @param bean The supplied bean instance to use (typically a test mock)
+   * @param bean The supplied bean instance to use for injection
    */
   <D> BeanScopeBuilder withBean(Class<D> type, D bean);
 
@@ -131,10 +133,27 @@ public interface BeanScopeBuilder {
    *
    * @param name The name qualifier
    * @param type The dependency injection type this bean is target for
-   * @param bean The supplied bean instance to use (typically a test mock)
+   * @param bean The supplied bean instance to use for injection
    */
   <D> BeanScopeBuilder withBean(String name, Class<D> type, D bean);
 
+  /**
+   * Add a supplied bean instance with the given name and generic type.
+   *
+   * @param name The name qualifier
+   * @param type The dependency injection type this bean is target for
+   * @param bean The supplied bean instance to use for injection
+   */
+  <D> BeanScopeBuilder withBean(String name, Type type, D bean);
+
+  /**
+   * Add a supplied bean instance with a generic type.
+   *
+   * @param type The dependency injection type this bean is target for
+   * @param bean The supplied bean instance to use for injection
+   */
+  <D> BeanScopeBuilder withBean(Type type, D bean);
+
   /**
    * Use the given BeanScope as the parent. This becomes an additional
    * source of beans that can be wired and accessed in this scope.
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index ada4fe40..be2deb7b 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -1,12 +1,13 @@
 package io.avaje.inject;
 
-import io.avaje.inject.spi.Module;
 import io.avaje.inject.spi.Builder;
 import io.avaje.inject.spi.EnrichBean;
+import io.avaje.inject.spi.Module;
 import io.avaje.inject.spi.SuppliedBean;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.lang.reflect.Type;
 import java.util.*;
 import java.util.function.Consumer;
 
@@ -53,10 +54,9 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
   }
 
   @Override
-  @SuppressWarnings({"unchecked", "rawtypes"})
   public BeanScopeBuilder withBeans(Object... beans) {
     for (Object bean : beans) {
-      suppliedBeans.add(new SuppliedBean(null, suppliedType(bean.getClass()), bean));
+      suppliedBeans.add(SuppliedBean.of(superOf(bean.getClass()), bean));
     }
     return this;
   }
@@ -68,7 +68,18 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
   @Override
   public <D> BeanScopeBuilder withBean(String name, Class<D> type, D bean) {
-    suppliedBeans.add(new SuppliedBean<>(name, type, bean));
+    suppliedBeans.add(SuppliedBean.of(name, type, bean));
+    return this;
+  }
+
+  @Override
+  public <D> BeanScopeBuilder withBean(Type type, D bean) {
+    return withBean(null, type, bean);
+  }
+
+  @Override
+  public <D> BeanScopeBuilder withBean(String name, Type type, D bean) {
+    suppliedBeans.add(SuppliedBean.ofType(name, type, bean));
     return this;
   }
 
@@ -93,7 +104,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
   }
 
   private <D> BeanScopeBuilder.ForTesting withMock(Class<D> type, String name, Consumer<D> consumer) {
-    suppliedBeans.add(new SuppliedBean<>(name, type, null, consumer));
+    suppliedBeans.add(SuppliedBean.of(name, type, null, consumer));
     return this;
   }
 
@@ -143,7 +154,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
   /**
    * Return the type that we map the supplied bean to.
    */
-  private Class<?> suppliedType(Class<?> suppliedClass) {
+  private static Class<?> superOf(Class<?> suppliedClass) {
     Class<?> suppliedSuper = suppliedClass.getSuperclass();
     if (Object.class.equals(suppliedSuper)) {
       return suppliedClass;
@@ -170,16 +181,16 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       this.factories.addAll(includeModules);
       this.suppliedBeans = suppliedBeans;
       for (Module includeModule : includeModules) {
-        moduleNames.add(includeModule.getClass().getCanonicalName());
+        moduleNames.add(includeModule.getClass().getTypeName());
       }
     }
 
     void add(Module factory) {
       FactoryState wrappedFactory = new FactoryState(factory);
-      providesMap.computeIfAbsent(factory.getClass().getCanonicalName(), s -> new FactoryList()).add(wrappedFactory);
+      providesMap.computeIfAbsent(factory.getClass().getTypeName(), s -> new FactoryList()).add(wrappedFactory);
       if (!isEmpty(factory.provides())) {
         for (Class<?> feature : factory.provides()) {
-          providesMap.computeIfAbsent(feature.getCanonicalName(), s -> new FactoryList()).add(wrappedFactory);
+          providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(wrappedFactory);
         }
       }
       if (isEmpty(factory.requires())) {
@@ -206,7 +217,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
     private void push(FactoryState factory) {
       factory.setPushed();
       factories.add(factory.factory());
-      moduleNames.add(factory.getClass().getCanonicalName());
+      moduleNames.add(factory.getClass().getTypeName());
     }
 
     /**
@@ -249,7 +260,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       } else if (!queue.isEmpty()) {
         StringBuilder sb = new StringBuilder();
         for (FactoryState factory : queue) {
-          sb.append("Module [").append(factory.getClass().getCanonicalName()).append("] has unsatisfied dependencies on modules:");
+          sb.append("Module [").append(factory.getClass()).append("] has unsatisfied dependencies on modules:");
           for (Class<?> depModuleName : factory.requires()) {
             if (!moduleNames.contains(depModuleName)) {
               sb.append(String.format(" [%s]", depModuleName));
@@ -289,7 +300,7 @@ class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
      */
     private boolean satisfiedDependencies(FactoryState factory) {
       for (Class<?> moduleOrFeature : factory.requires()) {
-        FactoryList factories = providesMap.get(moduleOrFeature.getCanonicalName());
+        FactoryList factories = providesMap.get(moduleOrFeature.getTypeName());
         if (factories == null || !factories.allPushed()) {
           return false;
         }
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 d6db7157..3189e7bf 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -3,6 +3,7 @@ package io.avaje.inject.spi;
 import io.avaje.inject.BeanScope;
 import jakarta.inject.Provider;
 
+import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Optional;
 import java.util.Set;
@@ -38,7 +39,7 @@ public interface Builder {
    * @param name  The qualifier name
    * @param types The types that the bean implements and provides
    */
-  boolean isAddBeanFor(String name, Class<?>... types);
+  boolean isAddBeanFor(String name, Type... types);
 
   /**
    * Return true if the bean should be created and registered with the context.
@@ -48,7 +49,7 @@ public interface Builder {
    *
    * @param types The types that the bean implements and provides
    */
-  boolean isAddBeanFor(Class<?>... types);
+  boolean isAddBeanFor(Type... types);
 
   /**
    * Register the bean instance into the context.
@@ -112,6 +113,14 @@ public interface Builder {
    */
   <T> Provider<T> getProvider(Class<T> cls, String name);
 
+  /**
+   * Return Provider for a generic interface type.
+   *
+   * @param cls  The usual implementation class
+   * @param type The generic interface type
+   */
+  <T> Provider<T> getProviderFor(Class<?> cls, Type type);
+
   /**
    * Get a dependency.
    */
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 b85a87a5..9f713e07 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanMap.java
@@ -1,5 +1,6 @@
 package io.avaje.inject.spi;
 
+import java.lang.reflect.Type;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.List;
@@ -23,7 +24,7 @@ class DBeanMap {
   /**
    * Add to the map of entries.
    */
-  void addAll(Map<DContextEntryBean,DEntry> map) {
+  void addAll(Map<DContextEntryBean, DEntry> map) {
     for (Map.Entry<String, DContextEntry> entry : beans.entrySet()) {
       for (DContextEntryBean contentEntry : entry.getValue().entries()) {
         map.computeIfAbsent(contentEntry, dContextEntryBean -> contentEntry.entry()).addKey(entry.getKey());
@@ -43,24 +44,24 @@ class DBeanMap {
 
   @SuppressWarnings("rawtypes")
   private void addSuppliedBean(SuppliedBean supplied) {
-    Class<?> suppliedType = supplied.getType();
-    DContextEntryBean entryBean = DContextEntryBean.of(supplied.getBean(), supplied.name(), SUPPLIED);
-    beans.computeIfAbsent(suppliedType.getCanonicalName(), s -> new DContextEntry()).add(entryBean);
-    for (Class<?> anInterface : suppliedType.getInterfaces()) {
-      beans.computeIfAbsent(anInterface.getCanonicalName(), s -> new DContextEntry()).add(entryBean);
+    Type suppliedType = supplied.type();
+    DContextEntryBean entryBean = DContextEntryBean.of(supplied.bean(), supplied.name(), SUPPLIED);
+    beans.computeIfAbsent(suppliedType.getTypeName(), s -> new DContextEntry()).add(entryBean);
+    for (Class<?> anInterface : supplied.interfaces()) {
+      beans.computeIfAbsent(anInterface.getTypeName(), s -> new DContextEntry()).add(entryBean);
     }
   }
 
   void register(int flag, Object bean) {
     DContextEntryBean entryBean = DContextEntryBean.of(bean, nextBean.name, flag);
-    for (Class<?> type : nextBean.types) {
-      beans.computeIfAbsent(type.getCanonicalName(), s -> new DContextEntry()).add(entryBean);
+    for (Type type : nextBean.types) {
+      beans.computeIfAbsent(type.getTypeName(), s -> new DContextEntry()).add(entryBean);
     }
   }
 
   @SuppressWarnings("unchecked")
-  <T> T get(Class<T> type, String name) {
-    DContextEntry entry = beans.get(type.getCanonicalName());
+  <T> T get(Type type, String name) {
+    DContextEntry entry = beans.get(type.getTypeName());
     if (entry == null) {
       return null;
     }
@@ -70,18 +71,17 @@ class DBeanMap {
   /**
    * Return all bean instances matching the given type.
    */
-  @SuppressWarnings("rawtypes")
-  List<Object> all(Class type) {
-    DContextEntry entry = beans.get(type.getCanonicalName());
+  List<Object> all(Type type) {
+    DContextEntry entry = beans.get(type.getTypeName());
     return entry != null ? entry.all() : Collections.emptyList();
   }
 
   /**
    * Return true if there is a supplied bean for the name and types.
    */
-  boolean isSupplied(String qualifierName, Class<?>... types) {
+  boolean isSupplied(String qualifierName, Type... types) {
     if (types != null) {
-      for (Class<?> type : types) {
+      for (Type type : types) {
         if (isSuppliedType(qualifierName, type)) {
           return true;
         }
@@ -90,15 +90,15 @@ class DBeanMap {
     return false;
   }
 
-  private boolean isSuppliedType(String qualifierName, Class<?> type) {
-    DContextEntry entry = beans.get(type.getCanonicalName());
+  private boolean isSuppliedType(String qualifierName, Type type) {
+    DContextEntry entry = beans.get(type.getTypeName());
     return entry != null && entry.isSupplied(qualifierName);
   }
 
   /**
    * Store the qualifier name and type for the next bean to register.
    */
-  void nextBean(String name, Class<?>[] types) {
+  void nextBean(String name, Type[] types) {
     nextBean = new NextBean(name, types);
   }
 
@@ -111,9 +111,9 @@ class DBeanMap {
 
   static class NextBean {
     final String name;
-    final Class<?>[] types;
+    final Type[] types;
 
-    NextBean(String name, Class<?>[] types) {
+    NextBean(String name, Type[] types) {
       this.name = name;
       this.types = types;
     }
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
index 8de6d0b1..48c84795 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBeanScope.java
@@ -7,6 +7,7 @@ import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
 import java.util.*;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -51,17 +52,26 @@ class DBeanScope implements BeanScope {
   }
 
   @Override
-  public <T> T get(Class<T> beanClass) {
-    return get(beanClass, null);
+  public <T> T get(Class<T> type) {
+    return get(type, null);
   }
 
   @Override
-  public <T> T get(Class<T> beanClass, String name) {
-    final T bean = beans.get(beanClass, name);
+  public <T> T get(Class<T> type, String name) {
+    return getByType(type, name);
+  }
+
+  @Override
+  public <T> T get(Type type, String name) {
+    return getByType(type, name);
+  }
+
+  private  <T> T getByType(Type type, String name) {
+    final T bean = beans.get(type, name);
     if (bean != null) {
       return bean;
     }
-    return (parent == null) ? null : parent.get(beanClass, name);
+    return (parent == null) ? null : parent.get(type, name);
   }
 
   @SuppressWarnings("unchecked")
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 c4dcd71e..58f54f00 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -4,6 +4,7 @@ import io.avaje.inject.BeanEntry;
 import io.avaje.inject.BeanScope;
 import jakarta.inject.Provider;
 
+import java.lang.reflect.Type;
 import java.util.*;
 import java.util.function.Consumer;
 
@@ -32,7 +33,7 @@ class DBuilder implements Builder {
   /**
    * Debug of the current bean being wired - used in injection errors.
    */
-  private Class<?> injectTarget;
+  private Type injectTarget;
 
   /**
    * Flag set when we are running post construct injection.
@@ -44,22 +45,22 @@ class DBuilder implements Builder {
   }
 
   @Override
-  public boolean isAddBeanFor(Class<?>... types) {
+  public boolean isAddBeanFor(Type... types) {
     return isAddBeanFor(null, types);
   }
 
   @Override
-  public boolean isAddBeanFor(String name, Class<?>... types) {
+  public boolean isAddBeanFor(String name, Type... types) {
     next(name, types);
     return true;
   }
 
-  protected void next(String name, Class<?>... types) {
+  protected void next(String name, Type... types) {
     injectTarget = firstOf(types);
     beanMap.nextBean(name, types);
   }
 
-  private Class<?> firstOf(Class<?>[] types) {
+  private Type firstOf(Type[] types) {
     return types != null && types.length > 0 ? types[0] : null;
   }
 
@@ -78,12 +79,12 @@ class DBuilder implements Builder {
     return combine(values, parent.list(interfaceType));
   }
 
-  private <T> T getMaybe(Class<T> beanClass, String name) {
-    T bean = beanMap.get(beanClass, name);
+  private <T> T getMaybe(Type type, String name) {
+    T bean = beanMap.get(type, name);
     if (bean != null) {
       return bean;
     }
-    return (parent == null) ? null : parent.get(beanClass, name);
+    return (parent == null) ? null : parent.get(type, name);
   }
 
   /**
@@ -166,6 +167,21 @@ class DBuilder implements Builder {
     return promise;
   }
 
+  @Override
+  public <T> Provider<T> getProviderFor(Class<?> cls, Type type) {
+    return () -> {
+      T bean = getMaybe(cls, null);
+      if (bean == null) {
+        bean = getMaybe(type, null);
+      }
+      if (bean != null) {
+        return bean;
+      }
+      String msg = "Unable to inject an instance for generic type " + type + " usually provided by " + cls + "?";
+      throw new IllegalStateException(msg);
+    };
+  }
+
   @Override
   public <T> T get(Class<T> cls) {
     return get(cls, null);
diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
index ee7f9936..e8d3323c 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilderExtn.java
@@ -2,6 +2,7 @@ package io.avaje.inject.spi;
 
 import io.avaje.inject.BeanScope;
 
+import java.lang.reflect.Type;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -31,7 +32,7 @@ class DBuilderExtn extends DBuilder {
   }
 
   @Override
-  public boolean isAddBeanFor(String qualifierName, Class<?>... types) {
+  public boolean isAddBeanFor(String qualifierName, Type... types) {
     next(qualifierName, types);
     if (hasSuppliedBeans) {
       return !beanMap.isSupplied(qualifierName, types);
@@ -48,7 +49,7 @@ class DBuilderExtn extends DBuilder {
     if (enrich != null) {
       return enrich.enrich(bean);
     }
-    for (Class<?> type : next.types) {
+    for (Type type : next.types) {
       enrich = enrichLookup(type, next.name);
       if (enrich != null) {
         return enrich.enrich(bean);
@@ -59,7 +60,7 @@ class DBuilderExtn extends DBuilder {
   }
 
   @SuppressWarnings({"unchecked"})
-  private <T> EnrichBean<T> enrichLookup(Class<?> type, String name) {
+  private <T> EnrichBean<T> enrichLookup(Type type, String name) {
     EnrichBean<T> enrich = enrichMap.get(KeyUtil.key(type, null));
     if (enrich == null && name != null) {
       enrich = enrichMap.get(KeyUtil.key(type, name));
diff --git a/inject/src/main/java/io/avaje/inject/spi/DEntry.java b/inject/src/main/java/io/avaje/inject/spi/DEntry.java
index de948601..ebafa0ab 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DEntry.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DEntry.java
@@ -49,6 +49,6 @@ class DEntry implements BeanEntry {
 
   @Override
   public boolean hasKey(Class<?> type) {
-    return keys.contains(type.getCanonicalName());
+    return keys.contains(type.getTypeName());
   }
 }
diff --git a/inject/src/main/java/io/avaje/inject/spi/GenericType.java b/inject/src/main/java/io/avaje/inject/spi/GenericType.java
new file mode 100644
index 00000000..00d2bb1f
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/GenericType.java
@@ -0,0 +1,55 @@
+package io.avaje.inject.spi;
+
+import java.lang.reflect.Type;
+
+/**
+ * Represents a full type including generics declaration, to avoid information loss due to type erasure.
+ * <p>
+ * This is a cut down version of Helidon GenericType Apache 2 licence.
+ *
+ * @param <T> the generic type parameter
+ */
+public class GenericType<T> implements Type {
+
+  private final Type type;
+
+  /**
+   * Constructs a new generic type, deriving the generic type and class from type parameter.
+   */
+  protected GenericType() throws IllegalArgumentException {
+    this.type = GenericTypeUtil.typeArgument(getClass());
+  }
+
+  /**
+   * Return the type represented by this generic type instance.
+   */
+  public Type type() {
+    return type;
+  }
+
+  @Override
+  public String getTypeName() {
+    return type.toString();
+  }
+
+  @Override
+  public int hashCode() {
+    return type.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) {
+      return true;
+    }
+    if (obj instanceof GenericType) {
+      return ((GenericType<?>) obj).type.equals(this.type);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return type.toString();
+  }
+}
diff --git a/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java b/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java
new file mode 100644
index 00000000..66dcc514
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/spi/GenericTypeUtil.java
@@ -0,0 +1,62 @@
+package io.avaje.inject.spi;
+
+import java.lang.reflect.*;
+import java.util.Arrays;
+import java.util.Stack;
+
+/**
+ * Cut down version of GenericTypeUtil from Helidon project Apache 2 license.
+ */
+final class GenericTypeUtil {
+
+  /**
+   * Return the Type for type parameter of {@code GenericType<T>}.
+   */
+  static Type typeArgument(Class<?> clazz) {
+    // collect superclasses
+    Stack<Type> superclasses = new Stack<>();
+    Type currentType;
+    Class<?> currentClass = clazz;
+    do {
+      currentType = currentClass.getGenericSuperclass();
+      superclasses.push(currentType);
+      if (currentType instanceof Class) {
+        currentClass = (Class<?>) currentType;
+      } else if (currentType instanceof ParameterizedType) {
+        currentClass = (Class<?>) ((ParameterizedType) currentType).getRawType();
+      }
+      if (currentClass.equals(Object.class)) {
+        break;
+      }
+      if (currentClass.equals(GenericType.class)) {
+        break;
+      }
+    } while (true);
+
+    // find which one supplies type argument and return it
+    TypeVariable<?> tv = ((Class<?>) GenericType.class).getTypeParameters()[0];
+    while (!superclasses.isEmpty()) {
+      currentType = superclasses.pop();
+      if (currentType instanceof ParameterizedType) {
+        ParameterizedType pt = (ParameterizedType) currentType;
+        Class<?> rawType = (Class<?>) pt.getRawType();
+        int argIndex = Arrays.asList(rawType.getTypeParameters()).indexOf(tv);
+        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
+            tv = (TypeVariable<?>) typeArg;
+            continue;
+          } else {
+            // found the value - return it
+            return typeArg;
+          }
+        }
+      }
+      // needed type argument not supplied - break and throw exception
+      break;
+    }
+    throw new IllegalArgumentException(currentType + " does not specify the type parameter T of GenericType<T>");
+  }
+
+}
diff --git a/inject/src/main/java/io/avaje/inject/spi/KeyUtil.java b/inject/src/main/java/io/avaje/inject/spi/KeyUtil.java
index fe3fc7c2..f43b41c5 100644
--- a/inject/src/main/java/io/avaje/inject/spi/KeyUtil.java
+++ b/inject/src/main/java/io/avaje/inject/spi/KeyUtil.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.spi;
 
+import java.lang.reflect.Type;
+
 /**
  * Build a "flat key" combining type and name.
  */
@@ -9,8 +11,8 @@ class KeyUtil {
     return name == null ? null : name.toLowerCase();
   }
 
-  static String key(Class<?> type, String name) {
-    return name == null ? type.getCanonicalName() : type.getCanonicalName() + "|" + name;
+  static String key(Type type, String name) {
+    return name == null ? type.getTypeName() : type.getTypeName() + "|" + name;
   }
 
 }
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 764ff826..acc1bfb0 100644
--- a/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/SuppliedBean.java
@@ -3,72 +3,144 @@ package io.avaje.inject.spi;
 import org.mockito.Mockito;
 
 import jakarta.inject.Named;
+
+import java.lang.reflect.Type;
 import java.util.function.Consumer;
 
 /**
  * Holds beans supplied to the dependency injection.
  * <p>
- * These are typically test doubles or mock instances that we supply in testing.
- * When we supply bean instances they take precedence over other beans that
- * would normally be injected.
- * </p>
+ * These can be externally supplied dependencies or test doubles for testing purposes.
  */
-public class SuppliedBean<B> {
-
-  private final String name;
+public abstract class SuppliedBean<B> {
 
-  private final Class<B> type;
+  protected final String name;
+  protected final Type type;
+  protected B bean;
 
-  private final Consumer<B> consumer;
+  /**
+   * Create with a class type and bean instance.
+   */
+  @SuppressWarnings({"rawtypes", "unchecked"})
+  public static SuppliedBean of(Class<?> type, Object bean) {
+    return new SuppliedBean.ForClass(null, type, bean, null);
+  }
 
-  private B bean;
+  /**
+   * Create for a class type with a consumer that runs once when the bean is obtained.
+   */
+  public static <B> SuppliedBean<B> of(String name, Class<B> type, B bean, Consumer<B> consumer) {
+    return new SuppliedBean.ForClass<>(name, type, bean, consumer);
+  }
 
   /**
-   * Create with a given target type and bean instance.
+   * Create for a class type with name.
    */
-  public SuppliedBean(String name, Class<B> type, B bean) {
-    this(name, type, bean, null);
+  public static <B> SuppliedBean<B> of(String name, Class<B> type, B bean) {
+    return new SuppliedBean.ForClass<>(name, type, bean, null);
   }
 
   /**
-   * Create with a consumer to setup the mock.
+   * Create a supplied bean for a generic type.
    */
-  public SuppliedBean(String name, Class<B> type, B bean, Consumer<B> consumer) {
+  public static <B> SuppliedBean<B> ofType(String name, Type type, B bean) {
+    return new SuppliedBean.ForType<>(name, type, bean);
+  }
+
+  private SuppliedBean(String name, Type type, B bean) {
     this.name = name;
     this.type = type;
     this.bean = bean;
-    this.consumer = consumer;
+  }
+
+  private SuppliedBean(String name, Class<B> type, B bean) {
+    this.name = initName(name, type);
+    this.type = type;
+    this.bean = bean;
+  }
+
+
+  String initName(String name, Class<?> type) {
+    if (name != null) {
+      return name;
+    }
+    Named annotation = type.getAnnotation(Named.class);
+    return (annotation == null) ? null : annotation.value();
   }
 
   /**
    * Return the dependency injection target type.
    */
-  public Class<B> getType() {
+  public Type type() {
     return type;
   }
 
   /**
-   * Return the bean instance to use (often a test double or mock).
+   * Return the qualifier name of the supplied bean.
+   */
+  public String name() {
+    return name;
+  }
+
+  /**
+   * Return the bean instance to use for injection.
    */
-  public B getBean() {
-    if (bean == null) {
-      // should extract a SPI for this
-      bean = Mockito.mock(type);
+  public abstract B bean();
+
+  /**
+   * Return the interfaces to additionally register along with the type.
+   */
+  public abstract Class<?>[] interfaces();
+
+  /**
+   * Type based supplied bean.
+   */
+  private static class ForType<B> extends SuppliedBean<B> {
+
+    private ForType(String name, Type type, B bean) {
+      super(name, type, bean);
     }
-    if (consumer != null) {
-      consumer.accept(bean);
+
+    @Override
+    public B bean() {
+      return bean;
+    }
+
+    @Override
+    public Class<?>[] interfaces() {
+      return new Class[0];
     }
-    return bean;
   }
 
   /**
-   * Return the qualifier name of the supplied bean.
+   * Class based supplied bean.
    */
-  public String name() {
-    if (name != null) {
-      return name;
+  private static class ForClass<B> extends SuppliedBean<B> {
+
+    private final Consumer<B> consumer;
+    private final Class<B> classType;
+
+    ForClass(String name, Class<B> type, B bean, Consumer<B> consumer) {
+      super(name, type, bean);
+      this.classType = type;
+      this.consumer = consumer;
+    }
+
+    @Override
+    public B bean() {
+      if (bean == null) {
+        // should extract a SPI for this
+        bean = Mockito.mock(classType);
+      }
+      if (consumer != null) {
+        consumer.accept(bean);
+      }
+      return bean;
+    }
+
+    @Override
+    public Class<?>[] interfaces() {
+      return classType.getInterfaces();
     }
-    Named annotation = type.getAnnotation(Named.class);
-    return (annotation == null) ? null : annotation.value();
   }
 }


Test patch:
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
index adfcfd44..0200714a 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/GenericTypeTest.java
@@ -75,6 +75,12 @@ public class GenericTypeTest {
     assertThat(type.getParams().get(1).getParams().get(0).getMainType()).isEqualTo("java.util.UUID");
   }
 
+  @Test
+  public void shortName() {
+    assertThat(GenericType.parse("java.lang.List<Foo>").shortName()).isEqualTo("ListFoo");
+    assertThat(GenericType.parse("java.lang.List<org.Foo<com.Bar>>").shortName()).isEqualTo("ListFooBar");
+  }
+
   @Test
   public void write() {
 
diff --git a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
index 9382b138..6c51af5b 100644
--- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java
@@ -3,6 +3,8 @@ package org.example.coffee;
 import io.avaje.inject.BeanEntry;
 import io.avaje.inject.BeanScope;
 import org.example.coffee.core.DuperPump;
+import org.example.coffee.generic.HazRepo;
+import org.example.coffee.generic.HazRepo$DI;
 import org.example.coffee.list.BSomei;
 import org.example.coffee.list.Somei;
 import org.example.coffee.provider.AProv;
@@ -13,6 +15,7 @@ import org.example.iface.IfaseBase;
 import org.example.inherit.*;
 import org.junit.jupiter.api.Test;
 
+import java.lang.reflect.Type;
 import java.util.List;
 import java.util.Optional;
 
@@ -64,7 +67,7 @@ public class CoffeeMakerTest {
 
       final BeanEntry entry = bsomeEntry.get();
       assertThat(entry.qualifierName()).isEqualTo("b");
-      assertThat(entry.keys()).containsExactly(can(BSomei.class), can(Somei.class));
+      assertThat(entry.keys()).containsExactly(name(BSomei.class), name(Somei.class));
       assertThat(entry.type()).isEqualTo(BSomei.class);
       assertThat(entry.priority()).isEqualTo(0);
       assertThat(entry.bean()).isEqualTo(context.get(Somei.class, "b"));
@@ -83,8 +86,8 @@ public class CoffeeMakerTest {
         .findFirst().orElse(null);
 
       assertThat(inhEntry.keys())
-        .containsExactly(can(InhOne.class), can(InhBase.class), can(InhBaseBase.class),
-          can(InhBaseIface2.class), can(InhBaseIface3.class), can(InhBaseIface.class));
+        .containsExactly(name(InhOne.class), name(InhBase.class), name(InhBaseBase.class),
+          name(InhBaseIface2.class), name(InhBaseIface3.class), name(InhBaseIface.class));
     }
   }
 
@@ -99,7 +102,22 @@ public class CoffeeMakerTest {
         .findFirst().orElse(null);
 
       assertThat(extendIfaces.keys())
-        .containsExactly(can(ConcreteExtend.class), can(IfaceExtend.class), can(IfaseBase.class));
+        .containsExactly(name(ConcreteExtend.class), name(IfaceExtend.class), name(IfaseBase.class));
+    }
+  }
+
+  @Test
+  public void beanScope_all_includesGenericInterfaces() {
+    try (BeanScope context = BeanScope.newBuilder().build()) {
+
+      final List<BeanEntry> beanEntries = context.all();
+
+      final BeanEntry hazRepo = beanEntries.stream()
+        .filter(e -> e.hasKey(HazRepo.class))
+        .findFirst().orElse(null);
+
+      assertThat(hazRepo.keys())
+        .containsExactly(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong));
     }
   }
 
@@ -114,12 +132,12 @@ public class CoffeeMakerTest {
         .findFirst().orElse(null);
 
       assertThat(extendIfaces.keys())
-        .containsExactly(can(AProvProvider.class), can(AProv.class));
+        .containsExactly(name(AProvProvider.class), name(AProv.class));
     }
   }
 
-  String can(Class<?> cls) {
-    return cls.getCanonicalName();
+  String name(Type cls) {
+    return cls.getTypeName();
   }
 
 }
diff --git a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
index 911169cf..5508004b 100644
--- a/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
+++ b/inject-test/src/test/java/org/example/coffee/generic/HazManagerTest.java
@@ -12,7 +12,6 @@ public class HazManagerTest {
 
   @Test
   public void find_when_allWired() {
-
     HazManager hazManager = ApplicationScope.get(HazManager.class);
     Haz haz = hazManager.find(42L);
 
@@ -20,8 +19,7 @@ public class HazManagerTest {
   }
 
   @Test
-  public void fin_with_mockHaz() {
-
+  public void find_with_mockHaz() {
     try (BeanScope context = BeanScope.newBuilder()
       .forTesting()
       .withMock(HazRepo.class)
@@ -36,7 +34,6 @@ public class HazManagerTest {
 
   @Test
   public void find_with_stubHazUsingMockito() {
-
     try (BeanScope context = BeanScope.newBuilder()
       .forTesting()
       .withMock(HazRepo.class, hazRepo -> {
@@ -52,8 +49,22 @@ public class HazManagerTest {
   }
 
   @Test
-  public void find_with_testDouble() {
+  public void withBean_usingGenericType() {
+    TDFoo testDouble = new TDFoo();
+
+    try (BeanScope context = BeanScope.newBuilder()
+      .withBean(HazRepo$DI.TYPE_RepositoryHazLong, testDouble)
+      .build()) {
+
+      HazManager hazManager = context.get(HazManager.class);
+      Haz haz = hazManager.find(42L);
 
+      assertThat(haz.id).isEqualTo(7L);
+    }
+  }
+
+  @Test
+  public void find_with_testDouble() {
     TDHazRepo testDouble = new TDHazRepo();
 
     try (BeanScope context = BeanScope.newBuilder()
@@ -76,6 +87,14 @@ public class HazManagerTest {
     }
   }
 
+  static class TDFoo implements Repository<Haz, Long> {
+
+    @Override
+    public Haz findById(Long id) {
+      return new Haz(7L);
+    }
+  }
+
   /**
    * Test double for HazRepo - nice when we want interesting test behaviour.
    */
diff --git a/inject/src/test/java/io/avaje/inject/spi/GenericTypeTest.java b/inject/src/test/java/io/avaje/inject/spi/GenericTypeTest.java
new file mode 100644
index 00000000..2503d11f
--- /dev/null
+++ b/inject/src/test/java/io/avaje/inject/spi/GenericTypeTest.java
@@ -0,0 +1,63 @@
+package io.avaje.inject.spi;
+
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.List;
+import java.util.Set;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class GenericTypeTest {
+
+  @Test
+  void testNoSubclass() {
+    assertThrows(IllegalArgumentException.class, GenericType::new);
+  }
+
+  @Test
+  void testNoTypeArguments() {
+    assertThrows(IllegalArgumentException.class, NoTypeArgs::new);
+  }
+
+  @Test
+  void getTypeName() {
+    Type type2 = new GenericType<List<String>>() {};
+    assertThat(type2.getTypeName()).isEqualTo("java.util.List<java.lang.String>");
+  }
+
+  @Test
+  void typeArguments() {
+    GenericType<String> type = new TypeArgs();
+
+    assertThat(type.type()).isEqualTo(String.class);
+  }
+
+  @Test
+  void genericTypeList() {
+    GenericType<Set<List<String>>> type = new GenericType<Set<List<String>>>() {};
+
+    Type reflectType = type.type();
+    assertThat(reflectType).isInstanceOf(ParameterizedType.class);
+
+    ParameterizedType pType = (ParameterizedType) reflectType;
+
+    assertThat(pType.getRawType()).isEqualTo(Set.class);
+
+    Type secondType = pType.getActualTypeArguments()[0];
+    assertThat(secondType).isInstanceOf(ParameterizedType.class);
+    pType = (ParameterizedType) secondType;
+    assertThat(pType.getRawType()).isEqualTo(List.class);
+    assertThat(pType.getActualTypeArguments()[0]).isEqualTo(String.class);
+  }
+
+  private static class TypeArgs extends GenericType<String> {
+
+  }
+
+  private static class NoTypeArgs<T> extends GenericType<T> {
+
+  }
+}