Repository: avaje/avaje-inject
Pull request #542 (2024-04-14 10:00:20)

Refactor simplify lazy to use existing OnceProvider
Extracted this refactoring from #540

The existing .registerProvider() without a leading .asPrototype() is already a "lazy only once" style provider. This change uses that and then removes the LazyProvider and registerLazy() as they are no longer needed or used.

This also changes the synchronized block to use a ReentrantLock in OnceProvider just in case the provider invocation does something interesting and using Java 21+ Virtual Threads.


Patch:
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 4855fc3a..b6325f49 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
@@ -202,7 +202,7 @@ final class MethodReader {
       writer.append(".asSecondary()");
     }
 
-    writer.indent(String.format(".%s(() -> {", lazy ? "registerLazy" : "registerProvider")).eol();
+    writer.indent(".registerProvider(() -> {").eol();
 
     startTry(writer, "  ");
     writer.indent(indent).append("  return ");
@@ -235,6 +235,8 @@ final class MethodReader {
         writer.append(".asPrimary()");
       } else if (secondary) {
         writer.append(".asSecondary()");
+      } else if (prototype) {
+        writer.append(".asPrototype()");
       }
       if (Util.isProvider(returnTypeRaw)) {
         writer.append(".registerProvider(bean);").eol();
@@ -360,7 +362,7 @@ final class MethodReader {
   }
 
   boolean isProtoType() {
-    return prototype;
+    return prototype && !Util.isProvider(returnTypeRaw);
   }
 
   boolean isLazy() {
diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
index b4de2f78..ace294b8 100644
--- a/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
+++ b/inject-generator/src/main/java/io/avaje/inject/generator/SimpleBeanWriter.java
@@ -176,7 +176,7 @@ final class SimpleBeanWriter {
     beanReader.buildAddFor(writer);
     if (beanReader.registerProvider()) {
       indent += "  ";
-      writer.append("      builder.%s(() -> {", beanReader.lazy() ? "registerLazy" : "asPrototype().registerProvider").eol();
+      writer.append("      builder.%s(() -> {", beanReader.lazy() ? "registerProvider" : "asPrototype().registerProvider").eol();
     }
     constructor.startTry(writer);
     writeCreateBean(constructor);
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 eb506576..38fead61 100644
--- a/inject/src/main/java/io/avaje/inject/spi/Builder.java
+++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java
@@ -76,13 +76,6 @@ public interface Builder {
    */
   <T> void registerProvider(Provider<T> provider);
 
-  /**
-   * Register a lazy provider into the context.
-   */
-  default <T> void registerLazy(Provider<T> original) {
-    registerProvider(new LazyProvider<>(original));
-  }
-
   /**
    * Register the bean instance into the context.
    *
diff --git a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
index ac2e6fa4..26c4e9ea 100644
--- a/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
+++ b/inject/src/main/java/io/avaje/inject/spi/DContextEntryBean.java
@@ -4,6 +4,7 @@ import io.avaje.inject.BeanEntry;
 
 import jakarta.inject.Provider;
 import java.util.Objects;
+import java.util.concurrent.locks.ReentrantLock;
 
 /**
  * Holds either the bean itself or a provider of the bean.
@@ -136,6 +137,7 @@ class DContextEntryBean {
    */
   static final class OnceProvider extends DContextEntryBean {
 
+    private final ReentrantLock lock = new ReentrantLock();
     private final Provider<?> provider;
     private Object bean;
 
@@ -146,11 +148,14 @@ class DContextEntryBean {
 
     @Override
     Object bean() {
-      synchronized (this) {
+      lock.lock();
+      try {
         if (bean == null) {
           bean = provider.get();
         }
         return bean;
+      } finally {
+        lock.unlock();
       }
     }
   }
diff --git a/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java b/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java
deleted file mode 100644
index 158f4665..00000000
--- a/inject/src/main/java/io/avaje/inject/spi/LazyProvider.java
+++ /dev/null
@@ -1,35 +0,0 @@
-package io.avaje.inject.spi;
-
-import jakarta.inject.Provider;
-
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * Wrap a Provider for lazy initialisation.
- */
-final class LazyProvider<T> implements Provider<T> {
-
-  private final ReentrantLock lock = new ReentrantLock();
-  private final Provider<T> original;
-  private T lazy;
-
-  LazyProvider(Provider<T> original) {
-    this.original = original;
-  }
-
-  @Override
-  public T get() {
-    if (lazy != null) {
-      return lazy;
-    }
-    lock.lock();
-    try {
-      if (lazy == null) {
-        lazy = original.get();
-      }
-      return lazy;
-    } finally {
-      lock.unlock();
-    }
-  }
-}


Test patch:
diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java
index c6b4c816..08555ba6 100644
--- a/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java
+++ b/blackbox-test-inject/src/main/java/org/example/myapp/lazy/LazyBean.java
@@ -19,7 +19,6 @@ public class LazyBean {
 
   @PostConstruct
   void init(BeanScope scope) {
-
     // note that nested test scopes will not be lazy
     if (initialized != null) initialized.set(true);
   }
diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java
index 5095fe42..f5c9a8d4 100644
--- a/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java
+++ b/blackbox-test-inject/src/test/java/org/example/myapp/lazy/LazyTest.java
@@ -15,9 +15,12 @@ class LazyTest {
     var initialized = new AtomicBoolean();
     try (var scope = BeanScope.builder().beans(initialized).build()) {
       assertThat(initialized).isFalse();
-      LazyBean prov = scope.get(LazyBean.class, "single");
+      LazyBean lazy = scope.get(LazyBean.class, "single");
       assertThat(initialized).isTrue();
-      assertThat(prov).isNotNull();
+      assertThat(lazy).isNotNull();
+
+      LazyBean lazyAgain = scope.get(LazyBean.class, "single");
+      assertThat(lazyAgain).isSameAs(lazy);
     }
   }
 
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
index 176d6da1..5253c358 100644
--- a/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProvider.java
@@ -1,9 +1,11 @@
 package org.example.coffee.provider;
 
+import java.util.Random;
 import java.util.function.Supplier;
 
 import io.avaje.inject.Bean;
 import io.avaje.inject.Factory;
+import io.avaje.inject.Prototype;
 import io.avaje.inject.Secondary;
 import jakarta.inject.Named;
 import jakarta.inject.Provider;
@@ -22,4 +24,10 @@ public class FactoryProvider {
   Provider<Supplier<String>> supply() {
     return () -> () -> "Stand proud Provider, you were strong";
   }
+
+  @Bean
+  @Prototype
+  Provider<Long> random() {
+    return new Random()::nextLong;
+  }
 }
diff --git a/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
index b1868a14..5b2e6e74 100644
--- a/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
+++ b/inject-test/src/test/java/org/example/coffee/provider/FactoryProviderTest.java
@@ -19,4 +19,12 @@ class FactoryProviderTest {
       assertThat(scope.get(String.class, "second")).isEqualTo("Nah, I'd win");
     }
   }
+
+  @Test
+  void testProtoType() {
+    try (var scope = BeanScope.builder().build()) {
+      var numberGetter = scope.get(ProtoTypeNumberGetter.class);
+      assertThat(numberGetter.number()).isNotEqualTo(numberGetter.number());
+    }
+  }
 }
diff --git a/inject-test/src/test/java/org/example/coffee/provider/ProtoTypeNumberGetter.java b/inject-test/src/test/java/org/example/coffee/provider/ProtoTypeNumberGetter.java
new file mode 100644
index 00000000..358d7d7a
--- /dev/null
+++ b/inject-test/src/test/java/org/example/coffee/provider/ProtoTypeNumberGetter.java
@@ -0,0 +1,18 @@
+package org.example.coffee.provider;
+
+import jakarta.inject.Provider;
+import jakarta.inject.Singleton;
+
+@Singleton
+class ProtoTypeNumberGetter {
+
+  private final Provider<Long> nProv;
+
+  ProtoTypeNumberGetter(Provider<Long> nProv) {
+    this.nProv = nProv;
+  }
+
+  Long number() {
+    return nProv.get();
+  }
+}