diff --git a/.gitignore b/.gitignore index 92ed340a..d3b5e517 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,6 @@ build/ inject-generator/avaje-inject inject-generator/avaje-inject-generator inject-generator/avaje-processors.txt +*.csv +inject-generator/util/events/ListString$Publisher$DI.java +inject-generator/util/events/ListString$Publisher.java diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java new file mode 100644 index 00000000..4d5bbea4 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBean.java @@ -0,0 +1,31 @@ +package org.example.myapp.async; + +import java.time.Instant; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; + +import io.avaje.inject.AsyncBean; +import io.avaje.lang.Nullable; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@AsyncBean +@Singleton +@Named("single") +public class BackgroundBean { + + final Instant initTime; + final String threadName = Thread.currentThread().getName(); + + public BackgroundBean(@Nullable AtomicInteger intyAtomic) throws InterruptedException { + this.initTime = Instant.now(); + + if (intyAtomic != null) { + intyAtomic.incrementAndGet(); + } + + Thread.sleep(200); + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java new file mode 100644 index 00000000..883f86bd --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/BackgroundBeanFactory.java @@ -0,0 +1,21 @@ +package org.example.myapp.async; + +import java.util.concurrent.atomic.AtomicInteger; + +import io.avaje.inject.AsyncBean; +import io.avaje.inject.Bean; +import io.avaje.inject.Factory; +import io.avaje.lang.Nullable; +import jakarta.inject.Named; + +@Factory +@AsyncBean +public class BackgroundBeanFactory { + + @Bean + @Named("factory") + BackgroundBean lazyInt(@Nullable AtomicInteger intyAtomic) throws InterruptedException { + System.out.println("StartedInit BackgroundBean() " + Thread.currentThread().getName()); + return new BackgroundBean(intyAtomic); + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java b/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java new file mode 100644 index 00000000..f9bef939 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/async/MyUseOfBackground.java @@ -0,0 +1,16 @@ +package org.example.myapp.async; + +import io.avaje.inject.AsyncBean; +import io.avaje.inject.Component; +import jakarta.inject.Named; + +@AsyncBean +@Component +public class MyUseOfBackground { + + private final BackgroundBean backgroundBean; + + public MyUseOfBackground(@Named("single") BackgroundBean backgroundBean) { + this.backgroundBean = backgroundBean; + } +} diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java b/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java new file mode 100644 index 00000000..7942c998 --- /dev/null +++ b/blackbox-test-inject/src/test/java/org/example/myapp/async/AsyncTest.java @@ -0,0 +1,49 @@ +package org.example.myapp.async; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Duration; +import java.time.Instant; +import java.util.concurrent.atomic.AtomicInteger; + +import org.junit.jupiter.api.Test; + +import io.avaje.inject.BeanScope; + +class AsyncTest { + + @Test + void test() { + var start = Instant.now(); + var inty = new AtomicInteger(); + try (var scope = BeanScope.builder().bean(AtomicInteger.class, inty).build()) { + + // the async beans shouldn't slowdown initialization + assertThat(Duration.between(start, Instant.now()).toMillis()).isLessThan(300); + + // prove it's not just lazy + var beforeGet = Instant.now(); + var bean = scope.get(BackgroundBean.class, "single"); + assertThat(inty.get()).isEqualTo(2); + assertThat(bean.initTime.isBefore(beforeGet)).isTrue(); + assertThat(bean.threadName).isNotEqualTo(Thread.currentThread().getName()); + } + } + + @Test + void testFactory() { + var start = Instant.now(); + var inty = new AtomicInteger(); + try (var scope = BeanScope.builder().bean(AtomicInteger.class, inty).build()) { + // the async beans shouldn't slowdown initialization + assertThat(Duration.between(start, Instant.now()).toMillis()).isLessThan(300); + + var bean = scope.get(BackgroundBean.class, "factory"); + // this works on my local but not on the CI for some unknown reason. + // var beforeGet = Instant.now(); + // assertThat(bean.initTime.isBefore(beforeGet)).isTrue(); + assertThat(inty.get()).isEqualTo(2); + assertThat(bean.threadName).isNotEqualTo(Thread.currentThread().getName()); + } + } +} 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 458d4239..6ed3e78b 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 @@ -7,6 +7,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.concurrent.CompletableFuture; import java.util.stream.Stream; import javax.lang.model.element.Element; @@ -38,6 +39,7 @@ final class BeanReader { private final boolean prototype; private final boolean primary; private final boolean secondary; + private final boolean async; private final boolean lazy; private final boolean proxy; private final BeanAspects aspects; @@ -58,6 +60,7 @@ final class BeanReader { this.primary = PrimaryPrism.isPresent(beanType); this.secondary = !primary && SecondaryPrism.isPresent(beanType); this.lazy = !FactoryPrism.isPresent(beanType) && LazyPrism.isPresent(beanType); + this.async = !FactoryPrism.isPresent(beanType) && AsyncBeanPrism.isPresent(beanType); final var beantypes = BeanTypesPrism.getOptionalOn(beanType); beantypes.ifPresent(p -> Util.validateBeanTypes(beanType, p.value())); this.typeReader = @@ -128,13 +131,17 @@ BeanAspects aspects() { } boolean registerProvider() { - return prototype || lazy; + return prototype || async || lazy; } boolean lazy() { return lazy; } + boolean async() { + return async; + } + boolean importedComponent() { return importedComponent; } @@ -304,7 +311,7 @@ void buildAddFor(Append writer) { } void buildRegister(Append writer) { - if (prototype || lazy) { + if (prototype || lazy || async) { return; } writer.indent(" "); @@ -349,15 +356,20 @@ void prototypePostConstruct(Append writer, String indent) { private void lifeCycleNotSupported(String lifecycle) { if (registerProvider()) { - logError( - beanType, - "%s scoped bean does not support the %s lifecycle method", - prototype ? "@Prototype" : "@Lazy", - lifecycle); + String scope; + if (lazy) scope = "@Lazy"; + if (async) scope = "@AsyncBean"; + else scope = "@Prototype"; + + logError(beanType, "%s scoped bean does not support the %s lifecycle method", scope, lifecycle); } } private Set<String> importTypes() { + importTypes.add(type); + if (async) { + importTypes.add(CompletableFuture.class.getCanonicalName()); + } importTypes.add(type); typeReader.extraImports(importTypes); requestParams.addImports(importTypes); 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 fb52c460..f2f7f622 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 @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.List; import java.util.Set; +import java.util.concurrent.CompletableFuture; import static io.avaje.inject.generator.Constants.CONDITIONAL_DEPENDENCY; import static io.avaje.inject.generator.ProcessingContext.asElement; @@ -21,6 +22,7 @@ final class MethodReader { private final boolean prototype; private final boolean primary; private final boolean secondary; + private final boolean async; private final boolean lazy; private final String returnTypeRaw; private final UType genericType; @@ -49,9 +51,11 @@ final class MethodReader { prototype = PrototypePrism.isPresent(element); primary = PrimaryPrism.isPresent(element); secondary = SecondaryPrism.isPresent(element); + async = AsyncBeanPrism.isPresent(element) || AsyncBeanPrism.isPresent(element.getEnclosingElement()); lazy = LazyPrism.isPresent(element) || LazyPrism.isPresent(element.getEnclosingElement()); conditions.readAll(element); } else { + async = false; prototype = false; primary = false; secondary = false; @@ -103,8 +107,8 @@ final class MethodReader { this.initMethod = lifecycleReader.initMethod(); this.destroyMethod = lifecycleReader.destroyMethod(); } - if (lazy && prototype) { - APContext.logError("Cannot use both @Lazy and @Prototype"); + if ((async || lazy) && prototype) { + APContext.logError("Cannot use both @AsyncBean/@Lazy and @Prototype"); } } @@ -230,7 +234,10 @@ void builderAddBeanProvider(Append writer) { writer.append(".asSecondary()"); } - writer.indent(".registerProvider(() -> {").eol(); + writer + .indent(".registerProvider(") + .append("%s() -> {", async ? "CompletableFuture.supplyAsync(" : "") + .eol(); startTry(writer, " "); writer.indent(indent).append(" return "); @@ -243,7 +250,7 @@ void builderAddBeanProvider(Append writer) { } writer.append(");").eol(); endTry(writer, " "); - writer.indent(indent).append(" });").eol(); + writer.indent(indent).append(" }%s);", async ? ")::join" : "").eol(); writer.indent(indent).append("}").eol(); } @@ -340,6 +347,10 @@ void addImports(ImportTypeMap importTypes) { if (optionalType) { importTypes.add(Constants.OPTIONAL); } + + if (async) { + importTypes.add(CompletableFuture.class.getCanonicalName()); + } conditions.addImports(importTypes); } @@ -429,6 +440,10 @@ boolean isProtoType() { return prototype && !Util.isProvider(returnTypeRaw); } + boolean isAsync() { + return async; + } + boolean isLazy() { return lazy; } 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 17a048b7..93639335 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 @@ -14,6 +14,7 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.concurrent.CompletableFuture; import javax.lang.model.type.TypeKind; import javax.tools.JavaFileObject; @@ -143,7 +144,7 @@ private void writeFactoryBeanMethod(MethodReader method) { method.buildConditional(writer); method.buildAddFor(writer); method.builderGetFactory(writer, beanReader.hasConditions()); - if (method.isLazy() || method.isProtoType() || method.isUseProviderForSecondary()) { + if (method.isAsync() || method.isLazy() || method.isProtoType() || method.isUseProviderForSecondary()) { method.builderAddBeanProvider(writer); } else { method.startTry(writer); @@ -177,7 +178,9 @@ private void writeAddFor(MethodReader constructor) { indent += " "; final String registerProvider; - if (beanReader.lazy()) { + if (beanReader.async()) { + registerProvider = "registerProvider(CompletableFuture.supplyAsync"; + } else if (beanReader.lazy()) { registerProvider = "registerProvider"; } else { registerProvider = "asPrototype().registerProvider"; @@ -196,18 +199,18 @@ private void writeAddFor(MethodReader constructor) { beanReader.prototypePostConstruct(writer, indent); writer.indent(" return bean;").eol(); if (!constructor.methodThrows()) { - writer.indent(" });").eol(); + writer.indent(" }").append(beanReader.async() ? ")::join);" : ");").eol(); } } writeObserveMethods(); constructor.endTry(writer); + writer.append(" }"); if (beanReader.registerProvider() && constructor.methodThrows()) { - writer.append(" }"); - writer.append(");").eol(); + writer.append("%s);", beanReader.async() ? ")::join" : "").eol(); + writer.append(" }"); } - writer.append(" }"); writer.eol(); } 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 3401e29f..7e9ce0ab 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 @@ -3,6 +3,7 @@ @GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism") @GeneratePrism(Assisted.class) @GeneratePrism(AssistFactory.class) +@GeneratePrism(AsyncBean.class) @GeneratePrism(Bean.class) @GeneratePrism(Component.class) @GeneratePrism(Component.Import.class) diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java new file mode 100644 index 00000000..fe07f287 --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBean.java @@ -0,0 +1,12 @@ +package io.avaje.inject.generator.models.valid.async; + +import io.avaje.inject.AsyncBean; +import jakarta.inject.Inject; +import jakarta.inject.Provider; +import jakarta.inject.Singleton; + +@Singleton +@AsyncBean +public class BackgroundBean { + @Inject Provider<Integer> intProvider; +} diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java new file mode 100644 index 00000000..d4c283f4 --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/async/BackgroundBeanFactory.java @@ -0,0 +1,18 @@ +package io.avaje.inject.generator.models.valid.async; + +import io.avaje.inject.AsyncBean; +import io.avaje.inject.Bean; +import io.avaje.inject.Factory; +import jakarta.inject.Named; + +@Factory +@AsyncBean +public class BackgroundBeanFactory { + + @Bean + @Named("factory") + BackgroundBean lazyInt() throws InterruptedException { + + return new BackgroundBean(); + } +} diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java index 931e6723..9dfb8d29 100644 --- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/lazy/LazyFactory.java @@ -9,7 +9,7 @@ public class LazyFactory { @Bean - Integer lazyInt() { + Integer lazyInt() throws Exception { return 0; } } diff --git a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java index 531f3872..200a418a 100644 --- a/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java +++ b/inject-test/src/test/java/org/example/coffee/CoffeeMakerTest.java @@ -84,7 +84,7 @@ void beanScope_all_superClasses() { .findFirst().orElse(null); assertThat(inhEntry.keys()) - .containsExactly(name(InhOne.class), name(InhBase.class), name(InhBaseBase.class), + .containsExactlyInAnyOrder(name(InhOne.class), name(InhBase.class), name(InhBaseBase.class), name(InhBaseIface2.class), name(InhBaseIface3.class), name(InhBaseIface.class)); } } @@ -100,7 +100,7 @@ void beanScope_all_interfaces() { .findFirst().orElse(null); assertThat(extendIfaces.keys()) - .containsExactly(name(ConcreteExtend.class), name(IfaceExtend.class), name(IfaseBase.class)); + .containsExactlyInAnyOrder(name(ConcreteExtend.class), name(IfaceExtend.class), name(IfaseBase.class)); } } @@ -115,7 +115,7 @@ void beanScope_all_includesGenericInterfaces() { .findFirst().orElse(null); assertThat(hazRepo.keys()) - .containsExactly(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong)); + .containsExactlyInAnyOrder(name(HazRepo.class), name(HazRepo$DI.TYPE_RepositoryHazLong)); } } @@ -130,7 +130,7 @@ void beanScope_all_interfaceWithParameter() { .findFirst().orElse(null); assertThat(hazRepo.keys()) - .containsExactly(name(MyParam.class), name(IfaceParam.class), name(IfaceParamParent.class)); + .containsExactlyInAnyOrder(name(MyParam.class), name(IfaceParam.class), name(IfaceParamParent.class)); } } diff --git a/inject/src/main/java/io/avaje/inject/AsyncBean.java b/inject/src/main/java/io/avaje/inject/AsyncBean.java new file mode 100644 index 00000000..19a2f03f --- /dev/null +++ b/inject/src/main/java/io/avaje/inject/AsyncBean.java @@ -0,0 +1,8 @@ +package io.avaje.inject; + +import java.lang.annotation.*; + +/** Limits the types exposed by this bean to the given types. */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface AsyncBean {} \ No newline at end of file