Repository: avaje/avaje-inject
Pull request #498 (2024-02-11 19:11:49)

Support Generic Auto Provides/Requires
-  Changes the `Module` interface to use `Type[]` instead of `Class<?>[]` 
- Updates generator to allow generic types to be added to auto  provides/requires  via `GenericType`
- updates the maven/gradle plugin to use `Type[]` instead of `Class<?>[]`

Now we can have a generic (factory) bean in Module A:

```java
@Component
public class OtherComponentGeneric implements Supplier<String> {

  @Override
  public String get() {
    return "hello";
  }
}
```

The generated Avaje Module class will have:

```java
public final class ModuleClass implements Module {
// ...rest of the module class
  @Override
  public Type[] autoProvides() {
    return autoProvides;
  }
  private static final Type[] autoProvides = {
    new GenericType<java.util.function.Supplier<java.lang.String>>(){},
    OtherComponentGeneric.class,
  };
// ...rest of the module class
```

and in Module B we can reference via:

```java
@Component
public class HasExternalDependencies {

  public final Supplier<String> stringSupplier;

  public HasExternalDependencies(
      Supplier<String> stringSupplier) {
    this.stringSupplier = stringSupplier;
  }
}
```

rbygrave: Did somebody hit a limitation here / was there a requirement for this?  

The autoProvides is for wiring across modules. I tend to believe that cross-module dependencies should be interfaces and I tend to think they shouldn't use generic types. Has someone hit an issue here?

SentryMan: Is it so inconceivable that a module would provide generic types? I've got a mongo common library that handles authentication and provides some common `MongoCollection` beans. Currently, I have to wrap the generic types in a non-generic wrapper class to wire across modules. 
SentryMan: Factory beans that return `Map`s are also not uncommon. Overall, wrapping generic types to get them to wire across modules is quite a hassle.
SentryMan: > Noting that this is a breaking change - it would need a bump to the major version

Surprisingly, it doesn't break anything except the plugins. `Class<?>` extends `Type`  so modules generated before this change will work as expected.
SentryMan: > Can you share an example?

an example of wrapping?
In module A:
```java
@Singleton
public record WrapperClass(Map<String, String> map){}
```

Then you can inject WrapperClass into a bean in some module B.
rob-bygrave: > an example of wrapping?

I meant a _real world_ example - as in, we are using tight coupling between 2 modules with a **_concrete_** class (and not an interface).  What is real world example where this is a good idea (versus a bad idea because that is tight coupling between modules that uses an implementation - a non-abstract implementation class to coupling 2 modules together ... and I'm going _"Really??"_).


SentryMan: > modules with a **_concrete_** class (and not an interface)

I don't follow, the point of this PR is that we can avoid concrete wrapper classes and use the generic interface/abstract classes
SentryMan: > modules with a concrete class (and not an interface)

I did think of an example though. Some time ago I was helping out a guy on discord with avaje and he was quite confused about the current behavior, he had a common library that would configure a Jackson `ObjectMapper` for his other applications.

The problem was that ObjectMapper is provided as 
```java
public final class ModuleClass implements Module {
// ...rest of the module class
  @Override
  public Class<?>[] autoProvides() {
    return autoProvides;
  }
  private static final  Class<?>[] autoProvides = {
    Versioned.class,
  };
```
so naturally his compilation would fail. I ended up telling him he had to manually use `@InjectModule(provides=ObjectMapper.class)` to get it to work.

Patch:
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 277cf4df..98e91d69 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -33,6 +33,6 @@ jobs:
     - name: Maven version
       run: mvn --version
     - name: Build with Maven
-      run: mvn clean test
+      run: mvn clean install
     - name: Build with Maven (javax)
       run: ./jakarta-to-javax.sh && mvn clean test && ./javax-to-jakarta.sh
diff --git a/blackbox-other/src/main/java/org/other/one/OtherComponentGeneric.java b/blackbox-other/src/main/java/org/other/one/OtherComponentGeneric.java
new file mode 100644
index 00000000..4c656893
--- /dev/null
+++ b/blackbox-other/src/main/java/org/other/one/OtherComponentGeneric.java
@@ -0,0 +1,14 @@
+package org.other.one;
+
+import java.util.function.Supplier;
+
+import io.avaje.inject.Component;
+
+@Component
+public class OtherComponentGeneric implements Supplier<String> {
+
+  @Override
+  public String get() {
+    return "hello";
+  }
+}
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 f014d64c..24a07748 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
@@ -158,11 +158,11 @@ final class BeanReader {
       }
     }
     conditions.requireTypes.stream()
