Repository: avaje/avaje-inject
Pull request #554 (2024-04-27 07:10:12)

Auto Generate Event Publishers
Makes publishing events more CDI-like. The generator will now create a publisher class when it sees `Event<T>` injected and a generated impl doesn't already exist.

- Remove ApplicationEvent Annotation
- Enhance generator to automatically create publisher classes
- can now generate generic publishers
- similar to CDI, adding a qualifier to an `Event<T>` dependency will create and wire a publisher that by default sends qualified events


Patch:
diff --git a/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java b/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java
new file mode 100644
index 00000000..5d5eeb02
--- /dev/null
+++ b/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java
@@ -0,0 +1,19 @@
+package org.other.one.events;
+
+import io.avaje.inject.Component;
+import io.avaje.inject.event.Event;
+import io.avaje.inject.event.ObserverManager;
+import io.avaje.inject.spi.Generated;
+import java.lang.reflect.Type;
+import org.other.one.SomeOptionalDep;
+
+@Component
+@Generated("avaje-inject-generator")
+public class SomeOptionalDep$Publisher extends Event<SomeOptionalDep> {
+
+  private static final Type TYPE = SomeOptionalDep.class;
+
+  public SomeOptionalDep$Publisher(ObserverManager manager) {
+    super(manager, TYPE, "");
+  }
+}
diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml
index 07cad133..4cddd162 100644
--- a/inject-generator/pom.xml
+++ b/inject-generator/pom.xml
@@ -51,6 +51,19 @@
           <useModulePath>false</useModulePath>
         </configuration>
       </plugin>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <configuration>
+          <annotationProcessorPaths>
+            <path>
+              <groupId>io.avaje</groupId>
+              <artifactId>avaje-prisms</artifactId>
+              <version>${avaje.prisms.version}</version>
+            </path>
+          </annotationProcessorPaths>
+        </configuration>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java
index f9ebe45a..94c68959 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java
@@ -2,42 +2,93 @@ package io.avaje.inject.generator;
 
 import static io.avaje.inject.generator.APContext.createSourceFile;
 import static io.avaje.inject.generator.APContext.logError;
+import static java.util.function.Predicate.not;
 
 import java.io.IOException;
 import java.io.Writer;
