Repository: avaje/avaje-inject
Pull request #470 (2023-12-09 00:41:13)

Use CodeGen for AOP Fallback
The current way of using fallback methods is pretty obtuse, so this adds

- Adds a new annotation to mark a method as a fallback for AOP.
- will generate code to register a fallback method to an invocation
- Adds new method to `Invocation` to invoke a registered fallback method
- also will use method references in generated proxies if possible

Given stuff like this:
```java

  @MyRetry
  public String retry2(String param0, int param1) {
    retryWithFallbackCounter++;
    throw new IllegalArgumentException("Retry2Fail[" + param0 + "," + param1 + "]");
  }

  @AOPFallback("retry2")
  public String fallbackRetry2(String param0, int param1, Throwable e) {
    return "fallbackRetry2-" + param0 + ":" + param1 + ":" + e.getMessage();
  }
```
this will get generated
```java
  @Override
  public java.lang.String retry2(String param0, int param1) {
    var call = new Invocation.Call<>(() -> super.retry2(param0, param1))
      .with(this, retry20, param0, param1)
      .fallback(ex -> fallbackRetry2(param0, param1, ex));
    try {
      retry20MyRetry.invoke(call);
      return call.finalResult();
    } catch (RuntimeException ex) {
      ex.addSuppressed(new InvocationException("retry2 proxy threw exception"));
      throw ex;
    } catch (Throwable t) {
      throw new InvocationException("retry2 proxy threw exception", t);
    }
  }

SentryMan: @rbygrave waiting for your thoughts before I start in on the method signature validation
rbygrave: > waiting for your thoughts before I start on the method signature validation

So yes I'm happy so far.

> start on the method signature validation

I'm not sure specifically what you mean there.  Maybe the new methods on Invocation?
SentryMan: yeah, I'm good with this
SentryMan: Feel free to change the names of method/class names

Patch:
diff --git a/.github/workflows/jdk-ea.yml b/.github/workflows/jdk-ea.yml
index 812d10ae..867df077 100644
--- a/.github/workflows/jdk-ea.yml
+++ b/.github/workflows/jdk-ea.yml
@@ -16,7 +16,7 @@ jobs:
     strategy:
       fail-fast: false
       matrix:
-        java_version: [GA,EA] ## valhalla (fails javadoc)
+        java_version: [GA,22-ea,EA] ## valhalla (fails javadoc)
         os: [ubuntu-latest]
 
     steps:
diff --git a/README.md b/README.md
index 87229c4e..20e6215a 100644
--- a/README.md
+++ b/README.md
@@ -25,9 +25,9 @@ APT-based dependency injection for server-side developers - https://avaje.io/inj
 </dependency>
 ```
 
-**JDK 22+**
+**JDK 23+**
 