-      .map(t -> new Dependency("con:" + t))
+      .map(t -> new Dependency(Constants.CONDITIONAL_DEPENDENCY + t))
       .forEach(list::add);
     conditions.missingTypes.stream()
       .filter(t -> !t.equals(type))
-      .map(t -> new Dependency("con:" + t))
+      .map(t -> new Dependency(Constants.CONDITIONAL_DEPENDENCY + t))
       .forEach(list::add);
     return list;
   }
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 b6f54a35..6c3d384d 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
@@ -54,4 +54,7 @@ final class Constants {
   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";
+
+  static final String CONDITIONAL_DEPENDENCY = "con:";
+  static final String SOFT_DEPENDENCY = "soft:";
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
index ec924793..d5109efb 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Dependency.java
@@ -1,5 +1,8 @@
 package io.avaje.inject.generator;
 
+import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY;
+import static io.avaje.inject.generator.Constants.SOFT_DEPENDENCY;
+
 final class Dependency {
 
   private final String name;
@@ -8,11 +11,11 @@ final class Dependency {
 
   Dependency(String name) {
     String nameStr;
-    if (name.startsWith("soft:")) {
+    if (name.startsWith(SOFT_DEPENDENCY)) {
       this.softDependency = true;
       this.conditionalDependency = false;
       nameStr = ProcessorUtils.trimAnnotations(name.substring(5));
-    } else if (name.startsWith("con:")) {
+    } else if (name.startsWith(CONDITIONAL_DEPENDENCY)) {
       this.softDependency = true;
       this.conditionalDependency = true;
       nameStr = ProcessorUtils.trimAnnotations(name.substring(4));
@@ -32,7 +35,7 @@ final class Dependency {
 
   @Override
   public String toString() {
-    return softDependency ? "soft:" + name : name;
+    return softDependency ? SOFT_DEPENDENCY + name : name;
   }
 
   String name() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
index 325e38a2..3eb61904 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java
@@ -64,14 +64,14 @@ final class ExternalProvider {
       try {
         final var module = iterator.next();
         System.out.println("Detected Module: " + module.getClass().getCanonicalName());
-        for (final Class<?> provide : module.provides()) {
-          providedTypes.add(provide.getCanonicalName());
+        for (final var provide : module.provides()) {
+          providedTypes.add(provide.getTypeName());
         }
-        for (final Class<?> provide : module.autoProvides()) {
-          providedTypes.add(provide.getCanonicalName());
+        for (final var provide : module.autoProvides()) {
+          providedTypes.add(provide.getTypeName());
         }
-        for (final Class<?> provide : module.autoProvidesAspects()) {
-          providedTypes.add(Util.wrapAspect(provide.getCanonicalName()));
+        for (final var provide : module.autoProvidesAspects()) {
+          providedTypes.add(Util.wrapAspect(provide.getTypeName()));
         }
       } catch (final ServiceConfigurationError expected) {
         // ignore expected error reading the module that we are also writing
@@ -99,11 +99,11 @@ final class ExternalProvider {
         continue;
       }
       APContext.logNote("Loaded Plugin: " + plugin.getClass().getCanonicalName());
-      for (final Class<?> provide : plugin.provides()) {
-        defaultScope.pluginProvided(provide.getCanonicalName());
+      for (final var provide : plugin.provides()) {
+        defaultScope.pluginProvided(provide.getTypeName());
       }
-      for (final Class<?> provide : plugin.providesAspects()) {
-        defaultScope.pluginProvided(Util.wrapAspect(provide.getCanonicalName()));
+      for (final var provide : plugin.providesAspects()) {
+        defaultScope.pluginProvided(Util.wrapAspect(provide.getTypeName()));
       }
     }
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
index b6325f49..b982809e 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java
@@ -9,7 +9,7 @@ import java.util.List;
 import java.util.Set;
 
 import static io.avaje.inject.generator.ProcessingContext.asElement;
-
+import static io.avaje.inject.generator.Constants.*;
 
 final class MethodReader {
 
@@ -139,11 +139,11 @@ final class MethodReader {
     dependsOn.add(factoryType);
 
     conditions.requireTypes.stream()
-      .map(t -> "con:" + t)
+      .map(t -> CONDITIONAL_DEPENDENCY + t)
       .forEach(dependsOn::add);
     conditions.missingTypes.stream()
       .filter(t -> !t.equals(returnTypeRaw))
-      .map(t -> "con:" + t)
+      .map(t -> CONDITIONAL_DEPENDENCY + t)
       .forEach(dependsOn::add);
     for (final MethodParam param : params) {
       dependsOn.add(Util.trimWildcard(param.paramType));
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 17bce9a4..7c70ef2c 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
@@ -404,10 +404,15 @@ final class ScopeInfo {
 
   private void buildProvidesMethod(Append writer, String fieldName, Set<String> types) {
     writer.append("  @Override").eol();
-    writer.append("  public Class<?>[] %s() {\n    return %s;\n  }", fieldName, fieldName).eol();
-    writer.append("  private final Class<?>[] %s = {", fieldName).eol();
+    final var arrayType = fieldName.contains("Aspects") ? "Class<?>" : "Type";
+    writer.append("  public %s[] %s() {\n    return %s;\n  }", arrayType, fieldName, fieldName).eol();
+    writer.append("  private final %s[] %s = {", arrayType, fieldName).eol();
     for (final String rawType : types) {
-      writer.append("    %s.class,", trimGenerics(rawType)).eol();
+      if (rawType.contains("<")) {
+        writer.append("    new GenericType<%s>(){},", rawType).eol();
+      } else {
+        writer.append("    %s.class,", trimGenerics(rawType)).eol();
+      }
     }
     writer.append("  };").eol().eol();
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
index d16d6319..00c91108 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleModuleWriter.java
@@ -105,7 +105,7 @@ final class SimpleModuleWriter {
         autoProvidesAspects.add(aspect);
       }
       final var forExternal = metaData.autoProvides();
-      if (forExternal != null && !forExternal.isEmpty() && !forExternal.contains("<")) {
+      if (forExternal != null && !forExternal.isEmpty()) {
         autoProvides.addAll(forExternal);
       }
     }
@@ -129,7 +129,7 @@ final class SimpleModuleWriter {
     Set<String> allClasses = distinctPublicClasses();
     writer.append("  @Override").eol();
     writer.append("  public Class<?>[] classes() {").eol();
-    writer.append("    return new Class<?>[]{").eol();
+    writer.append("    return new Class<?>[] {").eol();
     for (String rawType : new TreeSet<>(allClasses)) {
       writer.append("      %s.class,", rawType).eol();
     }
@@ -195,6 +195,8 @@ final class SimpleModuleWriter {
   private Set<String> factoryImportTypes() {
     Set<String> importTypes = new TreeSet<>();
     importTypes.add(Constants.GENERATED);
+    importTypes.add(Constants.GENERICTYPE);
+    importTypes.add("java.lang.reflect.Type");
     importTypes.add(Constants.BEANSCOPE);
     importTypes.add(Constants.INJECTMODULE);
     importTypes.add(Constants.DEPENDENCYMETA);
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
index cc02d3c1..9bf8f7ba 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java
@@ -46,6 +46,7 @@ final class TypeExtendsReader {
     this.baseType = baseType;
     this.extendsInjection = new TypeExtendsInjection(baseType, factory, importTypes);
     this.beanSimpleName = baseType.getSimpleName().toString();
+
     this.baseTypeIsInterface = baseType.getKind() == ElementKind.INTERFACE;
     this.publicAccess = baseType.getModifiers().contains(Modifier.PUBLIC);
     this.autoProvide = autoProvide();
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 f0689df8..24202434 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
@@ -2,6 +2,8 @@ package io.avaje.inject.generator;
 
 import javax.lang.model.element.Element;
 import javax.lang.model.element.TypeElement;
+import javax.lang.model.type.TypeKind;
+
 import java.util.List;
 import java.util.Set;
 
@@ -44,6 +46,7 @@ final class TypeReader {
 
   List<String> autoProvides() {
     return extendsReader.autoProvides().stream()
+        .filter(u -> u.componentTypes().stream().noneMatch(p -> p.kind() == TypeKind.TYPEVAR))
         .map(UType::full)
         .collect(toList());
   }
diff --git a/inject-gradle-plugin/build.gradle b/inject-gradle-plugin/build.gradle
index 67f1ca9f..7e9cbeee 100644
--- a/inject-gradle-plugin/build.gradle
+++ b/inject-gradle-plugin/build.gradle
@@ -6,7 +6,7 @@ plugins {
 }
 
 group 'io.avaje.inject'
-version '0.3'
+version '0.4'
 
 repositories {
   mavenLocal()
@@ -14,7 +14,7 @@ repositories {
 }
 
 dependencies {
-  implementation 'io.avaje:avaje-inject:9.4-RC1'
+  implementation 'io.avaje:avaje-inject:9.13-SNAPSHOT'
   implementation gradleApi()
 
   testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
diff --git a/inject-gradle-plugin/src/main/java/io/avaje/inject/plugin/AvajeInjectPlugin.java b/inject-gradle-plugin/src/main/java/io/avaje/inject/plugin/AvajeInjectPlugin.java
index 11ead28f..121f19c5 100644
--- a/inject-gradle-plugin/src/main/java/io/avaje/inject/plugin/AvajeInjectPlugin.java
+++ b/inject-gradle-plugin/src/main/java/io/avaje/inject/plugin/AvajeInjectPlugin.java
@@ -56,10 +56,10 @@ public class AvajeInjectPlugin implements Plugin<Project> {
 
     for (final var plugin : ServiceLoader.load(io.avaje.inject.spi.Plugin.class, cl)) {
       System.out.println("Loaded Plugin: " + plugin.getClass().getCanonicalName());
-      for (final Class<?> provide : plugin.provides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : plugin.provides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : plugin.providesAspects()) {
+      for (final var provide : plugin.providesAspects()) {
         providedTypes.add(wrapAspect(provide.getCanonicalName()));
       }
     }
@@ -74,13 +74,13 @@ public class AvajeInjectPlugin implements Plugin<Project> {
     final Set<String> providedTypes = new HashSet<>();
     for (final io.avaje.inject.spi.Module module : ServiceLoader.load(io.avaje.inject.spi.Module.class, classLoader)) {
       System.out.println("Detected External Module: " + module.getClass().getCanonicalName());
-      for (final Class<?> provide : module.provides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : module.provides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : module.autoProvides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : module.autoProvides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : module.autoProvidesAspects()) {
+      for (final var provide : module.autoProvidesAspects()) {
         providedTypes.add(wrapAspect(provide.getCanonicalName()));
       }
     }
diff --git a/inject-maven-plugin/pom.xml b/inject-maven-plugin/pom.xml
index dbdc5228..731cef76 100644
--- a/inject-maven-plugin/pom.xml
+++ b/inject-maven-plugin/pom.xml
@@ -10,7 +10,7 @@
   <groupId>io.avaje</groupId>
   <artifactId>avaje-inject-maven-plugin</artifactId>
   <packaging>maven-plugin</packaging>
-  <version>1.2</version>
+  <version>1.3</version>
 
   <dependencies>
     <dependency>
@@ -29,7 +29,7 @@
     <dependency>
       <groupId>io.avaje</groupId>
       <artifactId>avaje-inject</artifactId>
-      <version>9.4-RC2</version>
+      <version>9.13-SNAPSHOT</version>
     </dependency>
   </dependencies>
 
diff --git a/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java b/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
index 1653881f..2e2abad4 100644
--- a/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
+++ b/inject-maven-plugin/src/main/java/io/avaje/inject/mojo/AutoProvidesMojo.java
@@ -90,10 +90,10 @@ public class AutoProvidesMojo extends AbstractMojo {
     for (final var plugin : ServiceLoader.load(Plugin.class, newClassLoader)) {
       log.info("Loaded Plugin: " + plugin.getClass().getCanonicalName());
 
-      for (final Class<?> provide : plugin.provides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : plugin.provides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : plugin.providesAspects()) {
+      for (final var provide : plugin.providesAspects()) {
         providedTypes.add(wrapAspect(provide.getCanonicalName()));
       }
     }
@@ -111,13 +111,13 @@ public class AutoProvidesMojo extends AbstractMojo {
     for (final var module : ServiceLoader.load(Module.class, newClassLoader)) {
       log.info("Detected External Module: " + module.getClass().getCanonicalName());
 
-      for (final Class<?> provide : module.provides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : module.provides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : module.autoProvides()) {
-        providedTypes.add(provide.getCanonicalName());
+      for (final var provide : module.autoProvides()) {
+        providedTypes.add(provide.getTypeName());
       }
-      for (final Class<?> provide : module.autoProvidesAspects()) {
+      for (final var provide : module.autoProvidesAspects()) {
         providedTypes.add(wrapAspect(provide.getCanonicalName()));
       }
     }
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 550e64a3..c5b5482a 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -335,8 +335,8 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       }
     }
 
-    private void addFactoryProvides(FactoryState factoryState, Class<?>[] provides) {
-      for (final Class<?> feature : provides) {
+    private void addFactoryProvides(FactoryState factoryState, Type[] provides) {
+      for (final var feature : provides) {
         providesMap.computeIfAbsent(feature.getTypeName(), s -> new FactoryList()).add(factoryState);
       }
     }
@@ -394,8 +394,8 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       }
     }
 
-    private void unsatisfiedRequires(StringBuilder sb, Class<?>[] requiredType, String requires) {
-      for (final Class<?> depModuleName : requiredType) {
+    private void unsatisfiedRequires(StringBuilder sb, Type[] requiredType, String requires) {
+      for (final var depModuleName : requiredType) {
         if (notProvided(depModuleName.getTypeName())) {
           sb.append(String.format(" %s [%s]", requires, depModuleName.getTypeName()));
         }
@@ -438,8 +438,8 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
         && satisfiedDependencies(factory.autoRequires());
     }
 
-    private boolean satisfiedDependencies(Class<?>[] requires) {
-      for (final Class<?> dependency : requires) {
+    private boolean satisfiedDependencies(Type[] requires) {
+      for (final var dependency : requires) {
         if (notProvided(dependency.getTypeName())) {
           return false;
         }
@@ -475,19 +475,19 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       return factory;
     }
 
-    Class<?>[] requires() {
+    Type[] requires() {
       return factory.requires();
     }
 
-    Class<?>[] requiresPackages() {
+    Type[] requiresPackages() {
       return factory.requiresPackages();
     }
 
-    Class<?>[] autoRequires() {
+    Type[] autoRequires() {
       return factory.autoRequires();
     }
 
-    Class<?>[] autoRequiresAspects() {
+    Type[] autoRequiresAspects() {
       return factory.autoRequiresAspects();
     }
 
@@ -505,7 +505,7 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
       return !isEmpty(factory.provides());
     }
 
-    private boolean isEmpty(@Nullable Class<?>[] values) {
+    private boolean isEmpty(@Nullable Type[] values) {
       return values == null || values.length == 0;
     }
   }
diff --git a/inject/src/main/java/io/avaje/inject/spi/Module.java b/inject/src/main/java/io/avaje/inject/spi/Module.java
index 253aeddc..bbfc4321 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Module.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Module.java
@@ -1,5 +1,7 @@
 package io.avaje.inject.spi;
 
+import java.lang.reflect.Type;
+
 import io.avaje.inject.InjectModule;
 
 /**
@@ -15,21 +17,21 @@ public interface Module {
   /**
    * Return the set of types this module explicitly provides to other modules.
    */
-  default Class<?>[] provides() {
+  default Type[] provides() {
     return EMPTY_CLASSES;
   }
 
   /**
    * Return the types this module needs to be provided externally or via other modules.
    */
-  default Class<?>[] requires() {
+  default Type[] requires() {
     return EMPTY_CLASSES;
   }
 
   /**
    * Return the packages this module needs to be provided via other modules.
    */
-  default Class<?>[] requiresPackages() {
+  default Type[] requiresPackages() {
     return EMPTY_CLASSES;
   }
 
@@ -39,7 +41,7 @@ public interface Module {
    * This is a convenience when using multiple modules that is otherwise controlled manually by
    * explicitly using {@link InjectModule#provides()}.
    */
-  default Class<?>[] autoProvides() {
+  default Type[] autoProvides() {
     return EMPTY_CLASSES;
   }
 
@@ -60,7 +62,7 @@ public interface Module {
    * This is a convenience when using multiple modules that is otherwise controlled manually by
    * explicitly using {@link InjectModule#requires()} or {@link InjectModule#requiresPackages()}.
    */
-  default Class<?>[] autoRequires() {
+  default Type[] autoRequires() {
     return EMPTY_CLASSES;
   }
 
diff --git a/inject/src/main/java/io/avaje/inject/spi/Plugin.java b/inject/src/main/java/io/avaje/inject/spi/Plugin.java
index 9f89211f..bb7469be 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Plugin.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Plugin.java
@@ -25,7 +25,7 @@ public interface Plugin {
   /**
    * Return the classes that the plugin provides.
    */
-  default Class<?>[] provides() {
+  default Type[] provides() {
     return EMPTY_CLASSES;
   }
 


Test patch:
diff --git a/blackbox-test-inject/pom.xml b/blackbox-test-inject/pom.xml
index ed333971..19409327 100644
--- a/blackbox-test-inject/pom.xml
+++ b/blackbox-test-inject/pom.xml
@@ -90,7 +90,7 @@
 			<plugin>
 				<groupId>io.avaje</groupId>
 				<artifactId>avaje-inject-maven-plugin</artifactId>
-				<version>1.1</version>
+				<version>1.3</version>
 				<executions>
 					<execution>
 						<phase>process-sources</phase>
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/external/HasExternalDependencies.java b/blackbox-test-inject/src/main/java/org/example/myapp/external/HasExternalDependencies.java
index 0a9eed34..0ced1b8f 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/external/HasExternalDependencies.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/external/HasExternalDependencies.java
@@ -2,6 +2,11 @@ package org.example.myapp.external;
 
 import io.avaje.inject.Component;
 import io.avaje.jsonb.Jsonb;
+
+import java.util.concurrent.Callable;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+
 import org.example.external.aspect.MyExternalAspect;
 import org.other.one.OtherComponent;
 
@@ -12,14 +17,19 @@ public class HasExternalDependencies {
   public final Jsonb fromPlugin;
   // component from other module
   public final OtherComponent fromExternal;
+  public final Supplier<String> stringSupplier;
 
-  public HasExternalDependencies(Jsonb jsonb, OtherComponent otherComponent) {
+  public HasExternalDependencies(
+      Jsonb jsonb,
+      OtherComponent otherComponent,
+      Supplier<String> stringSupplier) {
     this.fromPlugin = jsonb;
     this.fromExternal = otherComponent;
+    this.stringSupplier = stringSupplier;
   }
 
   @MyExternalAspect
   public String doStuff() {
-    return "hello";
+    return stringSupplier.get();
   }
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/generic/MyConsumerFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/generic/MyConsumerFactory.java
index bb2e80b1..b6b66751 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/generic/MyConsumerFactory.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/generic/MyConsumerFactory.java
@@ -1,7 +1,11 @@
 package io.avaje.inject.generator.models.valid.generic;
 
+import java.util.Map;
+import java.util.function.BiConsumer;
+
 import io.avaje.inject.Bean;
 import io.avaje.inject.Factory;
+import io.avaje.inject.generator.models.valid.A0;
 
 @Factory
 public class MyConsumerFactory {
@@ -10,4 +14,10 @@ public class MyConsumerFactory {
   MyConsumer createTest() {
     return new MyConsumer();
   }
+
+  @Bean
+  Map<String, MyConsumer> genericMap(
+      BiConsumer<A0, io.avaje.inject.generator.models.valid.nested.A0> consumer) {
+    return null;
+  }
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/AbstractSuperClass.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/AbstractSuperClass.java
new file mode 100644
index 00000000..7afdc36b
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/AbstractSuperClass.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.supertypes;
+
+public abstract class AbstractSuperClass {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/OtherComponent.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/OtherComponent.java
new file mode 100644
index 00000000..e7fd5e37
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/OtherComponent.java
@@ -0,0 +1,8 @@
+package io.avaje.inject.generator.models.valid.supertypes;
+
+import io.avaje.inject.Component;
+
+@Component
+public class OtherComponent extends AbstractSuperClass implements SomeInterface, SomeInterface2 {
+
+}
\ No newline at end of file
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface.java
new file mode 100644
index 00000000..c1911e01
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.supertypes;
+
+public interface SomeInterface {}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface2.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface2.java
new file mode 100644
index 00000000..71210b0f
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/supertypes/SomeInterface2.java
@@ -0,0 +1,3 @@
+package io.avaje.inject.generator.models.valid.supertypes;
+
+public interface SomeInterface2 {}
diff --git a/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java
index c3862ef7..5d063e50 100644
--- a/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java
+++ b/inject-test/src/test/java/org/example/custom3/ParentScopeTest.java
@@ -55,8 +55,8 @@ class ParentScopeTest {
   @Test
   void module_classes() {
     MyThreeModule module = new MyThreeModule();
-    Class<?>[] classes = module.classes();
-    Set<Class<?>> asSet = new HashSet<>(Arrays.asList(classes));
+    var classes = module.classes();
+    var asSet = new HashSet<>(Arrays.asList(classes));
 
     assertThat(asSet).contains(
       org.example.custom3.TcsFactory.class,
diff --git a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
index 78254dfa..9a5fb7d4 100644
--- a/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
+++ b/inject/src/test/java/io/avaje/inject/BeanScopeBuilderTest.java
@@ -1,8 +1,8 @@
 package io.avaje.inject;
 
-import io.avaje.inject.spi.Builder;
-import io.avaje.inject.spi.Module;
-import org.junit.jupiter.api.Test;
+import static io.avaje.inject.spi.Module.EMPTY_CLASSES;
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 
 import java.lang.annotation.Annotation;
 import java.lang.reflect.Type;
@@ -12,9 +12,11 @@ import java.util.Map;
 import java.util.Optional;
 import java.util.stream.Collectors;
 
-import static io.avaje.inject.spi.Module.EMPTY_CLASSES;
-import static org.assertj.core.api.Assertions.assertThat;
-import static org.assertj.core.api.Assertions.assertThatThrownBy;
+import org.junit.jupiter.api.Test;
+
+import io.avaje.inject.spi.Builder;
+import io.avaje.inject.spi.GenericType;
+import io.avaje.inject.spi.Module;
 
 @SuppressWarnings("all")
 class BeanScopeBuilderTest {
@@ -104,6 +106,18 @@ class BeanScopeBuilderTest {
     assertThat(names(factoryOrder.factories())).containsExactly("one", "two");
   }
 
+  @Test
+  void feature_depends_generic() {
+    DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
+    factoryOrder.add(bc("two", EMPTY_CLASSES, of(new GenericType<Map<String, MyFeature>>() {})));
+    factoryOrder.add(bc("one", of(new GenericType<Map<String, MyFeature>>() {}), EMPTY_CLASSES));
+    factoryOrder.add(bc("three", of(new GenericType<Map<String, MyFeature>>() {}), EMPTY_CLASSES));
+    factoryOrder.orderFactories();
+
+    assertThat(names(factoryOrder.factories())).containsExactly("one", "three", "two");
+  }
+
+
   @Test
   void feature_depends2() {
     DBeanScopeBuilder.FactoryOrder factoryOrder = new DBeanScopeBuilder.FactoryOrder(null, Collections.emptySet(), true);
@@ -171,22 +185,22 @@ class BeanScopeBuilderTest {
       .collect(Collectors.toList());
   }
 
-  private TDModule bc(String name, Class<?>[] provides, Class<?>[] requires) {
+  private TDModule bc(String name, Type[] provides, Type[] requires) {
     return bc(name, provides, requires, new Class[0]);
   }
 
-  private TDModule bc(String name, Class<?>[] provides, Class<?>[] requires, Class<?>[] requiresPkg) {
+  private TDModule bc(String name, Type[] provides, Type[] requires, Type[] requiresPkg) {
     return new TDModule(name, provides, requires, requiresPkg);
   }
 
   private static class TDModule implements Module {
 
     final String name;
-    final Class<?>[] provides;
-    final Class<?>[] requires;
-    final Class<?>[] requiresPackages;
+    final Type[] provides;
+    final Type[] requires;
+    final Type[] requiresPackages;
 
-    private TDModule(String name, Class<?>[] provides, Class<?>[] requires, Class<?>[] requiresPackages) {
+    private TDModule(String name, Type[] provides, Type[] requires, Type[] requiresPackages) {
       this.name = name;
       this.provides = provides;
       this.requires = requires;
@@ -199,7 +213,7 @@ class BeanScopeBuilderTest {
     }
 
     @Override
-    public Class<?>[] provides() {
+    public Type[] provides() {
       return provides;
     }
 
@@ -209,12 +223,12 @@ class BeanScopeBuilderTest {
     }
 
     @Override
-    public Class<?>[] requires() {
+    public Type[] requires() {
       return requires;
     }
 
     @Override
-    public Class<?>[] requiresPackages() {
+    public Type[] requiresPackages() {
       return requiresPackages;
     }
 
@@ -224,8 +238,8 @@ class BeanScopeBuilderTest {
     }
   }
 
-  Class<?>[] of(Class<?>... cls) {
-    return cls != null ? cls : new Class<?>[0];
+  Type[] of(Type... cls) {
+    return cls != null ? cls : new Type[0];
   }
 
   class MyFeature {
@@ -241,7 +255,7 @@ class BeanScopeBuilderTest {
 
     final String containsType;
 
-    TDBeanScope(Class<?> containsType) {
+    TDBeanScope(Type containsType) {
       this.containsType = containsType.getTypeName();
     }