+import java.lang.reflect.Type;
 import java.text.MessageFormat;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 
 import javax.lang.model.element.Element;
 import javax.tools.JavaFileObject;
 
 /** Write the source code for the bean. */
 final class EventPublisherWriter {
-
-  private static final String TEMPLATE =
+  private static final Map<String, String> GENERATED_PUBLISHERS = new HashMap<>();
+  private final String TEMPLATE =
       "package {0};\n\n"
           + "{1}"
           + "@Component\n"
+          + "{2}"
           + "@Generated(\"avaje-inject-generator\")\n"
-          + "public class {2}Publisher extends Event<{2}> '{'\n"
+          + "public class {3} extends Event<{4}> '{'\n"
+          + "\n"
+          + "  private static final Type TYPE = {5};\n"
           + "\n"
-          + "  public {2}Publisher(ObserverManager manager) '{'\n"
-          + "    super(manager, {2}.class);\n"
+          + "  public {3}(ObserverManager manager) '{'\n"
+          + "    super(manager, TYPE, \"{6}\");\n"
           + "  '}'\n"
           + "'}'\n";
   private final String originName;
   private final ImportTypeMap importTypes = new ImportTypeMap();
   private final UType utype;
   private final String packageName;
+  private final String qualifier;
 
-  EventPublisherWriter(Element element) {
-    this.packageName = APContext.elements().getPackageOf(element).getQualifiedName().toString();
-    this.utype = UType.parse(element.asType());
-    this.originName = utype.mainType() + "Publisher";
-    importTypes.addAll(utype.importTypes());
-    if (utype.isGeneric()) {
-      APContext.logError(element, "Event publishers generation may not be used for generic classes. Generic event publishers must be constructed manually");
+  static void write(Element element) {
+    new EventPublisherWriter(element);
+  }
+
+  private EventPublisherWriter(Element element) {
+    final var asType = element.asType();
+    this.utype = UType.parse(asType).param0();
+
+    this.packageName =
+        APContext.elements()
+                .getPackageOf(APContext.typeElement(utype.mainType()))
+                .getQualifiedName()
+                .toString()
+                .replaceFirst("java.", "")
+            + ".events";
+    qualifier = Optional.ofNullable(Util.getNamed(element)).orElse("");
+    var className =
+        packageName
+            + "."
+            + (qualifier.isEmpty() ? "" : "Qualified")
+            + Util.shortName(utype).replace(".", "_")
+            + "$Publisher";
+
+    originName = getUniqueClassName(className, 0);
+
+    if (GENERATED_PUBLISHERS.containsKey(originName)) {
+      return;
     }
+    importTypes.addAll(utype.importTypes());
     write();
+    GENERATED_PUBLISHERS.put(originName, qualifier);
+  }
+
+  private String getUniqueClassName(String className, Integer recursiveIndex) {
+
+    Optional.ofNullable(APContext.typeElement(className))
+        .ifPresent(
+            e ->
+                GENERATED_PUBLISHERS.put(
+                    e.getQualifiedName().toString(),
+                    Optional.ofNullable(Util.getNamed(e)).orElse("")));
+
+    if (Optional.ofNullable(GENERATED_PUBLISHERS.get(className))
+        .filter(not(qualifier::equals))
+        .isPresent()) {
+      var index = className.indexOf("$Publisher");
+      className = className.substring(0, index) + "$Publisher" + ++recursiveIndex;
+      return getUniqueClassName(className, recursiveIndex);
+    }
+    return className;
   }
 
   private Writer createFileWriter() throws IOException {
@@ -48,7 +99,15 @@ final class EventPublisherWriter {
   void write() {
     try {
       var writer = new Append(createFileWriter());
-      writer.append(MessageFormat.format(TEMPLATE, packageName, imports(), utype.shortType()));
+
+      final var shortType = utype.shortWithoutAnnotations();
+      var typeString = utype.isGeneric() ? getGenericType() : shortType + ".class";
+
+      var name = qualifier.isBlank() ? "" : "@Named(\"" + qualifier + "\")\n";
+      var className = originName.replace(packageName + ".", "");
+      writer.append(
+          MessageFormat.format(
+              TEMPLATE, packageName, imports(), name, className, shortType, typeString, qualifier));
       writer.close();
     } catch (Exception e) {
       e.printStackTrace();
@@ -61,6 +120,15 @@ final class EventPublisherWriter {
     importTypes.add("io.avaje.inject.event.Event");
     importTypes.add("io.avaje.inject.event.ObserverManager");
     importTypes.add("io.avaje.inject.spi.Generated");
+    importTypes.add(Type.class.getCanonicalName());
+    if (!qualifier.isBlank()) {
+      importTypes.add(NamedPrism.PRISM_TYPE);
+    }
+    if (utype.isGeneric()) {
+
+      importTypes.add("io.avaje.inject.spi.GenericType");
+    }
+
     StringBuilder writer = new StringBuilder();
     for (String importType : importTypes.forImport()) {
       if (Util.validImportType(importType)) {
@@ -69,4 +137,39 @@ final class EventPublisherWriter {
     }
     return writer.append("\n").toString();
   }
+
+  String getGenericType() {
+    var sb = new StringBuilder();
+    sb.append("\n      new GenericType<");
+    writeGenericType(utype, new HashMap<>(), sb);
+    sb.append(">(){}.type();");
+    return sb.toString();
+  }
+
+  private void writeGenericType(
+      UType type, Map<String, String> seenShortNames, StringBuilder writer) {
+    final var typeShortName = Util.shortName(type.mainType());
+    final var mainType = seenShortNames.computeIfAbsent(typeShortName, k -> type.mainType());
+    if (type.isGeneric()) {
+      final var shortName =
+          Objects.equals(type.mainType(), mainType) ? typeShortName : type.mainType();
+      writer.append(shortName);
+      writer.append("<");
+      boolean first = true;
+      for (final var param : type.componentTypes()) {
+        if (first) {
+          first = false;
+          writeGenericType(param, seenShortNames, writer);
+          continue;
+        }
+        writer.append(", ");
+        writeGenericType(param, seenShortNames, writer);
+      }
+      writer.append(">");
+    } else {
+      final var shortName =
+          Objects.equals(type.mainType(), mainType) ? typeShortName : type.mainType();
+      writer.append(shortName);
+    }
+  }
 }
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
index 8e4f62f0..d9f57037 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java
@@ -29,6 +29,9 @@ final class FieldReader {
     if (nullable || element.asType().toString().startsWith("java.util.Optional<")) {
       ProcessingContext.addOptionalType(fieldType);
     }
+    if (type.fullWithoutAnnotations().startsWith("io.avaje.inject.event.Event")) {
+      EventPublisherWriter.write(element);
+    }
   }
 
   boolean isGenericParam() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
index 744de519..3b1584d4 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java
@@ -29,7 +29,6 @@ import static io.avaje.inject.generator.ProcessingContext.*;
 @GenerateAPContext
 @GenerateModuleInfoReader
 @SupportedAnnotationTypes({
-  ApplicationEventPrism.PRISM_TYPE,
   AspectImportPrism.PRISM_TYPE,
   AssistFactoryPrism.PRISM_TYPE,
   ComponentPrism.PRISM_TYPE,
@@ -146,9 +145,6 @@ public final class InjectProcessor extends AbstractProcessor {
     maybeElements(roundEnv, Constants.CONTROLLER).ifPresent(this::readBeans);
     maybeElements(roundEnv, ProxyPrism.PRISM_TYPE).ifPresent(this::readBeans);
     maybeElements(roundEnv, AssistFactoryPrism.PRISM_TYPE).ifPresent(this::readAssisted);
-    maybeElements(roundEnv, ApplicationEventPrism.PRISM_TYPE).stream()
-      .flatMap(Set::stream)
-      .forEach(EventPublisherWriter::new);
 
     maybeElements(roundEnv, ExternalPrism.PRISM_TYPE).stream()
       .flatMap(Set::stream)
@@ -157,11 +153,12 @@ public final class InjectProcessor extends AbstractProcessor {
       .map(u -> "java.util.List".equals(u.mainType()) ? u.param0() : u)
       .map(UType::fullWithoutAnnotations)
       .forEach(ProcessingContext::addOptionalType);
+
     allScopes.readBeans(roundEnv);
     defaultScope.write(processingOver);
     allScopes.write(processingOver);
 
-    if (roundEnv.processingOver()) {
+    if (processingOver) {
       var order =
         new FactoryOrder(ProcessingContext.avajeModules(), defaultScope.pluginProvided())
           .orderModules();
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 232bb327..36a2c7b2 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
@@ -445,6 +445,10 @@ final class MethodReader {
       if (nullable || param.asType().toString().startsWith("java.util.Optional<")) {
         ProcessingContext.addOptionalType(paramType);
       }
+
+      if (fullUType.fullWithoutAnnotations().startsWith("io.avaje.inject.event.Event")) {
+        EventPublisherWriter.write(param);
+      }
     }
 
     @Override
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 711fab99..2feaac6e 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
@@ -119,15 +119,16 @@ final class TypeExtendsReader {
   }
 
   List<UType> autoProvides() {
-    if (!autoProvide || !providesAspect.isEmpty()) {
-      return List.of();
-    }
     if (baseTypeIsInterface) {
       return List.of(Util.unwrapProvider(baseUType));
     }
     var autoProvides = new ArrayList<>(interfaceTypes);
     autoProvides.addAll(extendsTypes);
-    autoProvides.add(Util.unwrapProvider(baseUType));
+    if (!autoProvide || !providesAspect.isEmpty()) {
+      autoProvides.remove(baseUType);
+    } else {
+      autoProvides.add(Util.unwrapProvider(baseUType));
+    }
     return autoProvides;
   }
 
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 76947300..c9eac994 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
@@ -1,5 +1,4 @@
 @GeneratePrism(AOPFallback.class)
-@GeneratePrism(ApplicationEvent.class)
 @GeneratePrism(Aspect.class)
 @GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism")
 @GeneratePrism(Assisted.class)
diff --git a/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java b/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java
deleted file mode 100644
index e5d463d6..00000000
--- a/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.avaje.inject.event;
-
-import static java.lang.annotation.ElementType.TYPE;
-import static java.lang.annotation.RetentionPolicy.SOURCE;
-
-import java.lang.annotation.Retention;
-import java.lang.annotation.Target;
-
-/**
- * Marks a class/record as an event class, signaling the annotation processor to generate a
- * publisher class for the annotated type.
- */
-@Target(TYPE)
-@Retention(SOURCE)
-public @interface ApplicationEvent {}
diff --git a/inject/src/main/java/io/avaje/inject/event/Event.java b/inject/src/main/java/io/avaje/inject/event/Event.java
index 7e2c0b0e..98120825 100644
--- a/inject/src/main/java/io/avaje/inject/event/Event.java
+++ b/inject/src/main/java/io/avaje/inject/event/Event.java
@@ -37,9 +37,16 @@ import java.util.concurrent.CompletionStage;
 public abstract class Event<T> {
 
   protected final List<Observer<T>> observers;
+  protected final String defaultQualifier;
 
   protected Event(ObserverManager manager, Type type) {
     this.observers = manager.observersByType(type);
+    this.defaultQualifier = "";
+  }
+
+  protected Event(ObserverManager manager, Type type, String qualifier) {
+    this.observers = manager.observersByType(type);
+    this.defaultQualifier = qualifier;
   }
 
   /**
@@ -83,26 +90,26 @@ public abstract class Event<T> {
   }
 
   /**
-   * Fires an event and notifies observers with no qualifier.
+   * Fires an event and notifies observers with the qualifier set for this instance.
    *
    * @param event the event object
    */
   public void fire(T event) {
-    fire(event, "");
+    fire(event, defaultQualifier);
   }
 
   /**
-   * Fires an event to asynchronous observers without qualifiers
+   * Fires an event to asynchronous observers with the qualifier set for this instance.
    *
    * @param event the event object
    * @return a {@link CompletionStage} allowing further pipeline composition on the asynchronous
    *     operation.
    */
   public CompletionStage<T> fireAsync(T event) {
-    return fireAsync(event, "");
+    return fireAsync(event, defaultQualifier);
   }
 
-  private static class CollectingExceptionHandler {
+  private static final class CollectingExceptionHandler {
 
     private final List<Exception> throwables;
 
@@ -114,17 +121,17 @@ public abstract class Event<T> {
       this.throwables = throwables;
     }
 
-    public void handle(Exception throwable) {
+    void handle(Exception throwable) {
       throwables.add(throwable);
     }
 
-    public List<Exception> getHandledExceptions() {
+    List<Exception> handledExceptions() {
       return throwables;
     }
   }
 
   private void handleExceptions(CollectingExceptionHandler handler) {
-    var handledExceptions = handler.getHandledExceptions();
+    var handledExceptions = handler.handledExceptions();
     if (!handledExceptions.isEmpty()) {
       var exception =
         handledExceptions.size() == 1
diff --git a/inject/src/main/java/io/avaje/inject/event/ObserverManager.java b/inject/src/main/java/io/avaje/inject/event/ObserverManager.java
index db1a6657..f7c59b66 100644
--- a/inject/src/main/java/io/avaje/inject/event/ObserverManager.java
+++ b/inject/src/main/java/io/avaje/inject/event/ObserverManager.java
@@ -5,8 +5,8 @@ import java.util.List;
 
 /**
  * Manages all {@link Observer} instances in the BeanScope.
- *
- * <p>A default implementation is provided by avaje-inject.
+ * <p>
+ * A default implementation is provided by avaje-inject.
  */
 public interface ObserverManager {
 
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 6b4d9ed7..a887ac5f 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -76,8 +76,10 @@ public interface Builder {
    */
   <T> void registerProvider(Provider<T> provider);
 
-  /** Register the observer into the context. */
-  <T> void registerObserver(Type type, int priority, boolean sync, Consumer<T> observer, String qualifier);
+  /**
+   * Register the observer into the context.
+   */
+  <T> void registerObserver(Type type, int priority, boolean async, Consumer<T> observer, String qualifier);
 
   /**
    * Register the bean instance into the context.
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 372d21f8..97c59ba7 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java
@@ -190,9 +190,9 @@ class DBuilder implements Builder {
   }
 
   @Override
-  public <T> void registerObserver(Type type, int priority, boolean sync, Consumer<T> observer, String qualifier) {
+  public <T> void registerObserver(Type type, int priority, boolean async, Consumer<T> observer, String qualifier) {
     get(ObserverManager.class)
-        .registerObserver(type, new Observer<T>(priority, sync, observer, qualifier));
+        .registerObserver(type, new Observer<>(priority, async, observer, qualifier));
   }
 
   @Override


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java
new file mode 100644
index 00000000..5f9a7573
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java
@@ -0,0 +1,22 @@
+package org.example.myapp.events;
+
+import org.example.external.aspect.MyExternalAspect;
+import org.other.one.SomeOptionalDep;
+
+import io.avaje.inject.Component;
+import io.avaje.inject.event.Event;
+
+@Component
+public class ExternalEventPublisher {
+  Event<SomeOptionalDep> event;
+
+  public ExternalEventPublisher(Event<SomeOptionalDep> event) {
+
+    this.event = event;
+  }
+
+  @MyExternalAspect
+  public void fire(SomeOptionalDep dep) {
+    event.fire(dep);
+  }
+}
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java
new file mode 100644
index 00000000..0b06a736
--- /dev/null
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java
@@ -0,0 +1,19 @@
+package org.example.myapp.events;
+
+import org.other.one.SomeOptionalDep;
+
+import io.avaje.inject.event.Observes;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class ExternalObserver {
+
+  boolean invoked;
+  SomeOptionalDep recievedEvent;
+
+  public void observe(@Observes SomeOptionalDep event) {
+    invoked = true;
+
+    this.recievedEvent = event;
+  }
+}
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java b/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java
new file mode 100644
index 00000000..23cc78e8
--- /dev/null
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java
@@ -0,0 +1,33 @@
+package org.example.myapp.events;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.other.one.SomeOptionalDep;
+
+import io.avaje.inject.test.InjectTest;
+import jakarta.inject.Inject;
+
+@InjectTest
+class TestEventMessaging {
+
+  @Inject ExternalObserver observer;
+  @Inject ExternalEventPublisher event;
+
+  @BeforeEach
+  void before() {
+    observer.invoked = false;
+    observer.recievedEvent = null;
+  }
+
+  @Test
+  void test() {
+    var message = new SomeOptionalDep() {};
+
+    event.fire(message);
+
+    assertThat(observer.invoked).isTrue();
+    assertThat(observer.recievedEvent).isSameAs(message);
+  }
+}
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
index 7602e6eb..5d62511b 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java
@@ -13,6 +13,7 @@ import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
 import java.util.Set;
+import java.util.stream.Stream;
 
 import javax.tools.JavaCompiler;
 import javax.tools.JavaCompiler.CompilationTask;
@@ -30,7 +31,9 @@ class InjectProcessorTest {
   @AfterEach
   void deleteGeneratedFiles() throws IOException {
     try {
-      Files.walk(Paths.get("io").toAbsolutePath())
+      Stream.concat(
+              Files.walk(Paths.get("io").toAbsolutePath()),
+              Files.walk(Paths.get("lang").toAbsolutePath()))
           .sorted(Comparator.reverseOrder())
           .map(Path::toFile)
           .forEach(File::delete);
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java
index e6a73a5b..58cdce89 100644
--- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java
@@ -1,8 +1,5 @@
 package io.avaje.inject.generator.models.valid.observes;
 
-import io.avaje.inject.event.ApplicationEvent;
-
-@ApplicationEvent
 public class CustomEvent {
 
 }
diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java
new file mode 100644
index 00000000..d22c8299
--- /dev/null
+++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java
@@ -0,0 +1,26 @@
+package io.avaje.inject.generator.models.valid.observes;
+
+import java.util.List;
+
+import io.avaje.inject.event.Event;
+import io.avaje.inject.spi.GenericType;
+import jakarta.inject.Inject;
+import jakarta.inject.Named;
+import jakarta.inject.Singleton;
+
+@Singleton
+public class EventSender {
+  private final Event<CustomEvent> event;
+
+  @Inject Event<List<String>> fieldInject;
+
+  public EventSender(
+      @Named("same") Event<CustomEvent> event,
+      @Named("same") Event<CustomEvent> dup,
+      @Named("different") Event<CustomEvent> celsius) {
+    this.event = event;
+  }
+
+  @Inject
+  public void methodInject(@Named("method") Event<String> celsius) {}
+}
diff --git a/inject-test/src/test/java/org/example/observes/CustomEvent.java b/inject-test/src/test/java/org/example/observes/CustomEvent.java
index b7ca2519..ca9942a0 100644
--- a/inject-test/src/test/java/org/example/observes/CustomEvent.java
+++ b/inject-test/src/test/java/org/example/observes/CustomEvent.java
@@ -1,8 +1,5 @@
 package org.example.observes;
 
-import io.avaje.inject.event.ApplicationEvent;
-
-@ApplicationEvent
 public class CustomEvent {
   private final String string;
 
diff --git a/inject-test/src/test/java/org/example/observes/EventSender.java b/inject-test/src/test/java/org/example/observes/EventSender.java
new file mode 100644
index 00000000..18b9be0a
--- /dev/null
+++ b/inject-test/src/test/java/org/example/observes/EventSender.java
@@ -0,0 +1,11 @@
+package org.example.observes;
+
+import io.avaje.inject.Component;
+import io.avaje.inject.event.Event;
+import jakarta.inject.Inject;
+
+@Component
+public class EventSender {
+
+  @Inject Event<CustomEvent> event;
+}