-In JDK 22+, annotation processors are disabled by default, you will need to add a flag in maven to re-enable the processor.
+In JDK 23+, annotation processors are disabled by default, you will need to add a flag in maven to re-enable the processor.
 ```xml
 <plugin>
   <groupId>org.apache.maven.plugins</groupId>
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index 5dadff1d..97f7bea3 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -13,7 +13,7 @@
   <name>avaje inject generator</name>
   <description>annotation processor generating source code for avaje-inject dependency injection</description>
   <properties>
-    <avaje.prisms.version>1.17</avaje.prisms.version>
+    <avaje.prisms.version>1.18</avaje.prisms.version>
   </properties>
   <dependencies>
 
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/AspectMethod.java b/inject-generator/src/main/java/io/avaje/inject/generator/AspectMethod.java
index 54f73bda..3da002b5 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/AspectMethod.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/AspectMethod.java
@@ -2,10 +2,13 @@ package io.avaje.inject.generator;
 
 import static java.util.stream.Collectors.collectingAndThen;
 import static java.util.stream.Collectors.joining;
+import static java.util.stream.Collectors.toList;
 
 import javax.lang.model.element.ExecutableElement;
 import javax.lang.model.element.VariableElement;
 import javax.lang.model.type.TypeMirror;
+import javax.lang.model.util.ElementFilter;
+
 import java.util.*;
 import io.avaje.inject.generator.MethodReader.MethodParam;
 
@@ -18,6 +21,8 @@ final class AspectMethod {
   private final String simpleName;
   private final List<? extends TypeMirror> thrownTypes;
   private final String localName;
+  private final ExecutableElement fallback;
+  private final boolean methodRef;
 
   AspectMethod(int nameIndex, List<AspectPair> aspectPairs, ExecutableElement method) {
     this.aspectPairs = sort(aspectPairs);
@@ -27,6 +32,52 @@ final class AspectMethod {
     this.rawReturn = method.getReturnType().toString();
     this.thrownTypes = method.getThrownTypes();
     this.localName = simpleName + nameIndex;
+    this.fallback = findFallback(method);
+    methodRef = params.isEmpty();
+    validateFallback();
+  }
+
+  private ExecutableElement findFallback(ExecutableElement method) {
+    var methods = ElementFilter.methodsIn(method.getEnclosingElement().getEnclosedElements());
+    var sameNameMethods = methods.stream().filter(m -> m.getSimpleName().contentEquals(simpleName)).collect(toList());
+    var index = sameNameMethods.indexOf(method);
+
+    return methods.stream()
+      .filter(e -> matchFallback(e, sameNameMethods, index))
+      .findFirst()
+      .orElse(null);
+  }
+
+  private boolean matchFallback(ExecutableElement e, List<ExecutableElement> sameNameMethods, int index) {
+    return AOPFallbackPrism.getOptionalOn(e)
+      .filter(v -> matchFallback(v, sameNameMethods, index))
+      .isPresent();
+  }
+
+  private boolean matchFallback(AOPFallbackPrism v, List<ExecutableElement> sameNameMethods, int index) {
+    return v.value().contains(simpleName) && (sameNameMethods.size() == 1 || index == v.place());
+  }
+
+  private void validateFallback() {
+    if (fallback == null) {
+      return;
+    }
+    var returnType = Util.trimAnnotations(fallback.getReturnType().toString());
+    if (!returnType.contains(Util.shortName(rawReturn))) {
+      APContext.logError(fallback, "An AOP fallback method must have the same return type as the target method");
+    }
+
+    var fallParams = fallback.getParameters();
+    final var size = fallParams.size();
+    if (fallParams.isEmpty() || size == 1 && fallParams.get(0).asType().toString().contains("Throwable")) {
+      return;
+    }
+
+    if (params.size() != size && size != params.size() + 1) {
+      APContext.logError(
+          fallback,
+          "Invalid fallback signature. An AOP fallback method can have either 0 arguments, one Throwable argument, all the target method's arguments, or all the target method's arguments with Throwable appended.");
+    }
   }
 
   private List<AspectPair> sort(List<AspectPair> aspectPairs) {
@@ -36,8 +87,8 @@ final class AspectMethod {
 
   List<MethodReader.MethodParam> initParams(List<? extends VariableElement> parameters) {
     List<MethodReader.MethodParam> mps = new ArrayList<>(parameters.size());
-    for (VariableElement var : parameters) {
-      mps.add(new MethodReader.MethodParam(var));
+    for (var e : parameters) {
+      mps.add(new MethodReader.MethodParam(e));
     }
     return mps;
   }
@@ -79,7 +130,8 @@ final class AspectMethod {
     writer.append(" {").eol();
 
     String type = isVoid() ? "Run" : "Call<>";
-    writer.append("    var call = new Invocation.%s(() ->", type);
+
+    writer.append("    var call = new Invocation.%s(", type);
     invokeSuper(writer, simpleName);
     writer.append(")").eol();
     writeArgs(writer);
@@ -99,7 +151,12 @@ final class AspectMethod {
   }
 
   private void invokeSuper(Append writer, String simpleName) {
-    writer.append(" super.%s(", simpleName);
+    if (methodRef) {
+      writer.append("super::%s", simpleName);
+      return;
+    }
+
+    writer.append("() -> super.%s(", simpleName);
     for (int i = 0, size = params.size(); i < size; i++) {
       if (i > 0) {
         writer.append(", ");
@@ -133,11 +190,6 @@ final class AspectMethod {
     writer.eol();
   }
 
-  static String aspectTargetShortName(String target) {
-    String type = Util.shortName(target);
-    return Util.initLower(type);
-  }
-
   void writeArgs(Append writer) {
     writer.append("      .with(this, %s", localName);
     if (!params.isEmpty()) {
@@ -151,9 +203,7 @@ final class AspectMethod {
     }
     writer.append(")");
     int aspectCount = aspectPairs.size();
-    if (aspectCount < 2) {
-      writer.append(";").eol();
-    } else {
+    if (aspectCount >= 2) {
       // nesting all but last aspect
       writer.eol();
       writer.append("      // wrapping inner nested aspects based on ordering attribute").eol();
@@ -162,13 +212,13 @@ final class AspectMethod {
         AspectPair aspect = aspectPairs.get(i);
         String sn = aspect.annotationShortName();
         writer.append("      .wrap(%s%s)", localName, sn);
-        if (i < nesting -1) {
+        if (i < nesting - 1) {
           writer.eol();
-        } else {
-          writer.append(";").eol();
         }
       }
     }
+    writeFallback(writer);
+    writer.append(";").eol();
     writer.append("    try {").eol();
     if (aspectCount > 1) {
       writer.append("      // outer-most aspect").eol();
@@ -181,17 +231,51 @@ final class AspectMethod {
       writer.append("      return call.finalResult();").eol();
     }
 
-    writer.append("    } catch (RuntimeException ex) {").eol();
-    writer.append("      ex.addSuppressed(new InvocationException(\"%s proxy threw exception\"));", simpleName).eol();
-    writer.append("      throw ex;").eol();
+    writer.append("    } catch (RuntimeException $ex) {").eol();
+    writer.append("      $ex.addSuppressed(new InvocationException(\"%s proxy threw exception\"));", simpleName).eol();
+    writer.append("      throw $ex;").eol();
     writeThrowsCatch(writer);
     if (thrownTypes.stream().map(Object::toString).noneMatch("java.lang.Throwable"::equals)) {
-      writer.append("    } catch (Throwable t) {").eol();
-      writer.append("      throw new InvocationException(\"%s proxy threw exception\", t);", simpleName).eol();
+      writer.append("    } catch (Throwable $t) {").eol();
+      writer.append("      throw new InvocationException(\"%s proxy threw exception\", $t);", simpleName).eol();
     }
     writer.append("    }").eol();
   }
 
+  private void writeFallback(Append writer) {
+    if (fallback == null) {
+      return;
+    }
+
+    var fallParams = fallback.getParameters();
+    writer.eol().append("      .fallback(");
+    var hasThrowable = fallParams.stream().anyMatch(p -> p.asType().toString().contains("Throwable"));
+
+    if (fallParams.size() == 1 && hasThrowable) {
+      writer.append("this::%s", fallback.getSimpleName());
+      writer.append(")");
+      return;
+    }
+
+    var jdk = APContext.jdkVersion();
+    var unNamedPattern = jdk >= 22 || APContext.previewEnabled() && jdk == 21;
+
+    var exName = !hasThrowable && unNamedPattern ? "_" : "$ex";
+    writer.append("%s -> %s(", exName, fallback.getSimpleName());
+    if (!fallParams.isEmpty()) {
+      for (int i = 0, size = params.size(); i < size; i++) {
+        if (i > 0) {
+          writer.append(", ");
+        }
+        writer.append(params.get(i).simpleName());
+      }
+    }
+    if (hasThrowable) {
+      writer.append(", $ex");
+    }
+    writer.append("))");
+  }
+
   private void writeThrowsCatch(Append writer) {
     final var types = new ArrayList<>(thrownTypes);
     types.removeIf(ProcessingContext::isUncheckedException);
@@ -204,7 +288,7 @@ final class AspectMethod {
         .map(Util::shortName)
         .collect(collectingAndThen(joining(" | "), writer::append))
         .append(" e) {")
-        .eol(); 
+        .eol();
     writer.append("      e.addSuppressed(new InvocationException(\"%s proxy threw exception\"));", simpleName).eol();
     writer.append("      throw e;").eol();
   }
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 3038efa5..4c87dda6 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
@@ -25,13 +25,9 @@ final class GenericType {
    */
   static String trimGenericParams(String rawType) {
     int start = rawType.indexOf('<');
-    if (start > 0) {
-      // no package for any generic parameter types
-      if (rawType.indexOf('.', start) == -1) {
-        if (rawType.lastIndexOf('>') > -1) {
-          return rawType.substring(0, start);
-        }
-      }
+    // no package for any generic parameter types
+    if (start > 0 && rawType.indexOf('.', start) == -1 && rawType.lastIndexOf('>') > -1) {
+      return rawType.substring(0, start);
     }
     return rawType;
   }
@@ -108,7 +104,9 @@ final class GenericType {
   }
 
   private static boolean includeInImports(String type) {
-    return type != null && !type.startsWith("java.lang.") && type.contains(".");
+    return type != null
+        && type.contains(".")
+        && Util.notJavaLang(type);
   }
 
   /**
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 a7ce4fd8..b12d7fc9 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
@@ -188,7 +188,9 @@ final class TypeExtendsReader {
 
       final var superMirror = element.getSuperclass();
       final var superElement = asElement(superMirror);
-      addSuperType(superElement, superMirror, false);
+      if (superElement != null) {
+        addSuperType(superElement, superMirror, false);
+      }
     }
   }
 
@@ -227,7 +229,7 @@ final class TypeExtendsReader {
           .noneMatch(g -> typeElement(g.mainType()) == null);
 
       interfaceTypes.add(knownType ? rawType : GenericType.removeParameter(rawType));
-      if (!rawType.startsWith("java.lang.")) {
+      if (Util.notJavaLang(rawType)) {
         for (final TypeMirror supertype : types().directSupertypes(anInterface)) {
           readInterfacesOf(supertype);
         }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
index 1edcb12b..d9e52679 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/Util.java
@@ -23,6 +23,10 @@ final class Util {
   private static final int PROVIDER_LENGTH = PROVIDER_PREFIX.length();
   private static final int ASPECT_PROVIDER_LENGTH = ASPECT_PROVIDER_PREFIX.length();
 
+  static boolean notJavaLang(String type) {
+    return !type.startsWith("java.lang.") || Character.isLowerCase(type.charAt(10));
+  }
+
   static boolean isVoid(String type) {
     return "void".equalsIgnoreCase(type);
   }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
index d1c9ee94..0100ffbf 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java
@@ -8,6 +8,7 @@
 @GeneratePrism(Qualifier.class)
 @GeneratePrism(Named.class)
 @GeneratePrism(Inject.class)
+@GeneratePrism(AOPFallback.class)
 @GeneratePrism(Aspect.class)
 @GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism")
 @GeneratePrism(Primary.class)
@@ -26,10 +27,8 @@
 package io.avaje.inject.generator;
 
 import io.avaje.inject.*;
-import io.avaje.inject.aop.Aspect;
-import io.avaje.inject.spi.DependencyMeta;
-import io.avaje.inject.spi.Generated;
-import io.avaje.inject.spi.Proxy;
+import io.avaje.inject.aop.*;
+import io.avaje.inject.spi.*;
 import io.avaje.prism.GeneratePrism;
 import jakarta.inject.Inject;
 import jakarta.inject.Named;
diff --git a/inject/src/main/java/io/avaje/inject/Component.java b/inject/src/main/java/io/avaje/inject/Component.java
index 633ad14d..ad7c536a 100644
--- a/inject/src/main/java/io/avaje/inject/Component.java
+++ b/inject/src/main/java/io/avaje/inject/Component.java
@@ -70,7 +70,7 @@ public @interface Component {
   @Target({TYPE, PACKAGE, MODULE})
   @interface Import {
 
-    /** Specify types to generate DI classes for. */
+    /** Types to generate DI classes for. */
     Class<?>[] value();
   }
 }
diff --git a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
index 1c52a043..6b5b07fe 100644
--- a/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/DBeanScopeBuilder.java
@@ -15,7 +15,6 @@ import java.util.function.Supplier;
 import static java.lang.System.Logger.Level.DEBUG;
 import static java.lang.System.Logger.Level.INFO;
 import static java.util.Collections.emptySet;
-import static java.util.stream.Collectors.toSet;
 
 /** Build a bean scope with options for shutdown hook and supplying test doubles. */
 @NonNullApi
@@ -230,15 +229,14 @@ final class DBeanScopeBuilder implements BeanScopeBuilder.ForTesting {
 
   private void initProfiles() {
     if (profiles == null) {
-      profiles = propertyRequiresPlugin.get("avaje.profiles").map(DBeanScopeBuilder::toProfiles).orElse(emptySet());
+      profiles =
+          propertyRequiresPlugin
+              .get("avaje.profiles")
+              .map(p -> Set.of(p.split(",")))
+              .orElse(emptySet());
     }
   }
 
-  private static Set<String> toProfiles(String profiles) {
-    return Arrays.stream(profiles.split(",")).collect(toSet());
-  }
-
-
   @Override
   public BeanScope build() {
     final var start = System.currentTimeMillis();
diff --git a/inject/src/main/java/io/avaje/inject/aop/AOPFallback.java b/inject/src/main/java/io/avaje/inject/aop/AOPFallback.java
new file mode 100644
index 00000000..f63052b5
--- /dev/null
+++ b/inject/src/main/java/io/avaje/inject/aop/AOPFallback.java
@@ -0,0 +1,17 @@
+package io.avaje.inject.aop;
+
+import static java.lang.annotation.ElementType.METHOD;
+
+import java.lang.annotation.*;
+
+/** Marks a method as a recovery method for an AOP operation. */
+@Target(METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface AOPFallback {
+
+  /** the name of the target method to recover from. */
+  String value();
+
+  /** Distinguish what method to fallback when multiple methods share the same name. */
+  int place() default 0;
+}
diff --git a/inject/src/main/java/io/avaje/inject/aop/Aspect.java b/inject/src/main/java/io/avaje/inject/aop/Aspect.java
index a808966b..ef6cfba1 100644
--- a/inject/src/main/java/io/avaje/inject/aop/Aspect.java
+++ b/inject/src/main/java/io/avaje/inject/aop/Aspect.java
@@ -7,9 +7,7 @@ import java.lang.annotation.*;
  *
  * <p>Create an annotation and annotate with {@code @Aspect} to define an aspect annotation. The
  * associated type that implements {@link AspectProvider} will be used as the target class. The
- * aspect provider should be a {@code @Singleton} such that registers with <em>avaje-inject</em>.
- *
- * <p>
+ * aspect provider should be a {@code @Singleton} bean registered with <em>avaje-inject</em>.
  */
 @Target(ElementType.ANNOTATION_TYPE)
 @Retention(RetentionPolicy.RUNTIME)
@@ -37,24 +35,17 @@ public @interface Aspect {
    */
   @Target({ElementType.PACKAGE, ElementType.TYPE})
   @Retention(RetentionPolicy.SOURCE)
-  public @interface Import {
+  @interface Import {
 
+    /**
+     * Annotation type to import
+     */
     Class<? extends Annotation> value();
 
     /**
-     * Specify the priority ordering when multiple aspects are on a method.
-     *
-     * <p>When multiple aspects are on a method they are nested. The highest ordering value will be
-     * the outer-most aspect, the lowest ordering will be the inner-most aspect.
-     *
-     * <p>The outer-most aspect will have it's <em>before</em> executed first, followed by the
-     * <em>before</em> of the inner nested aspects ultimately down the invocation of the target
-     * method.
-     *
-     * <p>The reverse ordering occurs for <em>after</em> with the outer-most aspect having it's
-     * <em>after</em> executed last.
+     * Specify the priority of the imported aspect.
      *
-     * @return The ordering of this aspect. High value for outer-most aspect.
+     * @see Aspect#ordering()
      */
     int ordering() default 1000;
   }
diff --git a/inject/src/main/java/io/avaje/inject/aop/Fallback.java b/inject/src/main/java/io/avaje/inject/aop/Fallback.java
index e8e2ebbb..8609d164 100644
--- a/inject/src/main/java/io/avaje/inject/aop/Fallback.java
+++ b/inject/src/main/java/io/avaje/inject/aop/Fallback.java
@@ -3,14 +3,18 @@ package io.avaje.inject.aop;
 import java.lang.reflect.Method;
 
 /**
+ * @deprecated migrate to use {@link AOPFallback} to specify the fallback method.
+ * <p>
  * A fallback or recovery method used with Aspects.
  * <p>
  * This isn't strictly required but more a helper to make it easier for aspects
  * that want to use a fallback or recovery method.
  */
+@Deprecated(forRemoval = true)
 @FunctionalInterface
 public interface Fallback {
 
+
   /**
    * Find and return the fallback given the name and original method.
    * <p>
diff --git a/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java b/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java
index e58d4ee7..d120a75b 100644
--- a/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java
+++ b/inject/src/main/java/io/avaje/inject/aop/FallbackFinder.java
@@ -4,6 +4,7 @@ import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Method;
 import java.lang.reflect.Parameter;
 
+@Deprecated
 final class FallbackFinder {
 
   static Fallback find(String name, Method method) throws NoSuchMethodException {
@@ -43,7 +44,7 @@ final class FallbackFinder {
 
     @Override
     public Object invoke(Invocation call, Throwable e) {
-      try {
+       try {
         final Object result = fallbackMethod.invoke(call.instance(), call.arguments(e));
         call.result(result);
         return result;
diff --git a/inject/src/main/java/io/avaje/inject/aop/Invocation.java b/inject/src/main/java/io/avaje/inject/aop/Invocation.java
index 7b4742a9..2196badf 100644
--- a/inject/src/main/java/io/avaje/inject/aop/Invocation.java
+++ b/inject/src/main/java/io/avaje/inject/aop/Invocation.java
@@ -2,6 +2,8 @@ package io.avaje.inject.aop;
 
 import java.lang.reflect.Method;
 import java.util.Arrays;
+import java.util.function.Consumer;
+import java.util.function.Function;
 
 /**
  * Method invocation using in {@link MethodInterceptor#invoke(Invocation)} for Aspects.
@@ -51,8 +53,12 @@ public interface Invocation {
   Object[] arguments();
 
   /**
+   * @deprecated migrate to using {@link AOPFallback} to specify fallback method.
+   * That then removes the need to use this method altogether.
+   * <p>
    * Return the arguments additionally appending the throwable.
    */
+  @Deprecated(forRemoval = true)
   Object[] arguments(Throwable e);
 
   /**
@@ -67,6 +73,19 @@ public interface Invocation {
    */
   Object instance();
 
+  /**
+   * Return whether this invocation has a registered recovery method
+   */
+  boolean hasRecoveryMethod();
+
+  /**
+   * Invoke the recovery method associated for this invocation and return the result.
+   *
+   * @return The result of the method call. This will return null for void methods.
+   * @throws IllegalStateException if no fallback method is configured with this invocation
+   */
+  Object invokeRecoveryMethod(Throwable t);
+
   /**
    * Invocation base type for both callable and runnable methods.
    *
@@ -137,6 +156,12 @@ public interface Invocation {
      * @return The wrapped call
      */
     public abstract Base<T> wrap(MethodInterceptor methodInterceptor);
+
+    protected void noRecovery(Object recover) {
+      if (recover == null) {
+        throw new IllegalStateException("No recovery method available for this invocation");
+      }
+    }
   }
 
   /**
@@ -145,6 +170,7 @@ public interface Invocation {
   final class Run extends Base<Void> {
 
     private final CheckedRunnable delegate;
+    private Consumer<Throwable> fallback;
 
     /**
      * Create with a given closure to run.
@@ -159,12 +185,37 @@ public interface Invocation {
       return null;
     }
 
+    /**
+     * Register a fallback method which can be used to recover from an exception
+     * while intercepting an invocation
+     */
+    public Run fallback(Consumer<Throwable> fallback) {
+      this.fallback = fallback;
+      return this;
+    }
+
     @Override
     public Base<Void> wrap(MethodInterceptor methodInterceptor) {
-      return new Invocation.Run(() -> methodInterceptor.invoke(this))
-        .with(instance, method, args);
+      return new Invocation.Run(() -> methodInterceptor.invoke(this)).with(instance, method, args);
     }
 
+    @Override
+    public boolean hasRecoveryMethod() {
+      return fallback != null;
+    }
+
+    @Override
+    public Object invokeRecoveryMethod(Throwable t) {
+      noRecovery(fallback);
+      fallback.accept(t);
+      return null;
+    }
+
+    @Override
+    public Run with(Object instance, Method method, Object... args) {
+      super.with(instance, method, args);
+      return this;
+    }
   }
 
   /**
@@ -173,6 +224,7 @@ public interface Invocation {
   final class Call<T> extends Base<T> {
 
     private final CheckedSupplier<T> delegate;
+    private Function<Throwable, T> fallback;
 
     /**
      * Create with a given supplier.
@@ -192,14 +244,42 @@ public interface Invocation {
       return result;
     }
 
+    @Override
+    public Call<T> with(Object instance, Method method, Object... args) {
+      super.with(instance, method, args);
+      return this;
+    }
+
+    /**
+     * register a fallback method which can be used to recover from an exception while intercepting
+     * an invocation
+     */
+    public Call<T> fallback(Function<Throwable, T> fallback) {
+      this.fallback = fallback;
+      return this;
+    }
+
     @Override
     public Base<T> wrap(MethodInterceptor methodInterceptor) {
-      return new Invocation.Call<T>(() -> {
+      return new Invocation.Call<>(() -> {
         final Call<T> delegate = this;
         methodInterceptor.invoke(delegate);
         return delegate.finalResult();
       }).with(instance, method, args);
     }
+
+    @Override
+    public boolean hasRecoveryMethod() {
+      return fallback != null;
+    }
+
+    @Override
+    public Object invokeRecoveryMethod(Throwable t) {
+      noRecovery(fallback);
+      var result = fallback.apply(t);
+      super.result(result);
+      return result;
+    }
   }
 
   /**
diff --git a/inject/src/main/java/io/avaje/inject/aop/InvocationException.java b/inject/src/main/java/io/avaje/inject/aop/InvocationException.java
index f8352662..afbca558 100644
--- a/inject/src/main/java/io/avaje/inject/aop/InvocationException.java
+++ b/inject/src/main/java/io/avaje/inject/aop/InvocationException.java
@@ -4,7 +4,7 @@ package io.avaje.inject.aop;
  * Exception occurring during method interception.
  * <p>
  * When using aspects and {@link MethodInterceptor} any throwable that is undeclared on the
- * method is caught and re-throw as an InvocationException.
+ * method is caught and re-thrown as an InvocationException.
  */
 public class InvocationException extends RuntimeException {
 


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/r4j/MyExample.java b/blackbox-test-inject/src/main/java/org/example/myapp/r4j/MyExample.java
index b140d0e9..5d79ee48 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/r4j/MyExample.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/r4j/MyExample.java
@@ -3,6 +3,7 @@ package org.example.myapp.r4j;
 import org.example.myapp.resilience4j.MyRetry;
 
 import io.avaje.inject.Component;
+import io.avaje.inject.aop.AOPFallback;
 
 @Component
 public class MyExample {
@@ -16,22 +17,24 @@ public class MyExample {
     throw new IllegalArgumentException("no");
   }
 
-  @MyRetry(fallbackMethod = "myFallback", maxAttempts = 5)
+  @MyRetry(maxAttempts = 5)
   public String retryWithFallback() {
     retryWithFallbackCounter++;
     throw new IllegalArgumentException("no");
   }
 
+  @AOPFallback("retryWithFallback")
   public String myFallback() {
     return "fallback-response";
   }
 
-  @MyRetry(fallbackMethod = "fallbackRetry2")
+  @MyRetry
   public String retry2(String param0, int param1) {
     retryWithFallbackCounter++;
     throw new IllegalArgumentException("Retry2Fail[" + param0 + "," + param1 + "]");
   }
 
+  @AOPFallback("retry2")
   public String fallbackRetry2(String param0, int param1, Throwable e) {
     return "fallbackRetry2-" + param0 + ":" + param1 + ":" + e.getMessage();
   }
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/MyRetry.java b/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/MyRetry.java
index 1e4897c0..586ca8d7 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/MyRetry.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/MyRetry.java
@@ -9,8 +9,6 @@ import java.lang.annotation.RetentionPolicy;
 @Retention(RetentionPolicy.RUNTIME)
 public @interface MyRetry {
 
-  String fallbackMethod() default "";
-
   int maxAttempts() default -1;
 
   String waitDuration() default "";
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/RetryProvider.java b/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/RetryProvider.java
index 12933a67..5ae25483 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/RetryProvider.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/resilience4j/RetryProvider.java
@@ -1,19 +1,17 @@
 package org.example.myapp.resilience4j;
 
+import java.lang.reflect.Method;
+import java.time.Duration;
+import java.time.format.DateTimeParseException;
+import java.util.concurrent.atomic.AtomicInteger;
+
 import io.avaje.inject.Component;
 import io.avaje.inject.aop.AspectProvider;
-import io.avaje.inject.aop.Fallback;
 import io.avaje.inject.aop.Invocation;
 import io.avaje.inject.aop.MethodInterceptor;
 import io.github.resilience4j.retry.Retry;
 import io.github.resilience4j.retry.RetryConfig;
 
-import java.lang.reflect.Method;
-import java.time.Duration;
-import java.time.format.DateTimeParseException;
-import java.util.concurrent.atomic.AtomicInteger;
-import java.util.function.Supplier;
-
 @Component
 public class RetryProvider implements AspectProvider<MyRetry> {
 
@@ -32,64 +30,33 @@ public class RetryProvider implements AspectProvider<MyRetry> {
       } catch (DateTimeParseException e) {
         e.printStackTrace();
       }
-
     }
-    RetryConfig retryConfig = RetryConfig.ofDefaults();
     RetryConfig config = builder.build();
     io.github.resilience4j.retry.Retry retry = io.github.resilience4j.retry.Retry.of("id", config);
 
-    Fallback fallback = fallback(method, retryAnnotation);
-
-    return fallback == null ?  new RetryInterceptor(retry) : new RetryFallbackInterceptor(retry, fallback);
-  }
-
-  private Fallback fallback(Method method, MyRetry retryAnnotation) {
-    if (!retryAnnotation.fallbackMethod().isEmpty()) {
-      try {
-        return Fallback.find(retryAnnotation.fallbackMethod(), method);
-      } catch (NoSuchMethodException e) {
-        throw new IllegalStateException(e);
-      }
-    }
-    return null;
+    return new RetryFallbackInterceptor(retry);
   }
 
-
   public static AtomicInteger testCounter = new AtomicInteger();
 
-  static class RetryInterceptor implements MethodInterceptor {
-
-    private final io.github.resilience4j.retry.Retry retry;
-
-    public RetryInterceptor(Retry retry) {
-      this.retry = retry;
-    }
-
-    @Override
-    public void invoke(Invocation invocation) throws Throwable {
-      testCounter.incrementAndGet();
-      Retry.decorateCheckedSupplier(retry, invocation::invoke)
-        .apply();
-    }
-  }
-
   static class RetryFallbackInterceptor implements MethodInterceptor {
 
     private final io.github.resilience4j.retry.Retry retry;
-    private final Fallback fallback;
 
-    public RetryFallbackInterceptor(Retry retry, Fallback fallback) {
+    public RetryFallbackInterceptor(Retry retry) {
       this.retry = retry;
-      this.fallback = fallback;
     }
 
     @Override
     public void invoke(Invocation invocation) throws Throwable {
       testCounter.incrementAndGet();
-      Retry.decorateCheckedSupplier(retry, invocation::invoke)
-        .recover((throwable) -> (Supplier<Object>) () -> fallback.invoke(invocation, throwable))
-        .apply();
+      if (invocation.hasRecoveryMethod()) {
+        Retry.decorateCheckedSupplier(retry, invocation::invoke)
+            .recover(throwable -> () -> invocation.invokeRecoveryMethod(throwable))
+            .apply();
+      } else {
+        Retry.decorateCheckedSupplier(retry, invocation::invoke).apply();
+      }
     }
   }
-
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
index d942782d..ef1323b1 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/UtilTest.java
@@ -10,6 +10,16 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
 
 class UtilTest {
 
+  @Test
+  void notJavaLang() {
+    assertFalse(Util.notJavaLang("java.lang.A"));
+    assertFalse(Util.notJavaLang("java.lang.Foo"));
+
+    assertTrue(Util.notJavaLang("not.lang.Foo"));
+    assertTrue(Util.notJavaLang("java.lang.annotation.A"));
+    assertTrue(Util.notJavaLang("java.lang.other.Foo"));
+  }
+
   @Test
   void nestedShortName() {
     assertEquals(Util.shortName("com.example.Foo.Bar"), "Foo.Bar");
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java
new file mode 100644
index 00000000..fd9273e6
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/LangInject.java
@@ -0,0 +1,13 @@
+package io.avaje.inject.generator.models.valid;
+
+import java.lang.annotation.Annotation;
+
+import io.avaje.lang.Nullable;
+import jakarta.inject.Inject;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class LangInject {
+
+  @Inject @Nullable Annotation classLoadingMXBean;
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/aspect/MethodTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/aspect/MethodTest.java
index f75d4741..b6b11e32 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/aspect/MethodTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/aspect/MethodTest.java
@@ -3,6 +3,7 @@ package io.avaje.inject.generator.models.valid.aspect;
 import java.util.Map;
 
 import io.avaje.inject.Component;
+import io.avaje.inject.aop.AOPFallback;
 import io.avaje.inject.generator.models.valid.Timed;
 
 @Component
@@ -10,4 +11,28 @@ public class MethodTest {
 
   @Timed
   void test(@Param Map<@TypeUse String, String> str, @Param int inty, String regular) {}
+
+  @Timed
+  @AOPFallback("test")
+  void retry(@Param Map<@TypeUse String, String> str, @Param int inty, String regular) {}
+
+  @Timed
+  @AOPFallback("retry")
+  void retry2(Throwable ex) {}
+
+  @AOPFallback("retry2")
+  void retry3() {}
+
+  @Timed
+  void test2(@Param Map<@TypeUse String, String> str, @Param int inty, String regular) {}
+
+  @AOPFallback("test2")
+  void retry4(
+      @Param Map<@TypeUse String, String> str, @Param int inty, String regular, Throwable t) {}
+
+  @Timed
+  void test2( @Param int inty, String regular) {}
+
+  @AOPFallback(value = "test2", place = 1)
+  void retry4(@Param int inty, String regular, Throwable t) {}
 }
diff --git a/inject/src/test/java/io/avaje/inject/aop/InvocationFallbackTest.java b/inject/src/test/java/io/avaje/inject/aop/InvocationFallbackTest.java
index 2285512d..25ad41ca 100644
--- a/inject/src/test/java/io/avaje/inject/aop/InvocationFallbackTest.java
+++ b/inject/src/test/java/io/avaje/inject/aop/InvocationFallbackTest.java
@@ -2,7 +2,6 @@ package io.avaje.inject.aop;
 
 import org.junit.jupiter.api.Test;
 
-import java.lang.invoke.MethodHandles;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
@@ -11,13 +10,7 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 class InvocationFallbackTest {
 
-  private final MethodHandles.Lookup lookup = MethodHandles.lookup();
-
   private final List<String> trace = new ArrayList<>();
-  private final List<Method> methods = new ArrayList<>();
-  private final List<Object> results = new ArrayList<>();
-  private final List<Object> args = new ArrayList<>();
-
   private final Method doStuffMethod;
   private final Object myArg = new Object();
   private boolean throwOnDoStuff;
@@ -51,11 +44,12 @@ class InvocationFallbackTest {
   void invokeWithFallback() throws Throwable {
     throwOnDoStuff = true;
 
-    Invocation.Base<String> call = new Invocation.Call<>(() -> this.doStuff(myArg))
-      .with(this, doStuffMethod, myArg);
+    Invocation.Base<String> call =
+        new Invocation.Call<>(() -> doStuff(myArg))
+            .with(this, doStuffMethod, myArg)
+            .fallback(e -> fallbackDoStuff(myArg));
 
-    Fallback fallback = Fallback.find("fallbackDoStuff", doStuffMethod);
-    MyInterceptor myInterceptor = new MyInterceptor(fallback);
+    MyInterceptor myInterceptor = new MyInterceptor();
 
     // invoke the original method
     myInterceptor.invoke(call);
@@ -70,11 +64,11 @@ class InvocationFallbackTest {
   void invokeWithNoFallback() throws Throwable {
     throwOnDoStuff = false;
 
-    Invocation.Base<String> call = new Invocation.Call<>(() -> this.doStuff(myArg))
-      .with(this, doStuffMethod, myArg);
+    Invocation.Base<String> call = new Invocation.Call<>(() -> doStuff(myArg))
+      .with(this, doStuffMethod, myArg)
+      .fallback(this::fallbackDoStuff);
 
-    Fallback fallback = Fallback.find("fallbackDoStuff", doStuffMethod);
-    MyInterceptor myInterceptor = new MyInterceptor(fallback);
+     MyInterceptor myInterceptor = new MyInterceptor();
 
     // invoke the original method
     myInterceptor.invoke(call);
@@ -87,18 +81,12 @@ class InvocationFallbackTest {
 
   static class MyInterceptor implements MethodInterceptor {
 
-    final Fallback fallback;
-
-    MyInterceptor(Fallback fallback) {
-      this.fallback = fallback;
-    }
-
     @Override
     public void invoke(Invocation invocation) throws Throwable {
       try {
         invocation.invoke();
       } catch (Throwable e) {
-        fallback.invoke(invocation, e);
+        invocation.invokeRecoveryMethod(e);
       }
     }
   }