From 28823e1ec9a7abf6c6621d1333e413b230fbc3a3 Mon Sep 17 00:00:00 2001 From: Tomas Langer Date: Mon, 16 Dec 2024 22:23:58 +0100 Subject: [PATCH] New main class generator, binding generator. All tests passing in services --- service/README.md | 13 + .../service/codegen/ServiceCodegenTypes.java | 29 ++ .../codegen/ServiceDescriptorCodegen.java | 124 +++++- service/maven-plugin/README.md | 21 +- .../maven/plugin/BindingGenerator.java | 419 ++++++++++-------- .../plugin/CreateApplicationAbstractMojo.java | 71 ++- .../plugin/CreateTestApplicationMojo.java | 5 + .../service/maven/plugin/MainGenerator.java | 97 ++++ service/registry/pom.xml | 4 + .../helidon/service/registry/Activators.java | 213 +++++---- .../service/registry/ActivatorsPerLookup.java | 45 +- .../io/helidon/service/registry/Binding.java | 19 +- .../registry/BindingDependencyContext.java | 17 + .../io/helidon/service/registry/Bindings.java | 291 ++++++++++++ .../service/registry/CoreServiceRegistry.java | 45 +- .../service/registry/DependencyBlueprint.java | 30 +- .../registry/DependencyCardinality.java | 19 + .../registry/DependencyPlanBinder.java | 125 +----- .../service/registry/InjectionContext.java | 45 -- .../registry/InstanceNameServiceManager.java | 17 + .../registry/InterceptionMetadataImpl.java | 4 +- .../io/helidon/service/registry/IpPlan.java | 34 -- .../registry/RegistryStartupProvider.java | 102 +++++ .../service/registry/ServiceManager.java | 19 +- .../service/registry/ServicePlanBinder.java | 202 --------- .../service/registry/ServiceProvider.java | 146 +----- .../ServiceRegistryConfigBlueprint.java | 32 +- .../registry/ServiceRegistryManager.java | 301 ++++++++----- .../service/registry/ServiceSupplies.java | 24 +- .../registry/SupplierDependencyContext.java | 18 + .../registry/src/main/java/module-info.java | 4 + .../codegen/ServiceCodegenTypesTest.java | 11 + .../src/it/projects/test1/pom.xml | 1 + .../src/it/projects/test2/pom.xml | 1 + .../src/it/projects/test3/pom.xml | 1 - .../inject/maven/plugin/ProjectsTestIT.java | 44 +- .../test/registry/ServiceSupplier.java | 2 +- .../service/tests/toolbox/AbstractSaw.java | 42 +- .../service/tests/toolbox/TableSaw.java | 6 +- .../service/tests/toolbox/ToolBox.java | 2 +- .../tests/toolbox/impl/MainToolBox.java | 12 +- .../service/tests/toolbox/ToolBoxTest.java | 19 +- 42 files changed, 1617 insertions(+), 1059 deletions(-) create mode 100644 service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/MainGenerator.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/BindingDependencyContext.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/Bindings.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/DependencyCardinality.java delete mode 100644 service/registry/src/main/java/io/helidon/service/registry/InjectionContext.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/InstanceNameServiceManager.java delete mode 100644 service/registry/src/main/java/io/helidon/service/registry/IpPlan.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/RegistryStartupProvider.java delete mode 100644 service/registry/src/main/java/io/helidon/service/registry/ServicePlanBinder.java create mode 100644 service/registry/src/main/java/io/helidon/service/registry/SupplierDependencyContext.java diff --git a/service/README.md b/service/README.md index daa46673506..42af5637451 100644 --- a/service/README.md +++ b/service/README.md @@ -625,6 +625,19 @@ Service instance based (to obtain registry metadata in addition to the instance) 2. `Optional>` 3. `List>` +## Why are some dependency options not supported + +We do not support dependencies of types `List>` and `Optional>`, even though other frameworks (such as CDI) do have similar concepts. + +The reason is that we resolve all injection points as late as possible, and injecting these types would require an early resolution of instances. + +As we support optional factories (i.e. `Supplier>`), and the concept of `ServicesFactory`, we +do not know how many instances (and if any) are available at the time of injecting to a service. + +So if we supported `List>`, we would still need to resolve all the instances, so just use `List` +for this purpose. If a supplier is need to break dependency cycle, use `Supplier>`, and you will get instances +resolved only once you call `get()` on the supplier. + # Glossary | Term | Description | diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java index 1e0e11cea24..266a5e7e108 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceCodegenTypes.java @@ -161,6 +161,11 @@ public final class ServiceCodegenTypes { */ public static final TypeName SERVICE_CONFIG = TypeName.create("io.helidon.service.registry.ServiceRegistryConfig"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.ServiceRegistryConfig.Builder}. + */ + public static final TypeName SERVICE_CONFIG_BUILDER = + TypeName.create("io.helidon.service.registry.ServiceRegistryConfig.Builder"); /** * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.ServiceRegistry}. */ @@ -171,6 +176,16 @@ public final class ServiceCodegenTypes { */ public static final TypeName SERVICE_REGISTRY_MANAGER = TypeName.create("io.helidon.service.registry.ServiceRegistryManager"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.DependencyCardinality}. + */ + public static final TypeName DEPENDENCY_CARDINALITY = + TypeName.create("io.helidon.service.registry.DependencyCardinality"); + /** + * {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.DependencyCardinality}. + */ + public static final TypeName SERVICE_LOADER_DESCRIPTOR = + TypeName.create("io.helidon.service.registry.ServiceLoader__ServiceDescriptor"); /** * {@link io.helidon.common.types.TypeName} for * {@code io.helidon.service.registry.GeneratedService.PerInstanceDescriptor}. @@ -286,6 +301,12 @@ public final class ServiceCodegenTypes { public static final TypeName INTERCEPT_G_WRAPPER_QUALIFIED_FACTORY = TypeName.create("io.helidon.service.registry.GeneratedService.QualifiedFactoryInterceptionWrapper"); + /** + * {@link io.helidon.common.types.TypeName} for + * {@code io.helidon.service.registry.RegistryStartupProvider}. + */ + public static final TypeName REGISTRY_STARTUP_PROVIDER = + TypeName.create("io.helidon.service.registry.RegistryStartupProvider"); /** * A Set of Qualifier. */ @@ -312,6 +333,12 @@ public final class ServiceCodegenTypes { public static final TypeName SET_OF_STRINGS = TypeName.builder(TypeNames.SET) .addTypeArgument(TypeNames.STRING) .build(); + /** + * A list of Double. + */ + public static final TypeName LIST_OF_DOUBLES = TypeName.builder(TypeNames.LIST) + .addTypeArgument(TypeNames.BOXED_DOUBLE) + .build(); /** * A list of Dependency. */ @@ -334,3 +361,5 @@ public final class ServiceCodegenTypes { private ServiceCodegenTypes() { } } + + diff --git a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceDescriptorCodegen.java b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceDescriptorCodegen.java index f119862484f..4d703c2fdb3 100644 --- a/service/codegen/src/main/java/io/helidon/service/codegen/ServiceDescriptorCodegen.java +++ b/service/codegen/src/main/java/io/helidon/service/codegen/ServiceDescriptorCodegen.java @@ -61,6 +61,7 @@ import static io.helidon.service.codegen.CodegenHelper.annotationsField; import static io.helidon.service.codegen.ServiceCodegenTypes.ANY_GENERIC_TYPE; import static io.helidon.service.codegen.ServiceCodegenTypes.BUILDER_BLUEPRINT; +import static io.helidon.service.codegen.ServiceCodegenTypes.DEPENDENCY_CARDINALITY; import static io.helidon.service.codegen.ServiceCodegenTypes.GENERIC_T_TYPE; import static io.helidon.service.codegen.ServiceCodegenTypes.INTERCEPTION_DELEGATE; import static io.helidon.service.codegen.ServiceCodegenTypes.INTERCEPTION_EXTERNAL_DELEGATE; @@ -815,7 +816,7 @@ private List toMethodParams(DescribedService service, return method.parameterArguments() .stream() .map(param -> { - String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + String constantName = "DEP_" + paramCounter.getAndIncrement(); var assignment = translateParameter(param.typeName(), constantName); return new ParamDefinition(method, methodConstantName, @@ -848,7 +849,7 @@ private void injectConstructorParams(DescribedService service, constructor.parameterArguments() .stream() .map(param -> { - String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + String constantName = "DEP_" + paramCounter.getAndIncrement(); var assignment = translateParameter(param.typeName(), constantName); return new ParamDefinition(constructor, null, @@ -877,7 +878,7 @@ private void fieldParam(DescribedService describedService, List result, AtomicInteger paramCounter, TypedElementInfo field) { - String constantName = "IP_PARAM_" + paramCounter.getAndIncrement(); + String constantName = "DEP_" + paramCounter.getAndIncrement(); var assignment = translateParameter(field.typeName(), constantName); result.add(new ParamDefinition(field, @@ -1069,6 +1070,7 @@ private void injectionPointFields(ClassModel.Builder classModel, List params) { // constant for injection points for (ParamDefinition param : params) { + DependencyMetadata dependencyMetadata = dependencyMetadata(param); classModel.addField(field -> field .accessModifier(AccessModifier.PUBLIC) .isStatic(true) @@ -1145,6 +1147,25 @@ private void injectionPointFields(ClassModel.Builder classModel, } } + if (!dependencyMetadata.cardinality.equals("REQUIRED")) { + // only set if not default + it.addContent(".cardinality(") + .addContent(DEPENDENCY_CARDINALITY) + .addContent(".") + .addContent(dependencyMetadata.cardinality()) + .addContentLine(")"); + } + if (dependencyMetadata.serviceInstance()) { + it.addContent(".isServiceInstance(") + .addContent(String.valueOf(dependencyMetadata.serviceInstance())) + .addContentLine(")"); + } + if (dependencyMetadata.supplier()) { + it.addContent(".isSupplier(") + .addContent(String.valueOf(dependencyMetadata.supplier())) + .addContentLine(")"); + } + it.addContent(".build()") .decreaseContentPadding() .decreaseContentPadding(); @@ -1152,6 +1173,98 @@ private void injectionPointFields(ClassModel.Builder classModel, } } + private DependencyMetadata dependencyMetadata(ParamDefinition param) { + TypeName declared = param.declaredType(); + + // supplier is honored only on the first level (i.e. we do not support Optional or List) + boolean isSupplier = declared.isSupplier(); + boolean isServiceInstance; + /* + REQUIRED + OPTIONAL + LIST + */ + String cardinality; + + if (isSupplier) { + // Supplier, Supplier, Supplier, Supplier> ... + declared = declared.typeArguments().getFirst(); + } + + if (declared.isSupplier()) { + throw new CodegenException("Dependency is declared as a Supplier> - this is not supported", + param.elementInfo().originatingElementValue()); + } + + if (declared.isOptional()) { + // lists cannot be optional + cardinality = "OPTIONAL"; + TypeName actualContract = declared.typeArguments().getFirst(); + if (actualContract.isSupplier()) { + throw new CodegenException("Dependency has Optional> - this is not supported, please use " + + "Supplier", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isOptional()) { + throw new CodegenException("Dependency has Optional> - this is not supported", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isList()) { + throw new CodegenException("Dependency has Optional> - this is not supported, Lists are empty if " + + "no service satisfies them, so please use List<> instead", + param.elementInfo().originatingElementValue()); + } + isServiceInstance = isServiceInstance(actualContract); + } else if (isServiceInstance(declared)) { + // service instance is a direct map to an instance, so cannot be list or optional + cardinality = "REQUIRED"; + isServiceInstance = true; + + TypeName actualContract = declared.typeArguments().getFirst(); + if (actualContract.isSupplier()) { + throw new CodegenException("Dependency has ServiceInstance> - this is not supported, please use " + + "Supplier>", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isOptional()) { + throw new CodegenException("Dependency has ServiceInstance> - this is not supported, please use " + + "Optional>", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isList()) { + throw new CodegenException("Dependency has ServiceInstance> - this is not supported, please use" + + "List>", + param.elementInfo().originatingElementValue()); + } + } else if (declared.isList()) { + cardinality = "LIST"; + TypeName actualContract = declared.typeArguments().getFirst(); + if (actualContract.isSupplier()) { + throw new CodegenException("Dependency has List> - this is not supported, please use " + + "Supplier>", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isOptional()) { + throw new CodegenException("Dependency has List> - this is not supported", + param.elementInfo().originatingElementValue()); + } + if (actualContract.isList()) { + throw new CodegenException("Dependency has List> - this is not supported", + param.elementInfo().originatingElementValue()); + } + isServiceInstance = isServiceInstance(actualContract); + } else { + cardinality = "REQUIRED"; + isServiceInstance = false; + } + + return new DependencyMetadata(cardinality, isServiceInstance, isSupplier); + } + + private boolean isServiceInstance(TypeName typeName) { + return typeName.equals(SERVICE_SERVICE_INSTANCE); + } + private String ipIdDescription(TypeInfo service, ParamDefinition param) { TypeName serviceType = service.typeName(); StringBuilder result = new StringBuilder("Injection point dependency for "); @@ -2366,6 +2479,11 @@ private void addTypeConstant(ClassModel.Builder classModel, ); } + private record DependencyMetadata(String cardinality, + boolean serviceInstance, + boolean supplier) { + } + private record GenericTypeDeclaration(String constantName, TypeName typeName) { } diff --git a/service/maven-plugin/README.md b/service/maven-plugin/README.md index fc8a79d5955..e7368a81229 100644 --- a/service/maven-plugin/README.md +++ b/service/maven-plugin/README.md @@ -10,8 +10,25 @@ The Helidon Service Maven Plugin provides the following goals: This goal creates artifacts that are only valid for the service (assembled from libraries and its own sources). This goal generates: -1. Application Binding - a mapping of services to injection points (to bypass runtime lookups) - generates class `Injection__Binding` -2. Application Main - a generated main class that registers all services (to bypass service discovery) - generates class `ApplicationMain` +1. Application Binding - a mapping of services to injection points (to bypass runtime lookups), and of service descriptors (to bypass runtime service discovery and reflection) - generates class `Application__Binding` + +To create a custom main class, use the following: +```java +public static void main(String[] args) { + // this will disable service discovery + ServiceRegistryManager.start(Application__Binding.create()); + // alternative - to provide custom configuration; the configuration will only be updated from the generated binding class, + // all other options in the config will be left intact + // this will honor service discovery configured in the config! + // also if any run level is configured, run levels will not be modified; if you do not want to run any run level annotated + // service, simply set max run level to 0 + ServiceRegistryManager.start(Application__Binding.create(), ServiceRegistryConfig.builder().build()); +} +``` + +To use a generated main class, enable it with the maven plugin, an `ApplicationMain` class will be code generated with +a similar content. + Usage of this plugin goal is not required, yet it is recommended for final application module, it will add - binding for injection points, to avoid runtime lookups diff --git a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/BindingGenerator.java b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/BindingGenerator.java index abe97250e62..1fc4b2a08b6 100644 --- a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/BindingGenerator.java +++ b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/BindingGenerator.java @@ -18,6 +18,8 @@ import java.nio.file.Path; import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashMap; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.LinkedHashSet; @@ -25,6 +27,8 @@ import java.util.Map; import java.util.Objects; import java.util.Set; +import java.util.TreeSet; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import io.helidon.codegen.CodegenException; @@ -32,33 +36,31 @@ import io.helidon.codegen.classmodel.ClassModel; import io.helidon.codegen.classmodel.ContentBuilder; import io.helidon.codegen.classmodel.Method; -import io.helidon.codegen.compiler.Compiler; -import io.helidon.codegen.compiler.CompilerOptions; import io.helidon.common.types.AccessModifier; import io.helidon.common.types.Annotation; import io.helidon.common.types.Annotations; -import io.helidon.common.types.ElementKind; import io.helidon.common.types.ResolvedType; -import io.helidon.common.types.TypeInfo; import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; import io.helidon.service.codegen.DescriptorClassCode; import io.helidon.service.codegen.HelidonMetaInfServices; -import io.helidon.service.codegen.RegistryCodegenContext; -import io.helidon.service.codegen.ServiceDescriptorCodegen; import io.helidon.service.metadata.DescriptorMetadata; import io.helidon.service.registry.Dependency; import io.helidon.service.registry.Interception; import io.helidon.service.registry.Lookup; import io.helidon.service.registry.Qualifier; +import io.helidon.service.registry.Service.RunLevel; +import io.helidon.service.registry.ServiceDescriptor; import io.helidon.service.registry.ServiceInfo; import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; -import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_CONTRACT; -import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_PROVIDER; +import static io.helidon.service.codegen.ServiceCodegenTypes.LIST_OF_DOUBLES; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_ANNOTATION_RUN_LEVEL; import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_BINDING; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_CONFIG_BUILDER; import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_INJECTION_POINT_FACTORY; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_LOADER_DESCRIPTOR; import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_PLAN_BINDER; -import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_SERVICE_INSTANCE; /** * The default implementation for {@link BindingGenerator}. @@ -81,18 +83,18 @@ class BindingGenerator { * @param serviceTypes types to process * @param typeName generated binding type name * @param moduleName name of the module of this maven module - * @param compilerOptions compilation options + * @param sourcesToCompile list of sources to compile */ void createBinding(WrappedServices injectionServices, Set serviceTypes, TypeName typeName, String moduleName, - CompilerOptions compilerOptions) { + List sourcesToCompile) { Objects.requireNonNull(injectionServices); Objects.requireNonNull(serviceTypes); try { - codegen(injectionServices, serviceTypes, typeName, moduleName, compilerOptions); + codegen(injectionServices, serviceTypes, typeName, moduleName, sourcesToCompile); } catch (CodegenException ce) { handleError(ce); } catch (Throwable te) { @@ -104,7 +106,7 @@ void codegen(WrappedServices injectionServices, Set serviceTypes, TypeName typeName, String moduleName, - CompilerOptions compilerOptions) { + List sourcesToCompile) { ClassModel.Builder classModel = ClassModel.builder() .accessModifier(AccessModifier.PACKAGE_PRIVATE) .copyright(CodegenUtil.copyright(GENERATOR, @@ -121,7 +123,7 @@ void codegen(WrappedServices injectionServices, // deprecated default constructor - binding should always be service loaded classModel.addConstructor(ctr -> ctr - .accessModifier(AccessModifier.PACKAGE_PRIVATE)); + .accessModifier(AccessModifier.PRIVATE)); // public String name() classModel.addMethod(nameMethod -> nameMethod @@ -130,40 +132,56 @@ void codegen(WrappedServices injectionServices, .name("name") .addContentLine("return \"" + moduleName + "\";")); - // public void configure(ServiceInjectionPlanBinder binder) - classModel.addMethod(configureMethod -> configureMethod + // public void binding(DependencyPlanBinder binder) + classModel.addMethod(bindingMethod -> bindingMethod .addAnnotation(Annotations.OVERRIDE) - .name("configure") + .name("binding") // constructors of services for service loader are usually deprecated in Helidon .addAnnotation(Annotation.create(SuppressWarnings.class, "deprecation")) .addParameter(binderParam -> binderParam .name("binder") .type(SERVICE_PLAN_BINDER)) + .update(it -> createBindingMethodBody(injectionServices, + serviceTypes, + it))); + + // public void configure(ServiceRegistryConfig.Builder builder) + classModel.addMethod(configureMethod -> configureMethod + .addAnnotation(Annotations.OVERRIDE) + .name("configure") + .addParameter(configBuilder -> configBuilder + .name("builder") + .type(SERVICE_CONFIG_BUILDER) + ) .update(it -> createConfigureMethodBody(injectionServices, - serviceTypes, - it))); + classModel, + it)) + ); + // static Application__Binding create() + classModel.addMethod(create -> create + .accessModifier(AccessModifier.PACKAGE_PRIVATE) + .returnType(typeName) + .name("create") + .isStatic(true) + .description("Create a new application binding instance.") + .addContent("return new ") + .addContent(typeName.className()) + .addContentLine("();") + ); Path generated = ctx.filer() .writeSourceFile(classModel.build()); - TypeInfo appTypeInfo = createAppTypeInfo(typeName); - RegistryCodegenContext registryCodegenContext = RegistryCodegenContext.create(ctx); MavenRoundContext roundContext = new MavenRoundContext(ctx); - ServiceDescriptorCodegen descriptorCodegen = ServiceDescriptorCodegen.create(registryCodegenContext); - descriptorCodegen.service(GENERATOR, - roundContext, - List.of(appTypeInfo), - appTypeInfo); - List toCompile = new ArrayList<>(); - toCompile.add(generated); + sourcesToCompile.add(generated); HelidonMetaInfServices services = HelidonMetaInfServices.create(ctx.filer(), moduleName); for (DescriptorClassCode descriptor : roundContext.descriptors()) { Path path = ctx.filer().writeSourceFile(descriptor.classCode().classModel().build(), descriptor.classCode().originatingElements()); - toCompile.add(path); + sourcesToCompile.add(path); services.add(DescriptorMetadata.create(descriptor.registryType(), descriptor.classCode().newType(), @@ -173,7 +191,6 @@ void codegen(WrappedServices injectionServices, } services.write(); - Compiler.compile(compilerOptions, toCompile.toArray(new Path[0])); } BindingPlan bindingPlan(WrappedServices services, @@ -229,7 +246,7 @@ private static Consumer> toContentBuilder(ServiceInfo serviceI .addContent(")"); } else { // the usual singleton instance - return it -> it.addContent(serviceInfo.descriptorType().fqName()) + return it -> it.addContent(serviceInfo.descriptorType()) .addContent(".INSTANCE"); } } @@ -259,18 +276,148 @@ private static boolean isQualifiedInjectionTarget(ServiceInfo sp) { return hasContract || hasDependencies; } - private TypeInfo createAppTypeInfo(TypeName typeName) { - return TypeInfo.builder() - .kind(ElementKind.CLASS) - .typeName(typeName) - // to trigger generation of descriptor - .addAnnotation(Annotation.create(SERVICE_ANNOTATION_PROVIDER)) - .addInterfaceTypeInfo(TypeInfo.builder() - .kind(ElementKind.INTERFACE) - .typeName(SERVICE_BINDING) - .addAnnotation(Annotation.create(SERVICE_ANNOTATION_CONTRACT)) - .build()) - .build(); + private void createConfigureMethodBody(WrappedServices services, + ClassModel.Builder classModel, + Method.Builder method) { + /* + This method will: + - configure all run levels + - configure all services to avoid service discovery + */ + + configureRunLevels(services, classModel, method); + registerServiceDescriptors(services, classModel, method); + } + + private void configureRunLevels(WrappedServices services, ClassModel.Builder classModel, Method.Builder method) { + + /* + private static final List RUN_LEVELS = List.of(12D, 100D); + builder.runLevels(RUN_LEVELS); + */ + runLevelsConstantField(classModel, services); + method.addContentLine("builder.runLevels(RUN_LEVELS);"); + + } + + static void runLevelsConstantField(ClassModel.Builder classModel, WrappedServices services) { + Set runLevels = new TreeSet<>(); + + for (ServiceInfo serviceInfo : services.all()) { + if (serviceInfo.runLevel().isPresent()) { + runLevels.add(serviceInfo.runLevel().get()); + } + } + + List runLevelList = List.copyOf(runLevels); + classModel.addField(runLevelsField -> runLevelsField + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(LIST_OF_DOUBLES) + .name("RUN_LEVELS") + .addContent(List.class) + .addContent(".of(") + .update(fieldBuilder -> { + for (int i = 0; i < runLevelList.size(); i++) { + double current = runLevelList.get(i); + if (Double.compare(RunLevel.STARTUP, current) == 0) { + fieldBuilder.addContent(SERVICE_ANNOTATION_RUN_LEVEL) + .addContent(".STARTUP"); + } else if (Double.compare(RunLevel.SERVER, current) == 0) { + fieldBuilder.addContent(SERVICE_ANNOTATION_RUN_LEVEL) + .addContent(".SERVER"); + } else if (Double.compare(RunLevel.NORMAL, current) == 0) { + fieldBuilder.addContent(SERVICE_ANNOTATION_RUN_LEVEL) + .addContent(".NORMAL"); + } else { + fieldBuilder.addContent(String.valueOf(current)) + .addContent("D"); + } + if (i == runLevelList.size() - 1) { + fieldBuilder.addContentLine(""); + } else { + fieldBuilder.addContentLine(","); + } + } + }) + .addContent(")") + ); + } + + private void registerServiceDescriptors(WrappedServices services, ClassModel.Builder classModel, Method.Builder method) { + List serviceLoaded = new ArrayList<>(); + List serviceDescriptors = new ArrayList<>(); + + // for each discovered service, add it to the configuration + for (ServiceInfo serviceInfo : services.all()) { + if (serviceInfo instanceof ServiceLoader__ServiceDescriptor sl) { + serviceLoaded.add(sl); + } else { + serviceDescriptors.add(serviceInfo.descriptorType()); + } + } + + Map providerConstants = new HashMap<>(); + AtomicInteger constantCounter = new AtomicInteger(); + + serviceLoaded.stream() + .sorted(serviceLoaderComparator()) + .forEach(it -> addServiceLoader(classModel, method, providerConstants, constantCounter, it)); + + if (!serviceLoaded.isEmpty() && !serviceDescriptors.isEmpty()) { + // visually separate service loaded services from service descriptors + method.addContentLine(""); + } + + // viceDescriptor(ImperativeFeature__ServiceDescriptor.INSTANCE); + serviceDescriptors.stream() + .sorted() + .forEach(it -> method + .addContent("builder.addServiceDescriptor(") + .addContent(it) + .addContentLine(".INSTANCE);")); + } + + private void addServiceLoader(ClassModel.Builder classModel, + Method.Builder main, + Map providerConstants, + AtomicInteger constantCounter, + ServiceLoader__ServiceDescriptor sl) { + // Generated code: + // builder.addServiceDescriptor(serviceLoader(PROVIDER_1, + // YamlConfigParser.class, + // () -> new io.helidon.config.yaml.YamlConfigParser(), + // 90.0)); + TypeName providerInterface = sl.providerInterface(); + String constantName = providerConstants.computeIfAbsent(providerInterface, it -> { + int i = constantCounter.getAndIncrement(); + String constant = "PROVIDER_" + i; + classModel.addField(field -> field + .accessModifier(AccessModifier.PRIVATE) + .isStatic(true) + .isFinal(true) + .type(TypeNames.TYPE_NAME) + .name(constant) + .addContentCreate(providerInterface)); + return constant; + }); + + main.addContent("builder.addServiceDescriptor(") + .addContent(SERVICE_LOADER_DESCRIPTOR) + .addContent(".create(") + .addContent(constantName) + .addContentLine(",") + .increaseContentPadding() + .increaseContentPadding() + .increaseContentPadding() + .addContent(sl.serviceType()).addContentLine(".class,") + .addContent("() -> new ").addContent(sl.serviceType()).addContentLine("(),") + .addContent(String.valueOf(sl.weight())) + .addContentLine("));") + .decreaseContentPadding() + .decreaseContentPadding() + .decreaseContentPadding(); } private void handleError(CodegenException ce) { @@ -344,9 +491,9 @@ private List injectionPointProvidersFor(WrappedServices services, D return services.all(lookup); } - private void createConfigureMethodBody(WrappedServices services, - Set serviceTypes, - Method.Builder method) { + private void createBindingMethodBody(WrappedServices services, + Set serviceTypes, + Method.Builder method) { // find all interceptors and bind them List interceptors = services.all(Lookup.builder() @@ -378,8 +525,7 @@ private void createConfigureMethodBody(WrappedServices services, method.addContentLine("") .decreaseContentPadding(); } - method.addContentLine(");") - .addContentLine(""); + method.addContentLine(");"); // first collect required dependencies by descriptor Map> injectionPlan = new LinkedHashMap<>(); @@ -390,12 +536,12 @@ private void createConfigureMethodBody(WrappedServices services, } } - boolean supportNulls = false; // we group all bindings by descriptor they belong to injectionPlan.forEach((descriptorType, bindings) -> { - method.addContent("binder.bindTo(") + method.addContentLine("") + .addContent("binder.service(") .addContent(descriptorType.genericTypeName()) - .addContentLine(".INSTANCE)") + .addContent(".INSTANCE)") .increaseContentPadding(); for (Binding binding : bindings) { @@ -404,173 +550,56 @@ private void createConfigureMethodBody(WrappedServices services, .addContent(".") .addContent(binding.dependency.descriptorConstant()); - buildTimeBinding(method, binding, ipId, supportNulls); + buildTimeBinding(method, binding, ipId); } /* Commit the dependencies */ - method.addContentLine(".commit();") - .decreaseContentPadding() - .addContentLine(""); + method.addContentLine(";") + .decreaseContentPadding(); }); } /* - Very similar code is used for runtime discovery in ServiceProvider.planForIp + Very similar code is used for runtime discovery in io.helidon.service.registry.Bindings.DependencyBinding.discoverBinding make sure this is doing the same thing! Here we code generate the calls to the binding class */ private void buildTimeBinding(Method.Builder method, Binding binding, - Consumer> ipId, - boolean supportNulls) { + Consumer> ipId) { - Dependency injectionPoint = binding.dependency(); List discovered = binding.descriptors(); Iterator>> descriptors = discovered.stream() .map(BindingGenerator::toContentBuilder) .iterator(); - TypeName ipType = injectionPoint.typeName(); - - // now there are a few options - optional, list, and single instance - if (ipType.isList()) { - TypeName typeOfList = ipType.typeArguments().getFirst(); - if (typeOfList.isSupplier()) { - // inject List> - method.addContent(".bindListOfSuppliers("); - } else if (typeOfList.equals(SERVICE_SERVICE_INSTANCE)) { - method.addContent(".bindServiceInstanceList("); - } else { - // inject List - method.addContent(".bindList("); - } - method.update(ipId::accept); + method.addContentLine("") + .addContent(".bind(") + .update(ipId::accept); - if (discovered.isEmpty()) { - method.addContentLine(")"); - } else { - method.addContent(", ") - .update(it -> { - while (descriptors.hasNext()) { - descriptors.next().accept(it); - if (descriptors.hasNext()) { - it.addContent(", "); - } - } - }) - .addContentLine(")"); - } - } else if (ipType.isOptional()) { - TypeName typeOfOptional = ipType.typeArguments().getFirst(); - if (typeOfOptional.isSupplier()) { - // inject Optional> - method.addContent(".bindOptionalOfSupplier("); - } else if (typeOfOptional.equals(SERVICE_SERVICE_INSTANCE)) { - // inject Optional> - method.addContent(".bindOptionalOfServiceInstance("); - } else { - // inject Optional - method.addContent(".bindOptional("); - } - method.update(ipId::accept); - - if (discovered.isEmpty()) { - method.addContentLine(")"); - } else { - method.addContent(", "); - descriptors.next().accept(method); - method.addContentLine(")"); - } - } else if (ipType.isSupplier()) { - // one of the supplier options - - TypeName typeOfSupplier = ipType.typeArguments().getFirst(); - if (typeOfSupplier.isOptional()) { - // inject Supplier> - method.addContent(".bindSupplierOfOptional(") - .update(ipId::accept); - if (discovered.isEmpty()) { - method.addContentLine(")"); - } else { - method.addContent(", "); - descriptors.next().accept(method); - method.addContentLine(")"); - } - } else if (typeOfSupplier.isList()) { - // inject Supplier> - method.addContent(".bindSupplierOfList(") - .update(ipId::accept); - if (discovered.isEmpty()) { - method.addContentLine(")"); - } else { - method.addContent(", ") - .update(it -> { - while (descriptors.hasNext()) { - descriptors.next().accept(it); - if (descriptors.hasNext()) { - it.addContent(", "); - } - } - }) - .addContentLine(")"); - } - } else { - // inject Supplier - method.addContent(".bindSupplier(") - .update(ipId::accept); - - if (discovered.isEmpty()) { - // null binding is not supported at runtime - throw new CodegenException("Injection point requires a value, but no provider discovered: " - + injectionPoint); - } - method.addContent(", "); - descriptors.next().accept(method); - method.addContentLine(")"); - } - } else if (ipType.equals(SERVICE_SERVICE_INSTANCE)) { - // inject Contract - if (discovered.isEmpty()) { - if (supportNulls) { - method.addContent(".bindNull(") - .update(ipId::accept) - .addContentLine(")"); - } else { - // null binding is not supported at runtime - throw new CodegenException("Injection point requires a value, but no provider discovered: " - + injectionPoint); - } - } else { - method.addContent(".bindServiceInstance(") - .update(ipId::accept) - .addContent(", ") - .update(descriptors.next()::accept) - .addContentLine(")"); - } + if (discovered.isEmpty()) { + method.addContent(")"); } else { - // inject Contract - if (discovered.isEmpty()) { - if (supportNulls) { - method.addContent(".bindNull(") - .update(ipId::accept) - .addContentLine(")"); - } else { - // null binding is not supported at runtime - throw new CodegenException("Injection point requires a value, but no provider discovered: " - + injectionPoint); - } - } else { - method.addContent(".bind(") - .update(ipId::accept) - .addContent(", ") - .update(descriptors.next()::accept) - .addContentLine(")"); - } + method.addContent(", ") + .update(it -> { + while (descriptors.hasNext()) { + descriptors.next().accept(it); + if (descriptors.hasNext()) { + it.addContent(", "); + } + } + }) + .addContent(")"); } } + private Comparator serviceLoaderComparator() { + return Comparator.comparing(ServiceLoader__ServiceDescriptor::providerInterface) + .thenComparing(ServiceDescriptor::serviceType); + } + record InjectionPlan(List unqualifiedProviders, List qualifiedProviders) { } @@ -580,8 +609,8 @@ record BindingPlan(TypeName descriptorType, } /** - * @param dependency to bind to - * @param descriptors matching descriptors + * @param dependency to bind to + * @param descriptors matching descriptors */ record Binding(Dependency dependency, List descriptors) { diff --git a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateApplicationAbstractMojo.java b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateApplicationAbstractMojo.java index 0b1f348c1de..778325c5dbd 100644 --- a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateApplicationAbstractMojo.java +++ b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateApplicationAbstractMojo.java @@ -42,6 +42,7 @@ import io.helidon.codegen.CodegenScope; import io.helidon.codegen.ModuleInfo; import io.helidon.codegen.ModuleInfoSourceParser; +import io.helidon.codegen.compiler.Compiler; import io.helidon.codegen.compiler.CompilerOptions; import io.helidon.common.types.TypeName; import io.helidon.service.registry.ServiceInfo; @@ -53,14 +54,18 @@ import org.apache.maven.project.MavenProject; /** - * Abstract base for the Injection {@code maven-plugin} responsible for creating + * Abstract base for the Service {@code maven-plugin} responsible for creating * {@code Binding}, Test {@code Binding}, and application Main class. */ abstract class CreateApplicationAbstractMojo extends CodegenAbstractMojo { /** * Class name of the binding class generated by Maven plugin (for end user application). */ - protected static final String BINDING_CLASS_NAME = "Injection__Binding"; + protected static final String BINDING_CLASS_NAME = "ApplicationBinding"; + /** + * Class name of the main class generated by Maven plugin (for end user application). + */ + protected static final String MAIN_CLASS_NAME = "ApplicationMain"; /** * The -source argument for the Java compiler. * Note: using the same as maven-compiler for convenience and least astonishment. @@ -75,12 +80,27 @@ abstract class CreateApplicationAbstractMojo extends CodegenAbstractMojo { defaultValue = "true") private boolean validate; /** - * Whether to generate binding class (provides generated injection plan for all services). + * Whether to generate binding class (provides generated injection plan for all services and registration of all + * service descriptors). */ @Parameter(property = "helidon.service.registry.binding.generate", defaultValue = "true") private boolean generateBinding; + /** + * Whether to generate a main class that takes care of bootstrapping the service registry and initializing + * all services that have a RunLevel annotation. + */ + @Parameter(property = "helidon.service.registry.main.generate", defaultValue = "false") + private boolean generateMain; + + /** + * Name of the generated binding class. + */ + @Parameter(property = "helidon.service.registry.main.class.name", + defaultValue = MAIN_CLASS_NAME) + private String mainClassName; + /** * Default constructor. */ @@ -195,19 +215,50 @@ void applicationBinding(MavenCodegenContext scanContext, validator.validate(services); } + TypeName bindingTypeName = TypeName.create(packageName + "." + className); + List sourcesToCompile = new ArrayList<>(); + if (generateBinding) { // get the binding generator only after services are initialized (we need to ignore any existing apps) - BindingGenerator creator = new BindingGenerator(scanContext, failOnError()); - TypeName bindingTypeName = TypeName.create(packageName + "." + className); + BindingGenerator generator = new BindingGenerator(scanContext, failOnError()); getLog().info("Generating application binding: " + bindingTypeName.fqName()); - creator.createBinding(services, - allServices, - bindingTypeName, - moduleName, - compilerOptions); + generator.createBinding(services, + allServices, + bindingTypeName, + moduleName, + sourcesToCompile); } + + if (generateMain()) { + TypeName mainTypeName = TypeName.builder() + .packageName(packageName) + .className(mainClassName) + .build(); + + MainGenerator generator = new MainGenerator(scanContext); + generator.createMain(services, + generateBinding, + bindingTypeName, + mainTypeName, + sourcesToCompile); + } + + if (!sourcesToCompile.isEmpty()) { + Compiler.compile(compilerOptions, sourcesToCompile.toArray(new Path[0])); + } + + } + + /** + * Whether to generate a main class. + * Main class is only generated for main (production sources) and never for test. + * + * @return whether to generate main class + */ + boolean generateMain() { + return generateMain; } /** diff --git a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateTestApplicationMojo.java b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateTestApplicationMojo.java index 23c067462b3..3dc21081c27 100644 --- a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateTestApplicationMojo.java +++ b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/CreateTestApplicationMojo.java @@ -65,6 +65,11 @@ public class CreateTestApplicationMojo extends CreateApplicationAbstractMojo { public CreateTestApplicationMojo() { } + @Override + boolean generateMain() { + return false; + } + @Override protected Path generatedSourceDirectory() { return generatedTestSourcesDirectory.toPath(); diff --git a/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/MainGenerator.java b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/MainGenerator.java new file mode 100644 index 00000000000..47b18621cc5 --- /dev/null +++ b/service/maven-plugin/src/main/java/io/helidon/service/maven/plugin/MainGenerator.java @@ -0,0 +1,97 @@ +package io.helidon.service.maven.plugin; + +import java.nio.file.Path; +import java.util.List; + +import io.helidon.codegen.CodegenUtil; +import io.helidon.codegen.classmodel.ClassModel; +import io.helidon.codegen.classmodel.Method; +import io.helidon.common.types.AccessModifier; +import io.helidon.common.types.TypeName; +import io.helidon.common.types.TypeNames; + +import static io.helidon.service.codegen.ServiceCodegenTypes.REGISTRY_STARTUP_PROVIDER; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_CONFIG; +import static io.helidon.service.codegen.ServiceCodegenTypes.SERVICE_REGISTRY_MANAGER; + +/** + * Generates the application main class. + */ +class MainGenerator { + private static final TypeName GENERATOR = TypeName.create(MainGenerator.class); + + private final MavenCodegenContext ctx; + + MainGenerator(MavenCodegenContext ctx) { + this.ctx = ctx; + } + + void createMain(WrappedServices services, + boolean generateBinding, + TypeName bindingTypeName, + TypeName generatedType, + List sourcesToCompile) { + + ClassModel.Builder classModel = ClassModel.builder() + .type(generatedType) + .accessModifier(AccessModifier.PUBLIC) + .copyright(CodegenUtil.copyright(GENERATOR, + GENERATOR, + generatedType)) + .addAnnotation(CodegenUtil.generatedAnnotation(GENERATOR, + generatedType, + generatedType, + "1", + "")) + .addDescriptionLine("Main class generated for Helidon Service Registry Application.") + .isFinal(true); + + classModel.addConstructor(ctr -> ctr + .accessModifier(AccessModifier.PRIVATE)); + + classModel.addMethod(main -> main + .accessModifier(AccessModifier.PUBLIC) + .isStatic(true) + .returnType(TypeNames.PRIMITIVE_VOID) + .description("Start the application.") + .name("main") + .addParameter(args -> args + .type(String[].class) + .description("Command line arguments.") + .name("args")) + .update(it -> mainMethodBody(services, generateBinding, bindingTypeName, classModel, it))); + + Path generated = ctx.filer() + .writeSourceFile(classModel.build()); + sourcesToCompile.add(generated); + } + + private void mainMethodBody(WrappedServices services, + boolean generateBinding, + TypeName bindingTypeName, + ClassModel.Builder classModel, + Method.Builder method) { + + if (!generateBinding) { + BindingGenerator.runLevelsConstantField(classModel, services); + method.addContent("var config = ") + .addContent(SERVICE_CONFIG) + .addContentLine(".builder().runLevels(RUN_LEVELS).build();"); + } + + method.addContent("var manager = ") + .addContent(SERVICE_REGISTRY_MANAGER) + .addContent(".start("); + + if (generateBinding) { + method.addContent(bindingTypeName) + .addContentLine(".create());"); + } else { + method.addContentLine("config);"); + } + + method.addContent(REGISTRY_STARTUP_PROVIDER) + .addContentLine(".registerShutdownHandler(manager);"); + } + +} diff --git a/service/registry/pom.xml b/service/registry/pom.xml index 0578105289b..2a820c01eda 100644 --- a/service/registry/pom.xml +++ b/service/registry/pom.xml @@ -39,6 +39,10 @@ + + io.helidon + helidon + io.helidon.common helidon-common diff --git a/service/registry/src/main/java/io/helidon/service/registry/Activators.java b/service/registry/src/main/java/io/helidon/service/registry/Activators.java index 5d388142a56..f78f2b0b373 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/Activators.java +++ b/service/registry/src/main/java/io/helidon/service/registry/Activators.java @@ -74,21 +74,29 @@ static Activator create(ServiceProvider provider, T instance) { static Supplier> create(CoreServiceRegistry registry, ServiceProvider provider) { ServiceDescriptor descriptor = provider.descriptor(); + // as this is going to create an instance of the service, we need the dependency context + var bindingPlan = registry.bindings().bindingPlan(descriptor); + DependencyContext dependencyContext = new BindingDependencyContext(bindingPlan); if (descriptor.scope().equals(Service.PerLookup.TYPE)) { return switch (descriptor.factoryType()) { case NONE -> new MissingDescribedActivator<>(provider); case SERVICE -> { if (descriptor instanceof PerInstanceDescriptor dbd) { - yield () -> new ActivatorsPerLookup.PerInstanceActivator<>(registry, provider, dbd); + yield () -> new ActivatorsPerLookup.PerInstanceActivator<>(registry, + dependencyContext, + bindingPlan, + provider, + dbd); } - yield () -> new ActivatorsPerLookup.SingleServiceActivator<>(provider); + yield () -> new ActivatorsPerLookup.SingleServiceActivator<>(provider, dependencyContext); } - case SUPPLIER -> () -> new ActivatorsPerLookup.SupplierActivator<>(provider); - case SERVICES -> () -> new ActivatorsPerLookup.ServicesFactoryActivator<>(provider); - case INJECTION_POINT -> () -> new ActivatorsPerLookup.IpFactoryActivator<>(provider); + case SUPPLIER -> () -> new ActivatorsPerLookup.SupplierActivator<>(provider, dependencyContext); + case SERVICES -> () -> new ActivatorsPerLookup.ServicesFactoryActivator<>(provider, dependencyContext); + case INJECTION_POINT -> () -> new ActivatorsPerLookup.IpFactoryActivator<>(provider, dependencyContext); case QUALIFIED -> () -> new ActivatorsPerLookup.QualifiedFactoryActivator<>(provider, + dependencyContext, (QualifiedFactoryDescriptor) descriptor); }; } else { @@ -96,28 +104,62 @@ static Supplier> create(CoreServiceRegistry registry, ServicePr case NONE -> new MissingDescribedActivator<>(provider); case SERVICE -> { if (descriptor instanceof PerInstanceDescriptor dbd) { - yield () -> new PerInstanceActivator<>(registry, provider, dbd); + yield () -> new PerInstanceActivator<>(registry, provider, dependencyContext, bindingPlan, dbd); } - yield () -> new Activators.SingleServiceActivator<>(provider); + yield () -> new Activators.SingleServiceActivator<>(provider, dependencyContext); } - case SUPPLIER -> () -> new Activators.SupplierActivator<>(provider); - case SERVICES -> () -> new ServicesFactoryActivator<>(provider); - case INJECTION_POINT -> () -> new IpFactoryActivator<>(provider); + case SUPPLIER -> () -> new Activators.SupplierActivator<>(provider, dependencyContext); + case SERVICES -> () -> new ServicesFactoryActivator<>(provider, dependencyContext); + case INJECTION_POINT -> () -> new IpFactoryActivator<>(provider, dependencyContext); case QUALIFIED -> () -> new QualifiedFactoryActivator<>(provider, + dependencyContext, (QualifiedFactoryDescriptor) descriptor); }; } } + interface InstanceHolder { + static InstanceHolder create(ServiceProvider serviceProvider, DependencyContext dependencyContext) { + // the same instance is returned for the lifetime of the service provider + return new InstanceHolderImpl<>(dependencyContext, + serviceProvider.interceptionMetadata(), + serviceProvider.descriptor()); + } + + // we use the instance holder to hold either the actual instance, + // or the factory; this is a place for improvement + @SuppressWarnings("unchecked") + static InstanceHolder create(Object instance) { + return new FixedInstanceHolder<>((T) instance); + } + + T get(); + + default void construct() { + } + + default void inject() { + } + + default void postConstruct() { + } + + default void preDestroy() { + } + } + abstract static class BaseActivator implements Activator { final ServiceProvider provider; + final DependencyContext dependencyContext; + private final ReadWriteLock instanceLock = new ReentrantReadWriteLock(); volatile ActivationPhase currentPhase = ActivationPhase.INIT; - BaseActivator(ServiceProvider provider) { + BaseActivator(ServiceProvider provider, DependencyContext dependencyContext) { this.provider = provider; + this.dependencyContext = dependencyContext; } // three states @@ -365,9 +407,9 @@ static class FixedActivator extends BaseActivator { private final Optional>> instances; FixedActivator(ServiceProvider provider, T instance) { - super(provider); + super(provider, null); - List> values = List.of(QualifiedInstance.create(instance, provider.serviceInfo().qualifiers())); + List> values = List.of(QualifiedInstance.create(instance, provider.descriptor().qualifiers())); this.instances = Optional.of(values); } @@ -381,7 +423,7 @@ static class FixedSupplierActivator extends BaseActivator { private final Supplier>>> instances; FixedSupplierActivator(ServiceProvider provider, Supplier instanceSupplier) { - super(provider); + super(provider, null); instances = LazyValue.create(() -> { List> values = List.of(QualifiedInstance.create(instanceSupplier.get(), @@ -400,8 +442,8 @@ protected Optional>> targetInstances() { static class FixedIpFactoryActivator extends IpFactoryActivator { FixedIpFactoryActivator(ServiceProvider provider, - InjectionPointFactory instance) { - super(provider); + InjectionPointFactory instance) { + super(provider, null); serviceInstance = InstanceHolder.create(instance); } } @@ -409,7 +451,7 @@ static class FixedIpFactoryActivator extends IpFactoryActivator { static class FixedServicesFactoryActivator extends ServicesFactoryActivator { FixedServicesFactoryActivator(ServiceProvider provider, Service.ServicesFactory factory) { - super(provider); + super(provider, null); serviceInstance = InstanceHolder.create(factory); } } @@ -417,7 +459,7 @@ static class FixedServicesFactoryActivator extends ServicesFactoryActivator extends QualifiedFactoryActivator { FixedQualifiedFactoryActivator(ServiceProvider provider, QualifiedFactory factory) { - super(provider, (QualifiedFactoryDescriptor) provider.descriptor()); + super(provider, null, (QualifiedFactoryDescriptor) provider.descriptor()); serviceInstance = InstanceHolder.create(factory); } } @@ -431,8 +473,8 @@ static class SingleServiceActivator extends BaseActivator { protected InstanceHolder serviceInstance; protected List> targetInstances; - SingleServiceActivator(ServiceProvider provider) { - super(provider); + SingleServiceActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -451,7 +493,7 @@ protected void construct(ActivationResult.Builder response) { lock.lock(); if (serviceInstance == null) { // it may have been set explicitly when creating registry - this.serviceInstance = InstanceHolder.create(provider, provider.injectionPlan()); + this.serviceInstance = InstanceHolder.create(provider, dependencyContext); } this.serviceInstance.construct(); } finally { @@ -495,8 +537,8 @@ protected void preDestroy(ActivationResult.Builder response) { * {@code MyService implements Supplier}. */ static class SupplierActivator extends SingleServiceActivator { - SupplierActivator(ServiceProvider provider) { - super(provider); + SupplierActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -531,8 +573,10 @@ static class QualifiedFactoryActivator extends SingleServiceActivator { private final Set supportedContracts; private final boolean anyMatch; - QualifiedFactoryActivator(ServiceProvider provider, QualifiedFactoryDescriptor qpd) { - super(provider); + QualifiedFactoryActivator(ServiceProvider provider, + DependencyContext dependencyContext, + QualifiedFactoryDescriptor qpd) { + super(provider, dependencyContext); this.supportedQualifier = qpd.qualifierType(); this.supportedContracts = provider.descriptor() .contracts() @@ -588,8 +632,8 @@ private List> targetInstances(Lookup lookup, Qualifier qual * {@code MyService implements InjectionPointProvider}. */ static class IpFactoryActivator extends SingleServiceActivator { - IpFactoryActivator(ServiceProvider provider) { - super(provider); + IpFactoryActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -624,8 +668,8 @@ protected Optional>> targetInstances(Lookup lookup) { * {@code MyService implements ServicesProvider}. */ static class ServicesFactoryActivator extends SingleServiceActivator { - ServicesFactoryActivator(ServiceProvider provider) { - super(provider); + ServicesFactoryActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -672,39 +716,50 @@ static class PerInstanceActivator extends BaseActivator { private final CoreServiceRegistry registry; private final ResolvedType createFor; private final Lookup createForLookup; + private final Bindings.ServiceBindingPlan bindingPlan; + private List> serviceInstances; private List> targetInstances; - PerInstanceActivator(CoreServiceRegistry registry, ServiceProvider provider, PerInstanceDescriptor dbd) { - super(provider); + PerInstanceActivator(CoreServiceRegistry registry, + ServiceProvider provider, + DependencyContext dependencyContext, + Bindings.ServiceBindingPlan bindingPlan, + PerInstanceDescriptor dbd) { + super(provider, dependencyContext); this.registry = registry; + this.bindingPlan = bindingPlan; this.createFor = ResolvedType.create(dbd.createFor()); this.createForLookup = Lookup.builder() .addContract(createFor) .build(); } - static Map> updatePlan(Map> injectionPlan, - ServiceInstance driver, - Qualifier name) { - - Set ips = Set.copyOf(injectionPlan.keySet()); + static DependencyContext updatePlan(Bindings.ServiceBindingPlan injectionPlan, + DependencyContext dependencyContext, + ServiceInstance driver, + Qualifier name) { Set contracts = driver.contracts(); - Map> updatedPlan = new HashMap<>(injectionPlan); + Map> targetPlan = new HashMap<>(); - for (Dependency dependency : ips) { + for (Bindings.DependencyBinding binding : injectionPlan.allBindings()) { + Dependency dependency = binding.dependency(); + + boolean updated = false; // injection point for the driving instance if (contracts.contains(ResolvedType.create(dependency.contract())) && dependency.qualifiers().isEmpty()) { - if (ServiceInstance.TYPE.equals(dependency.typeName())) { - // if the injection point has the same contract, no qualifiers, then it is the driving instance - updatedPlan.put(dependency, new IpPlan<>(() -> driver, injectionPlan.get(dependency).descriptors())); + updated = true; + // if the injection point has the same contract, no qualifiers, then it is the driving instance + if (dependency.isServiceInstance()) { + // return ServiceInstance + targetPlan.put(dependency, () -> driver); } else { - // if the injection point has the same contract, no qualifiers, then it is the driving instance - updatedPlan.put(dependency, new IpPlan<>(driver, injectionPlan.get(dependency).descriptors())); + // return instance + targetPlan.put(dependency, driver::get); } } // injection point for the service name @@ -713,14 +768,18 @@ static Map> updatePlan(Map> injectio if (dependency.qualifiers() .stream() .anyMatch(it -> Service.InstanceName.TYPE.equals(it.typeName()))) { - updatedPlan.put(dependency, - new IpPlan<>(() -> name.value().orElse(Service.Named.DEFAULT_NAME), - injectionPlan.get(dependency).descriptors())); + updated = true; + targetPlan.put(dependency, () -> name.value().orElse(Service.Named.DEFAULT_NAME)); } } + + if (!updated) { + // fallback to original instance + targetPlan.put(dependency, () -> dependencyContext.dependency(dependency)); + } } - return updatedPlan; + return new SupplierDependencyContext(targetPlan); } @Override @@ -728,7 +787,7 @@ protected void construct(ActivationResult.Builder response) { // at this moment, we must resolve services that are driving this instance // we do not want to use lookup, as that is doing too much for us - List> drivingInstances = driversFromPlan(provider.injectionPlan(), createFor) + List> drivingInstances = driversFromPlan(bindingPlan, createFor) .stream() .map(registry::serviceManager) .flatMap(it -> it.activator() @@ -739,7 +798,7 @@ protected void construct(ActivationResult.Builder response) { .toList(); serviceInstances = drivingInstances.stream() - .map(it -> QualifiedServiceInstance.create(provider, it)) + .map(it -> QualifiedServiceInstance.create(provider, bindingPlan, dependencyContext, it)) .toList(); for (QualifiedServiceInstance serviceInstance : serviceInstances) { serviceInstance.serviceInstance().construct(); @@ -795,23 +854,27 @@ protected Optional>> targetInstances(Lookup lookup) { return Optional.of(List.copyOf(response)); } - private List driversFromPlan(Map> ipSupplierMap, ResolvedType createFor) { + private List driversFromPlan(Bindings.ServiceBindingPlan ipSupplierMap, ResolvedType createFor) { // I need the list of descriptors from the injection plan - for (Map.Entry> entry : ipSupplierMap.entrySet()) { - Dependency dependency = entry.getKey(); + for (Bindings.DependencyBinding binding : ipSupplierMap.allBindings()) { + Dependency dependency = binding.dependency(); if (createFor.equals(ResolvedType.create(dependency.contract())) && dependency.qualifiers().size() == 1 && dependency.qualifiers().contains(Qualifier.WILDCARD_NAMED)) { - return List.of(entry.getValue().descriptors()); + return binding.descriptors(); } } + // there is not return registry.servicesByContract(createFor); } private record QualifiedServiceInstance(InstanceHolder serviceInstance, Set qualifiers) { - static QualifiedServiceInstance create(ServiceProvider provider, ServiceInstance driver) { + static QualifiedServiceInstance create(ServiceProvider provider, + Bindings.ServiceBindingPlan bindingPlan, + DependencyContext dependencyContext, + ServiceInstance driver) { Set qualifiers = driver.qualifiers(); Qualifier name = qualifiers.stream() .filter(not(Qualifier.WILDCARD_NAMED::equals)) @@ -824,43 +887,13 @@ static QualifiedServiceInstance create(ServiceProvider provider, Servi .collect(Collectors.toSet()); newQualifiers.add(name); - Map> injectionPlan = updatePlan(provider.injectionPlan(), driver, name); + DependencyContext targetDependencyContext = updatePlan(bindingPlan, dependencyContext, driver, name); - return new QualifiedServiceInstance<>(InstanceHolder.create(provider, injectionPlan), newQualifiers); + return new QualifiedServiceInstance<>(InstanceHolder.create(provider, targetDependencyContext), newQualifiers); } } } - interface InstanceHolder { - static InstanceHolder create(ServiceProvider serviceProvider, Map> injectionPlan) { - // the same instance is returned for the lifetime of the service provider - return new InstanceHolderImpl<>(InjectionContext.create(injectionPlan), - serviceProvider.interceptionMetadata(), - serviceProvider.descriptor()); - } - - // we use the instance holder to hold either the actual instance, - // or the factory; this is a place for improvement - @SuppressWarnings("unchecked") - static InstanceHolder create(Object instance) { - return new FixedInstanceHolder<>((T) instance); - } - - T get(); - - default void construct() { - } - - default void inject() { - } - - default void postConstruct() { - } - - default void preDestroy() { - } - } - private static class FixedInstanceHolder implements InstanceHolder { private final T instance; @@ -882,15 +915,13 @@ private static class InstanceHolderImpl implements InstanceHolder { private volatile T instance; private InstanceHolderImpl(DependencyContext ctx, - InterceptionMetadata interceptionMetadata, - ServiceDescriptor source) { + InterceptionMetadata interceptionMetadata, + ServiceDescriptor source) { this.ctx = ctx; this.interceptionMetadata = interceptionMetadata; this.source = source; } - - @Override public T get() { return instance; @@ -925,7 +956,7 @@ private static class MissingDescribedActivator implements Supplier provider) { - this.serviceType = provider.serviceInfo().serviceType().fqName(); + this.serviceType = provider.descriptor().serviceType().fqName(); if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { LOGGER.log(System.Logger.Level.DEBUG, diff --git a/service/registry/src/main/java/io/helidon/service/registry/ActivatorsPerLookup.java b/service/registry/src/main/java/io/helidon/service/registry/ActivatorsPerLookup.java index 299962a0fde..dd314ee2fee 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ActivatorsPerLookup.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ActivatorsPerLookup.java @@ -19,7 +19,6 @@ import java.util.ArrayList; import java.util.LinkedHashSet; import java.util.List; -import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.concurrent.locks.ReentrantLock; @@ -60,8 +59,8 @@ private ActivatorsPerLookup() { static class SingleServiceActivator extends Activators.BaseActivator { protected OnDemandInstance serviceInstance; - SingleServiceActivator(ServiceProvider provider) { - super(provider); + SingleServiceActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -75,7 +74,7 @@ protected Optional>> targetInstances() { @Override protected void construct(ActivationResult.Builder response) { - this.serviceInstance = new OnDemandInstance<>(InjectionContext.create(provider.injectionPlan()), + this.serviceInstance = new OnDemandInstance<>(dependencyContext, provider.interceptionMetadata(), provider.descriptor()); } @@ -90,8 +89,8 @@ protected void preDestroy(ActivationResult.Builder response) { * {@code MyService implements Supplier}. */ static class SupplierActivator extends SingleServiceActivator { - SupplierActivator(ServiceProvider provider) { - super(provider); + SupplierActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @Override @@ -133,8 +132,10 @@ static class QualifiedFactoryActivator extends SingleServiceActivator { private final Set supportedContracts; private final boolean anyMatch; - QualifiedFactoryActivator(ServiceProvider provider, GeneratedService.QualifiedFactoryDescriptor qpd) { - super(provider); + QualifiedFactoryActivator(ServiceProvider provider, + DependencyContext dependencyContext, + GeneratedService.QualifiedFactoryDescriptor qpd) { + super(provider, dependencyContext); this.supportedQualifier = qpd.qualifierType(); this.supportedContracts = provider.descriptor().contracts() .stream() @@ -184,8 +185,8 @@ private List> targetInstances(Lookup lookup, Qualifier qual * {@code MyService implements InjectionPointProvider}. */ static class IpFactoryActivator extends SingleServiceActivator { - IpFactoryActivator(ServiceProvider provider) { - super(provider); + IpFactoryActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @SuppressWarnings("unchecked") @@ -215,8 +216,8 @@ protected Optional>> targetInstances(Lookup lookup) { * {@code MyService implements ServicesProvider}. */ static class ServicesFactoryActivator extends SingleServiceActivator { - ServicesFactoryActivator(ServiceProvider provider) { - super(provider); + ServicesFactoryActivator(ServiceProvider provider, DependencyContext dependencyContext) { + super(provider, dependencyContext); } @SuppressWarnings("unchecked") @@ -244,16 +245,20 @@ protected Optional>> targetInstances(Lookup lookup) { */ static class PerInstanceActivator extends Activators.BaseActivator { private final CoreServiceRegistry registry; + private final Bindings.ServiceBindingPlan bindingPlan; private final ResolvedType createFor; private List> serviceInstances; PerInstanceActivator(CoreServiceRegistry registry, + DependencyContext dependencyContext, + Bindings.ServiceBindingPlan bindingPlan, ServiceProvider provider, GeneratedService.PerInstanceDescriptor dbd) { - super(provider); + super(provider, dependencyContext); this.registry = registry; + this.bindingPlan = bindingPlan; this.createFor = ResolvedType.create(dbd.createFor()); } @@ -269,7 +274,7 @@ protected void construct(ActivationResult.Builder response) { .stream() .flatMap(List::stream) .map(qi -> it.registryInstance(Lookup.EMPTY, qi))) - .map(it -> QualifiedOnDemandInstance.create(provider, it)) + .map(it -> QualifiedOnDemandInstance.create(provider, bindingPlan, dependencyContext, it)) .collect(Collectors.toList()); } @@ -291,6 +296,8 @@ protected void preDestroy(ActivationResult.Builder response) { private record QualifiedOnDemandInstance(OnDemandInstance serviceInstance, Set qualifiers) { static QualifiedOnDemandInstance create(ServiceProvider provider, + Bindings.ServiceBindingPlan bindingPlan, + DependencyContext dependencyContext, ServiceInstance driver) { Set qualifiers = driver.qualifiers(); Qualifier name = qualifiers.stream() @@ -304,11 +311,13 @@ static QualifiedOnDemandInstance create(ServiceProvider provider, .collect(Collectors.toSet()); newQualifiers.add(name); - Map> injectionPlan = Activators.PerInstanceActivator.updatePlan(provider.injectionPlan(), - driver, - name); + DependencyContext targetDependencyContext = + Activators.PerInstanceActivator.updatePlan(bindingPlan, + dependencyContext, + driver, + name); - return new QualifiedOnDemandInstance<>(new OnDemandInstance<>(InjectionContext.create(injectionPlan), + return new QualifiedOnDemandInstance<>(new OnDemandInstance<>(targetDependencyContext, provider.interceptionMetadata(), provider.descriptor()), newQualifiers); diff --git a/service/registry/src/main/java/io/helidon/service/registry/Binding.java b/service/registry/src/main/java/io/helidon/service/registry/Binding.java index 445ce4f89ce..4c9795702a2 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/Binding.java +++ b/service/registry/src/main/java/io/helidon/service/registry/Binding.java @@ -27,7 +27,6 @@ *

* Binding instances MUST NOT have injection points. */ -@Service.Contract public interface Binding { /** * Type name of this interface. @@ -42,9 +41,21 @@ public interface Binding { String name(); /** - * Configure injection points and dependencies in this application. + * For each service in this application, bind services that satisfy its injection points. * - * @param binder the binder used to register the service provider injection plans + * @param binder the binder used to register the service provider dependency injection plan */ - void configure(DependencyPlanBinder binder); + void binding(DependencyPlanBinder binder); + + /** + * Register all services with the configuration. + * When application binding is available to the registry, automatic discovery of services is disabled, and only services + * registered in this method will be used by the registry. + *

+ * The services registered in this method must be aligned with {@link #binding(DependencyPlanBinder)}, as otherwise + * inconsistent registry would be created. + * + * @param builder configuration builder to register service descriptors + */ + void configure(ServiceRegistryConfig.Builder builder); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/BindingDependencyContext.java b/service/registry/src/main/java/io/helidon/service/registry/BindingDependencyContext.java new file mode 100644 index 00000000000..6803d41247a --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/BindingDependencyContext.java @@ -0,0 +1,17 @@ +package io.helidon.service.registry; + +class BindingDependencyContext implements DependencyContext { + private final Bindings.ServiceBindingPlan serviceBinding; + + BindingDependencyContext(Bindings.ServiceBindingPlan serviceBinding) { + this.serviceBinding = serviceBinding; + } + + @SuppressWarnings("unchecked") + @Override + public T dependency(Dependency dependency) { + Bindings.DependencyBinding binding = serviceBinding.binding(dependency); + // services that match + return (T) binding.instanceSupply().get(); + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/Bindings.java b/service/registry/src/main/java/io/helidon/service/registry/Bindings.java new file mode 100644 index 00000000000..da01e3e0c0a --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/Bindings.java @@ -0,0 +1,291 @@ +package io.helidon.service.registry; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.locks.ReentrantLock; +import java.util.concurrent.locks.ReentrantReadWriteLock; +import java.util.function.Supplier; +import java.util.stream.Collectors; + +import io.helidon.common.types.ResolvedType; +import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupply; +import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupplyList; +import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupplyOptional; +import io.helidon.service.registry.ServiceSupplies.ServiceSupply; +import io.helidon.service.registry.ServiceSupplies.ServiceSupplyList; +import io.helidon.service.registry.ServiceSupplies.ServiceSupplyOptional; + +/** + * Contains bindings for each service. + *

+ * A binding is a map of injection point to zero or more service descriptors that satisfy it. + */ +class Bindings { + private final Map> bindingsByContract = new HashMap<>(); + private final Map bindingPlans = new IdentityHashMap<>(); + private final ReentrantLock lock = new ReentrantLock(); + private final CoreServiceRegistry registry; + + Bindings(CoreServiceRegistry registry) { + this.registry = registry; + } + + /* + Register each service known to the registry (via service descriptors) + */ + void register(ServiceInfo serviceInfo) { + lock.lock(); + try { + ServiceBindingPlan bindingPlan = new ServiceBindingPlan(registry, serviceInfo); + this.bindingPlans.put(serviceInfo, bindingPlan); + for (var binding : bindingPlan.allBindings()) { + bindingsByContract.computeIfAbsent(ResolvedType.create(binding.dependency.contract()), it -> new ArrayList<>()) + .add(binding); + } + } finally { + lock.unlock(); + } + } + + /* + A contract was late bound through Services.set(...), we must forget all bindings for that contract + */ + void forgetContract(ResolvedType type) { + lock.lock(); + try { + List toRemove = bindingsByContract.remove(type); + if (toRemove == null) { + // nobody injects this contract + return; + } + toRemove.forEach(DependencyBinding::clear); + } finally { + lock.unlock(); + } + } + + /* + Binding plan for a specific service, this allows us to: + - get contract instances to actually inject the service + - bind pre-built (compile time) + */ + ServiceBindingPlan bindingPlan(ServiceInfo service) { + ServiceBindingPlan bindingPlan = bindingPlans.get(service); + if (bindingPlan == null) { + // this means we failed to bind services on registry startup, we should have complete knowledge of all + // available service infos + throw new ServiceRegistryException("An attempt to get binding plan for service that was not discovered: " + + service.serviceType()); + } + return bindingPlan; + } + + static class ServiceBindingPlan { + private final Map bindingPlan = new HashMap<>(); + private final ServiceInfo serviceInfo; + private final CoreServiceRegistry registry; + + ServiceBindingPlan(CoreServiceRegistry registry, ServiceInfo serviceInfo) { + this.serviceInfo = serviceInfo; + this.registry = registry; + createBindings(); + } + + void ensure() { + for (Dependency dependency : serviceInfo.dependencies()) { + bindingPlan.get(dependency) + .instanceSupply(); + } + } + + Collection allBindings() { + return bindingPlan.values(); + } + + DependencyBinding binding(Dependency dependency) { + return bindingPlan.get(dependency); + } + + private void createBindings() { + for (Dependency dependency : serviceInfo.dependencies()) { + bindingPlan.put(dependency, new DependencyBinding(registry, serviceInfo, dependency)); + } + } + } + + static class DependencyBinding { + private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); + + private final CoreServiceRegistry registry; + private final ServiceInfo serviceInfo; + private final Dependency dependency; + private final Lookup lookup; + + private boolean bound; + private List serviceInfos; + private Supplier instanceSupply; + + private DependencyBinding(CoreServiceRegistry registry, ServiceInfo serviceInfo, Dependency dependency) { + this.registry = registry; + this.serviceInfo = serviceInfo; + this.dependency = dependency; + this.lookup = Lookup.builder() + .dependency(dependency) + .update(it -> { + if (serviceInfo.contracts().contains(ResolvedType.create(dependency.contract())) + && serviceInfo.qualifiers().containsAll(dependency.qualifiers())) { + // when injecting a contract that we also implement, we must inject a service of lower weight + it.weight(serviceInfo.weight()); + } + }) + .build(); + } + + public List descriptors() { + return serviceInfos; + } + + Dependency dependency() { + return dependency; + } + + /* + Bind from build time generated binding + */ + void bind(List serviceInfos) { + lock.writeLock().lock(); + try { + this.bound = true; + this.serviceInfos = serviceInfos; + createInstanceSupply(); + } finally { + lock.writeLock().unlock(); + } + } + + /* + A supplier of value to be injected into this dependency + */ + Supplier instanceSupply() { + lock.readLock().lock(); + try { + if (bound) { + return instanceSupply; + } + } finally { + lock.readLock().unlock(); + } + + lock.writeLock().lock(); + try { + if (bound) { + return instanceSupply; + } + // we will provide a value in the next block, we are in write lock, so nobody can read now + bound = true; + discoverBinding(); + createInstanceSupply(); + return this.instanceSupply; + } finally { + lock.writeLock().unlock(); + } + } + + /* + Clear the binding if there was a late binding event + */ + void clear() { + lock.writeLock().lock(); + try { + bound = false; + serviceInfos = null; + instanceSupply = null; + } finally { + lock.writeLock().unlock(); + } + } + + private void createInstanceSupply() { + if (dependency.isServiceInstance()) { + createInstanceSupplyServiceInstance(); + } else { + createInstanceSupplyDirectContract(); + } + if (dependency.isSupplier()) { + this.instanceSupply = new DependencySupplier(dependency, this.instanceSupply); + } + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void createInstanceSupplyDirectContract() { + this.instanceSupply = switch (dependency.cardinality()) { + case REQUIRED -> new ServiceSupply<>(lookup, + managers(serviceInfos)); + case OPTIONAL -> new ServiceSupplyOptional<>(lookup, + managers(serviceInfos)); + case LIST -> new ServiceSupplyList<>(lookup, + managers(serviceInfos)); + }; + } + + @SuppressWarnings({"rawtypes", "unchecked"}) + private void createInstanceSupplyServiceInstance() { + this.instanceSupply = switch (dependency.cardinality()) { + case REQUIRED -> new ServiceInstanceSupply<>(lookup, + managers(serviceInfos)); + case OPTIONAL -> new ServiceInstanceSupplyOptional<>(lookup, + managers(serviceInfos)); + case LIST -> new ServiceInstanceSupplyList<>(lookup, + managers(serviceInfos)); + }; + } + + @SuppressWarnings("rawtypes") + private List managers(List serviceInfos) { + return serviceInfos.stream() + .map(registry::serviceManager) + .collect(Collectors.toUnmodifiableList()); + } + + private void discoverBinding() { + // lookup services, exclude ourself (when doing chained injection, we lookup by weight) + List found = registry.lookupServices(lookup) + .stream() + .filter(it -> it != serviceInfo) + .collect(Collectors.toList()); + if (found.isEmpty() && (dependency.cardinality() == DependencyCardinality.REQUIRED)) { + throw new ServiceRegistryException("There is no service in registry that satisfied this dependency: " + + dependency); + } + + // we need all service descriptors, as a service may not yield a service + // (such as optional suppliers, ServicesFactory etc.), so we use the next one + + this.serviceInfos = found; + } + + private static class DependencySupplier implements Supplier { + private final Dependency dependency; + private final Supplier instanceSupply; + + DependencySupplier(Dependency dependency, Supplier instanceSupply) { + this.dependency = dependency; + this.instanceSupply = instanceSupply; + } + + @Override + public Object get() { + return instanceSupply; + } + + @Override + public String toString() { + return "DependencySupplier for " + dependency + ", supply: " + instanceSupply; + } + } + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java index fffd510b4b7..9333a3936a1 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java +++ b/service/registry/src/main/java/io/helidon/service/registry/CoreServiceRegistry.java @@ -82,11 +82,16 @@ class CoreServiceRegistry implements ServiceRegistry, Scopes { private final Lock scopeHandlerInstancesLock = new ReentrantLock(); private final boolean interceptionEnabled; private final InterceptionMetadata interceptionMetadata; + private final ServiceManager serviceManagerForInstanceName; // runtime fields (to obtain actual service instances) // service descriptor to its manager private final Map> servicesByDescriptor = new IdentityHashMap<>(); private final ActivationRequest activationRequest; + + private final Bindings bindings; + private final boolean allowLateBinding; + private Map> interceptors; @SuppressWarnings("unchecked") @@ -106,6 +111,9 @@ class CoreServiceRegistry implements ServiceRegistry, Scopes { this.interceptionMetadata = interceptionEnabled ? InterceptionMetadataImpl.create(this) : InterceptionMetadataImpl.noop(); + // again - we leak our instance early, but it is not used until runtime from bindings + this.bindings = new Bindings(this); + this.allowLateBinding = config.allowLateBinding(); // these must be bound here, as the instance exists now // (and we do not want to allow post-constructor binding) @@ -133,6 +141,8 @@ class CoreServiceRegistry implements ServiceRegistry, Scopes { For each known service descriptor, create an appropriate service manager */ descriptors.forEach(descriptor -> { + bindings.register(descriptor); + Object instance = explicitInstances.get(descriptor); ServiceProvider provider = new ServiceProvider<>( this, @@ -141,19 +151,23 @@ class CoreServiceRegistry implements ServiceRegistry, Scopes { if (instance != null) { Activator activator = Activators.create(provider, instance); servicesByDescriptor.put(descriptor, - new ServiceManager<>(scopeSupplier(descriptor), + new ServiceManager<>(this, + scopeSupplier(descriptor), provider, true, () -> activator)); } else { // we must always prefer explicit instances - so if specified, we will never override it servicesByDescriptor.putIfAbsent(descriptor, - new ServiceManager<>(scopeSupplier(descriptor), + new ServiceManager<>(this, + scopeSupplier(descriptor), provider, false, Activators.create(this, provider))); } }); + + this.serviceManagerForInstanceName = new InstanceNameServiceManager(this); } @Override @@ -361,6 +375,10 @@ public RegistryMetrics metrics() { return metrics; } + Bindings bindings() { + return bindings; + } + void add(Class contract, double weight, T instance) { stateWriteLock.lock(); try { @@ -377,7 +395,8 @@ void add(Class contract, double weight, T instance) { vt); Activator activator = Activators.create(provider, instance); - servicesByDescriptor.put(vt, new ServiceManager<>(scopeSupplier(vt), + servicesByDescriptor.put(vt, new ServiceManager<>(this, + scopeSupplier(vt), provider, true, () -> activator)); @@ -396,6 +415,11 @@ void add(Class contract, double weight, T instance) { @SuppressWarnings("unchecked") void set(Class contract, T[] instances) { + if (!allowLateBinding) { + throw new ServiceRegistryException("This service registry instance does not support late binding, as it was " + + "explicitly disabled through registry configuration: " + id); + } + stateWriteLock.lock(); try { ResolvedType contractType = ResolvedType.create(contract); @@ -413,7 +437,8 @@ void set(Class contract, T[] instances) { vt); Activator activator = Activators.create(provider, instance); - servicesByDescriptor.put(vt, new ServiceManager<>(scopeSupplier(vt), + servicesByDescriptor.put(vt, new ServiceManager<>(this, + scopeSupplier(vt), provider, true, () -> activator)); @@ -432,11 +457,14 @@ void set(Class contract, T[] instances) { + "A service provider must have exactly one instance."); } Activator activator = Activators.create(provider, instances[0]); - servicesByDescriptor.put(serviceInfo, new ServiceManager<>(scopeSupplier(serviceInfo), + servicesByDescriptor.put(serviceInfo, new ServiceManager<>(this, + scopeSupplier(serviceInfo), provider, true, () -> activator)); } + // reset bindings, as build-time binding would ignore instances explicitly set + bindings.forgetContract(contractType); } finally { stateWriteLock.unlock(); } @@ -461,6 +489,9 @@ List servicesByContract(ResolvedType contract) { @SuppressWarnings("unchecked") ServiceManager serviceManager(ServiceInfo info) { + if (info == InstanceName__ServiceDescriptor.INSTANCE) { + return (ServiceManager) serviceManagerForInstanceName; + } ServiceManager result = (ServiceManager) servicesByDescriptor.get(info); if (result == null) { throw new ServiceRegistryException("Attempt to use service info not managed by this registry: " + info); @@ -480,7 +511,7 @@ void interceptors(ServiceInfo... serviceInfos) { Set ordered = new TreeSet<>(SERVICE_INFO_COMPARATOR); for (ServiceInfo serviceInfo : serviceInfos) { ServiceManager serviceManager = this.serviceManager(serviceInfo); - ordered.add(serviceManager.injectDescriptor()); + ordered.add(serviceManager.descriptor()); } // there may be more than one application, we need to add to existing @@ -539,7 +570,7 @@ List> interceptors() { void ensureInjectionPlans() { servicesByDescriptor.values() - .forEach(ServiceManager::ensureInjectionPlan); + .forEach(ServiceManager::ensureBindingPlan); } private void cacheAndAccess(Lookup lookup, List result) { diff --git a/service/registry/src/main/java/io/helidon/service/registry/DependencyBlueprint.java b/service/registry/src/main/java/io/helidon/service/registry/DependencyBlueprint.java index a11d9536a37..d651f296ee2 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/DependencyBlueprint.java +++ b/service/registry/src/main/java/io/helidon/service/registry/DependencyBlueprint.java @@ -29,9 +29,9 @@ /** * Dependency metadata. - * The basic dependency supports other services to be passed to a constructor parameter. - * The dependency may be a contract, {@link java.util.List} of contracts, or an {@link java.util.Optional} - * of contract, or {@link java.util.function.Supplier} of any of these. + *

+ * Dependencies can be injected into a service through a constructor. We also support field injection, though it is not + * recommended due to complicated unit testing. */ @Prototype.Blueprint interface DependencyBlueprint { @@ -140,4 +140,28 @@ interface DependencyBlueprint { */ @Option.Redundant(stringValue = false) Optional method(); + + /** + * Cardinality of this dependency. Defaults to {@link io.helidon.service.registry.DependencyCardinality#REQUIRED}. + * + * @return cardinality of this dependency + */ + @Option.Default("REQUIRED") + DependencyCardinality cardinality(); + + /** + * Whether this dependency uses {@link io.helidon.service.registry.ServiceInstance}. + * Defaults to {@code false}, which means the service is injected via its contract. + * + * @return whether the dependency is declared as a {@link io.helidon.service.registry.ServiceInstance} + */ + boolean isServiceInstance(); + + /** + * Whether this dependency uses a {@link java.util.function.Supplier} instead of a direct instance. + * This defaults to {@code false}. + * + * @return whether the dependency injection point uses a supplier + */ + boolean isSupplier(); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/DependencyCardinality.java b/service/registry/src/main/java/io/helidon/service/registry/DependencyCardinality.java new file mode 100644 index 00000000000..afb256e2047 --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/DependencyCardinality.java @@ -0,0 +1,19 @@ +package io.helidon.service.registry; + +/** + * Cardinality of the injection point. + */ +public enum DependencyCardinality { + /** + * Optional instance (the dependency is declared as {@code Optional}, this does not imply nullability. + */ + OPTIONAL, + /** + * Required instance. + */ + REQUIRED, + /** + * List of instances. + */ + LIST +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/DependencyPlanBinder.java b/service/registry/src/main/java/io/helidon/service/registry/DependencyPlanBinder.java index 24a4abb3b5d..137685140e3 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/DependencyPlanBinder.java +++ b/service/registry/src/main/java/io/helidon/service/registry/DependencyPlanBinder.java @@ -31,7 +31,7 @@ public interface DependencyPlanBinder { * @param descriptor the service to receive the injection plan. * @return the binder to use for binding the injection plan to the service provider */ - Binder bindTo(ServiceInfo descriptor); + Binder service(ServiceInfo descriptor); /** * Bind all discovered interceptors. @@ -53,129 +53,12 @@ interface Binder { * Binds a single service to the injection point identified by the id. * The injection point expects a single service instance. * - * @param dependency the injection point identity - * @param descriptor the service descriptor to bind to this identity - * @return the binder builder - */ - Binder bind(Dependency dependency, - ServiceInfo descriptor); - - /** - * Bind to an optional field, with zero or one services. - * The injection point expects an {@link java.util.Optional} of service instance. - * - * @param dependency injection point identity - * @param descriptors the service descriptor to bind (zero or one) - * @return the binder builder - */ - Binder bindOptional(Dependency dependency, - ServiceInfo... descriptors); - - /** - * Binds to a list field, with zero or more services. - * The injection point expects a {@link java.util.List} of service instances. - * - * @param dependency the injection point identity - * @param descriptors service descriptors to bind to this identity (zero or more) - * @return the binder builder - */ - Binder bindList(Dependency dependency, - ServiceInfo... descriptors); - - /** - * Binds to a supplier field. - * The injection point expects a {@link java.util.function.Supplier} of service. - * - * @param dependency the injection point identity - * @param descriptor the service descriptor to bind to this identity. - * @return the binder builder - */ - Binder bindSupplier(Dependency dependency, - ServiceInfo descriptor); - - /** - * Bind to a supplier of optional field. - * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.Optional} of service. - * - * @param dependency injection point identity - * @param descriptor the service descriptor to bind (zero or one) - * @return the binder builder - */ - Binder bindSupplierOfOptional(Dependency dependency, - ServiceInfo... descriptor); - - /** - * Bind to an optional supplier field. - * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.Optional} of service. - * - * @param dependency injection point identity - * @param descriptor the service descriptor to bind (zero or one) - * @return the binder builder - */ - Binder bindOptionalOfSupplier(Dependency dependency, - ServiceInfo... descriptor); - - /** - * Bind to a supplier of list. - * The injection point expects a {@link java.util.function.Supplier} of {@link java.util.List} of services. - * * @param dependency the injection point identity - * @param descriptors service descriptor to bind to this identity (zero or more) + * @param descriptor the service descriptor to bind to this identity * @return the binder builder */ - Binder bindSupplierOfList(Dependency dependency, - ServiceInfo... descriptors); - - /** - * Bind to a list of suppliers. - * The injection point expects a {@link java.util.List} of {@link java.util.function.Supplier Suppliers} of service. - * - * @param dependency the injection point identity - * @param descriptors service descriptor to bind to this identity (zero or more) - * @return the binder builder - */ - Binder bindListOfSuppliers(Dependency dependency, - ServiceInfo... descriptors); - - /** - * Represents a null bind. - * - * @param dependency the injection point identity - * @return the binder builder - */ - Binder bindNull(Dependency dependency); - - /** - * Bind service instance. - * - * @param dependency the injection point identity - * @param descriptor the service descriptor to bind - * @return the binder builder - */ - Binder bindServiceInstance(Dependency dependency, ServiceInfo descriptor); - - /** - * Bind to a list of service instances. - * - * @param dependency the injection point identity - * @param descriptors the service descriptors to bind (zero or more) - * @return the binder builder - */ - Binder bindServiceInstanceList(Dependency dependency, ServiceInfo... descriptors); - - /** - * Bind to an optional of service instance. - * - * @param dependency the injection point identity - * @param descriptor the service descriptor to bind (zero or one) - * @return the binder builder - */ - Binder bindOptionalOfServiceInstance(Dependency dependency, ServiceInfo... descriptor); - - /** - * Commits the bindings for this service provider. - */ - void commit(); + Binder bind(Dependency dependency, + ServiceInfo... descriptor); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/InjectionContext.java b/service/registry/src/main/java/io/helidon/service/registry/InjectionContext.java deleted file mode 100644 index 1ca996e9bb8..00000000000 --- a/service/registry/src/main/java/io/helidon/service/registry/InjectionContext.java +++ /dev/null @@ -1,45 +0,0 @@ -/* - * Copyright (c) 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.service.registry; - -import java.util.Map; -import java.util.NoSuchElementException; - -class InjectionContext implements DependencyContext { - private final Map> injectionPlan; - - InjectionContext(Map> injectionPlan) { - this.injectionPlan = injectionPlan; - } - - static DependencyContext create(Map> injectionPlan) { - return new InjectionContext(injectionPlan); - } - - @SuppressWarnings("unchecked") - @Override - public T dependency(Dependency dependency) { - IpPlan ipPlan = injectionPlan.get(dependency); - if (ipPlan == null) { - throw new NoSuchElementException("Cannot resolve injection id " + dependency + " for service " - + dependency.service().fqName() - + ", this dependency was not declared in " - + "the service descriptor"); - } - return (T) ipPlan.get(); - } -} diff --git a/service/registry/src/main/java/io/helidon/service/registry/InstanceNameServiceManager.java b/service/registry/src/main/java/io/helidon/service/registry/InstanceNameServiceManager.java new file mode 100644 index 00000000000..26676e53797 --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/InstanceNameServiceManager.java @@ -0,0 +1,17 @@ +package io.helidon.service.registry; + +class InstanceNameServiceManager extends ServiceManager { + InstanceNameServiceManager(CoreServiceRegistry registry) { + // this is never used - we replace it in dependency injection plan with the correct instance + super(registry, () -> null, null, true, () -> null); + } + + @Override + void ensureBindingPlan() { + } + + @Override + ServiceInfo descriptor() { + return InstanceName__ServiceDescriptor.INSTANCE; + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/InterceptionMetadataImpl.java b/service/registry/src/main/java/io/helidon/service/registry/InterceptionMetadataImpl.java index ad76cbc0c66..bb388af3037 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/InterceptionMetadataImpl.java +++ b/service/registry/src/main/java/io/helidon/service/registry/InterceptionMetadataImpl.java @@ -112,11 +112,11 @@ private List> interceptors(List t List> result = new ArrayList<>(); for (ServiceManager interceptor : allInterceptors) { - if (applicable(typeAnnotations, interceptor.injectDescriptor())) { + if (applicable(typeAnnotations, interceptor.descriptor())) { result.add(new ServiceSupply<>(Lookup.EMPTY, List.of(interceptor))); continue; } - if (applicable(element.annotations(), interceptor.injectDescriptor())) { + if (applicable(element.annotations(), interceptor.descriptor())) { result.add(new ServiceSupply<>(Lookup.EMPTY, List.of(interceptor))); } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/IpPlan.java b/service/registry/src/main/java/io/helidon/service/registry/IpPlan.java deleted file mode 100644 index 8aed49ffe23..00000000000 --- a/service/registry/src/main/java/io/helidon/service/registry/IpPlan.java +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.service.registry; - -import java.util.function.Supplier; - -/** - * Injection point plan of injection. - * - * @param valueSupplier supplier of the value - * @param descriptors descriptor(s) used to obtain the value(s) in the supplier - * @param type of the value - */ -record IpPlan(Supplier valueSupplier, - ServiceInfo... descriptors) implements Supplier { - @Override - public T get() { - return valueSupplier.get(); - } -} diff --git a/service/registry/src/main/java/io/helidon/service/registry/RegistryStartupProvider.java b/service/registry/src/main/java/io/helidon/service/registry/RegistryStartupProvider.java new file mode 100644 index 00000000000..424c9ca298d --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/RegistryStartupProvider.java @@ -0,0 +1,102 @@ +package io.helidon.service.registry; + +import java.util.List; +import java.util.Optional; + +import io.helidon.Main; +import io.helidon.common.Weight; +import io.helidon.common.Weighted; +import io.helidon.spi.HelidonShutdownHandler; +import io.helidon.spi.HelidonStartupProvider; + +/** + * {@link java.util.ServiceLoader} implementation of a Helidon startup provider for Helidon Service Registry based + * applications. + */ +@Weight(Weighted.DEFAULT_WEIGHT) // explicit default weight, this should be the "default" startup class +public class RegistryStartupProvider implements HelidonStartupProvider { + private static final System.Logger LOGGER = System.getLogger(RegistryStartupProvider.class.getName()); + + /** + * Default constructor required by {@link java.util.ServiceLoader}. + * + * @deprecated please do not use directly + */ + @Deprecated + public RegistryStartupProvider() { + } + + /** + * Register a shutdown handler. + * The handler is registered, and never de-registered. This method should only be used by main classes of applications. + * If a custom shutdown is desired, please use + * {@link io.helidon.Main#addShutdownHandler(io.helidon.spi.HelidonShutdownHandler)} directly. + * + * @param registryManager registry manager + */ + public static void registerShutdownHandler(ServiceRegistryManager registryManager) { + System.Logger logger = System.getLogger(RegistryStartupProvider.class.getName()); + Main.addShutdownHandler(new RegistryShutdownHandler(logger, registryManager)); + } + + @Override + public void start(String[] arguments) { + var manager = ServiceRegistryManager.create(); + var registry = manager.registry(); + registerShutdownHandler(manager); + + for (Double runLevel : runLevels(registry)) { + List all = registry.all(Lookup.builder() + .addScope(Service.Singleton.TYPE) + .runLevel(runLevel) + .build()); + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.DEBUG, "Starting services in run level: " + runLevel + ": "); + for (Object o : all) { + LOGGER.log(System.Logger.Level.DEBUG, "\t" + o); + } + } else if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.TRACE, "Starting services in run level: " + runLevel); + } + } + } + + private List runLevels(ServiceRegistry registry) { + // child classes will have this method code generated at build time + return registry.lookupServices(Lookup.EMPTY) + .stream() + .map(ServiceInfo::runLevel) + .flatMap(Optional::stream) + .distinct() + .sorted() + .toList(); + } + + // higher than default, so we stop server as a service, not through shutdown + @Weight(Weighted.DEFAULT_WEIGHT + 10) + private static final class RegistryShutdownHandler implements HelidonShutdownHandler { + private final System.Logger logger; + private final ServiceRegistryManager registryManager; + + private RegistryShutdownHandler(System.Logger logger, ServiceRegistryManager registryManager) { + this.logger = logger; + this.registryManager = registryManager; + } + + @Override + public void shutdown() { + try { + registryManager.shutdown(); + } catch (Exception e) { + logger.log(System.Logger.Level.ERROR, + "Failed to shutdown Helidon Inject registry", + e); + } + } + + @Override + public String toString() { + return "Helidon Inject shutdown handler"; + } + } +} diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceManager.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceManager.java index 7abd5a5d6d0..9d71249b3e9 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceManager.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceManager.java @@ -30,12 +30,15 @@ class ServiceManager { private final ServiceProvider provider; private final boolean explicitInstance; private final Supplier> activatorSupplier; + private final CoreServiceRegistry registry; private final Supplier scopeSupplier; - ServiceManager(Supplier scopeSupplier, + ServiceManager(CoreServiceRegistry registry, + Supplier scopeSupplier, ServiceProvider provider, boolean explicitInstance, Supplier> activatorSupplier) { + this.registry = registry; this.scopeSupplier = scopeSupplier; this.provider = provider; this.explicitInstance = explicitInstance; @@ -47,12 +50,14 @@ public String toString() { return provider.descriptor().serviceType().classNameWithEnclosingNames(); } - void ensureInjectionPlan() { + void ensureBindingPlan() { if (explicitInstance) { // we do not need injection plan, if service was provided as an instance return; } - provider.injectionPlan(); + registry.bindings() + .bindingPlan(provider.descriptor()) + .ensure(); } ServiceInstance registryInstance(Lookup lookup, QualifiedInstance instance) { @@ -61,18 +66,10 @@ ServiceInstance registryInstance(Lookup lookup, QualifiedInstance instance instance); } - DependencyPlanBinder.Binder servicePlanBinder() { - return provider.servicePlanBinder(); - } - ServiceInfo descriptor() { return provider.descriptor(); } - ServiceInfo injectDescriptor() { - return provider.descriptor(); - } - /* Get service activator for the scope it is in (always works for singleton, may fail for other) this provides an instance of an activator that is bound to a scope instance diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServicePlanBinder.java b/service/registry/src/main/java/io/helidon/service/registry/ServicePlanBinder.java deleted file mode 100644 index dddad478bad..00000000000 --- a/service/registry/src/main/java/io/helidon/service/registry/ServicePlanBinder.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Copyright (c) 2024 Oracle and/or its affiliates. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package io.helidon.service.registry; - -import java.util.ArrayList; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.function.Consumer; -import java.util.function.Supplier; -import java.util.stream.Stream; - -import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupply; -import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupplyList; -import io.helidon.service.registry.ServiceSupplies.ServiceInstanceSupplyOptional; -import io.helidon.service.registry.ServiceSupplies.ServiceSupply; -import io.helidon.service.registry.ServiceSupplies.ServiceSupplyList; -import io.helidon.service.registry.ServiceSupplies.ServiceSupplyOptional; - -class ServicePlanBinder implements DependencyPlanBinder.Binder { - private final Map> injectionPlan = new LinkedHashMap<>(); - - private final ServiceDescriptor self; - private final Consumer>> injectionPlanConsumer; - private final CoreServiceRegistry registry; - - private ServicePlanBinder(CoreServiceRegistry registry, - ServiceDescriptor self, - Consumer>> injectionPlanConsumer) { - this.registry = registry; - this.self = self; - this.injectionPlanConsumer = injectionPlanConsumer; - } - - static DependencyPlanBinder.Binder create(CoreServiceRegistry registry, - ServiceDescriptor descriptor, - Consumer>> planConsumer) { - return new ServicePlanBinder(registry, descriptor, planConsumer); - } - - @Override - public DependencyPlanBinder.Binder bind(Dependency dependency, ServiceInfo descriptor) { - if (descriptor == InstanceName__ServiceDescriptor.INSTANCE) { - injectionPlan.put(dependency, new IpPlan<>(new InstanceNameFailingSupplier(dependency), descriptor)); - } else { - ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), - List.of(registry.serviceManager(descriptor))); - - injectionPlan.put(dependency, new IpPlan<>(supply, descriptor)); - } - return this; - } - - @Override - public DependencyPlanBinder.Binder bindServiceInstance(Dependency dependency, ServiceInfo descriptor) { - var supply = new ServiceInstanceSupply<>(Lookup.create(dependency), List.of(registry.serviceManager(descriptor))); - injectionPlan.put(dependency, new IpPlan<>(supply, descriptor)); - - return this; - } - - @Override - public DependencyPlanBinder.Binder bindOptional(Dependency dependency, ServiceInfo... descriptors) { - ServiceSupplyOptional supply = new ServiceSupplyOptional<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindOptionalOfServiceInstance(Dependency dependency, ServiceInfo... descriptors) { - var supply = new ServiceInstanceSupplyOptional<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindList(Dependency dependency, ServiceInfo... descriptors) { - ServiceSupplyList supply = new ServiceSupplyList<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindServiceInstanceList(Dependency dependency, ServiceInfo... descriptors) { - var supply = new ServiceInstanceSupplyList<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindSupplier(Dependency dependency, ServiceInfo descriptor) { - ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), - toManagers(descriptor)); - - injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptor)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindSupplierOfOptional(Dependency dependency, ServiceInfo... descriptors) { - ServiceSupplyOptional supply = new ServiceSupplyOptional<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindSupplierOfList(Dependency dependency, ServiceInfo... descriptors) { - ServiceSupplyList supply = new ServiceSupplyList<>(Lookup.create(dependency), - toManagers(descriptors)); - - injectionPlan.put(dependency, new IpPlan<>(() -> supply, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindOptionalOfSupplier(Dependency dependency, ServiceInfo... descriptors) { - // we must resolve this right now, so we just use the first descriptor, and hope the user did not inject - // this in a wrong scope - ServiceSupply supply = new ServiceSupply<>(Lookup.create(dependency), - toManagers(descriptors[0])); - injectionPlan.put(dependency, new IpPlan<>(() -> Optional.of(supply), descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindListOfSuppliers(Dependency dependency, ServiceInfo... descriptors) { - Lookup lookup = Lookup.create(dependency); - // we must resolve the list right now (one for each descriptor) - List> supplies = Stream.of(descriptors) - .map(this::toManagers) - .map(it -> new ServiceSupply<>(lookup, it)) - .toList(); - - injectionPlan.put(dependency, new IpPlan<>(() -> supplies, descriptors)); - return this; - } - - @Override - public DependencyPlanBinder.Binder bindNull(Dependency dependency) { - injectionPlan.put(dependency, new IpPlan<>(() -> null)); - return this; - } - - @Override - public void commit() { - injectionPlanConsumer.accept(Map.copyOf(injectionPlan)); - } - - @Override - public String toString() { - return "Service plan binder for " + self.serviceType(); - } - - private List> toManagers(ServiceInfo... descriptors) { - List> result = new ArrayList<>(); - for (ServiceInfo descriptor : descriptors) { - result.add(registry.serviceManager(descriptor)); - } - return result; - } - - private static final class InstanceNameFailingSupplier implements Supplier { - private final Dependency dependency; - - private InstanceNameFailingSupplier(Dependency dependency) { - this.dependency = dependency; - } - - @Override - public Object get() { - throw new ServiceRegistryException( - "@" + Service.InstanceName.class.getName() - + "should have been resolved to correct name during lookup for " - + dependency); - } - } -} diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceProvider.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceProvider.java index bf2d5a25c13..cd005cd5c46 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceProvider.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceProvider.java @@ -16,14 +16,10 @@ package io.helidon.service.registry; -import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.Set; -import java.util.concurrent.atomic.AtomicReference; import io.helidon.common.types.ResolvedType; -import io.helidon.common.types.TypeName; /** * Takes care of a single service descriptor. @@ -31,14 +27,11 @@ * @param type of the provided service */ class ServiceProvider { - private final CoreServiceRegistry registry; - private final ServiceInfo serviceInfo; private final ServiceDescriptor descriptor; private final ActivationRequest activationRequest; private final InterceptionMetadata interceptionMetadata; private final Contracts.ContractLookup contracts; - private volatile Map> injectionPlan = null; ServiceProvider(CoreServiceRegistry serviceRegistry, ServiceDescriptor descriptor) { @@ -46,10 +39,8 @@ class ServiceProvider { Objects.requireNonNull(serviceRegistry); Objects.requireNonNull(descriptor); - this.registry = serviceRegistry; - this.interceptionMetadata = registry.interceptionMetadata(); - this.activationRequest = registry.activationRequest(); - this.serviceInfo = descriptor; + this.interceptionMetadata = serviceRegistry.interceptionMetadata(); + this.activationRequest = serviceRegistry.activationRequest(); this.descriptor = descriptor; this.contracts = Contracts.create(descriptor); @@ -57,31 +48,13 @@ class ServiceProvider { @Override public String toString() { - return "ServiceProvider for " + serviceInfo.serviceType().fqName(); - } - - ServiceInfo serviceInfo() { - return serviceInfo; + return "ServiceProvider for " + descriptor.serviceType().fqName(); } ServiceDescriptor descriptor() { return descriptor; } - DependencyPlanBinder.Binder servicePlanBinder() { - return ServicePlanBinder.create(registry, descriptor, it -> this.injectionPlan = it); - } - - Map> injectionPlan() { - Map> usedIp = injectionPlan; - if (usedIp == null) { - // no application, we have to create injection plan from current services - usedIp = createInjectionPlan(); - this.injectionPlan = usedIp; - } - return usedIp; - } - InterceptionMetadata interceptionMetadata() { return interceptionMetadata; } @@ -93,117 +66,4 @@ Set contracts(Lookup lookup) { ActivationRequest activationRequest() { return activationRequest; } - - private Map> createInjectionPlan() { - // for core services, we must use Dependency, for inject services, we must use Dependency - List dependencies = descriptor.dependencies(); - - if (dependencies.isEmpty()) { - return Map.of(); - } - - AtomicReference>> injectionPlan = new AtomicReference<>(); - - DependencyPlanBinder.Binder binder = ServicePlanBinder.create(registry, - descriptor, - injectionPlan::set); - for (Dependency injectionPoint : dependencies) { - planForIp(binder, injectionPoint); - } - - binder.commit(); - - return injectionPlan.get(); - } - - private void planForIp(DependencyPlanBinder.Binder injectionPlan, - Dependency injectionPoint) { - /* - very similar code is used in ApplicationCreator.buildTimeBinding - make sure this is kept in sync! - */ - Lookup lookup = Lookup.create(injectionPoint); - - if (descriptor.contracts().containsAll(lookup.contracts()) - && descriptor.qualifiers().equals(lookup.qualifiers())) { - // injection point lookup must have a single contract for each injection point - // if this service implements the contracts actually required, we must look for services with lower weight - // but only if we also have the same qualifiers - lookup = Lookup.builder(lookup) - .weight(descriptor.weight()) - .build(); - } - - List discovered = registry.lookupServices(lookup) - .stream() - .filter(it -> it != descriptor) - .toList(); - - /* - Very similar code is used for build time code generation in ApplicationCreator.buildTimeBinding - make sure this is kept in sync! - */ - TypeName ipType = injectionPoint.typeName(); - - // now there are a few options - optional, list, and single instance - if (ipType.isList()) { - ServiceInfo[] descriptors = discovered.toArray(new ServiceInfo[0]); - TypeName typeOfList = ipType.typeArguments().getFirst(); - if (typeOfList.isSupplier()) { - // inject List> - injectionPlan.bindListOfSuppliers(injectionPoint, descriptors); - } else if (typeOfList.equals(ServiceInstance.TYPE)) { - injectionPlan.bindServiceInstanceList(injectionPoint, descriptors); - } else { - // inject List - injectionPlan.bindList(injectionPoint, descriptors); - } - } else if (ipType.isOptional()) { - // inject Optional - if (discovered.isEmpty()) { - injectionPlan.bindOptional(injectionPoint); - } else { - TypeName typeOfOptional = ipType.typeArguments().getFirst(); - if (typeOfOptional.isSupplier()) { - injectionPlan.bindOptionalOfSupplier(injectionPoint, discovered.getFirst()); - } else if (typeOfOptional.equals(ServiceInstance.TYPE)) { - injectionPlan.bindOptionalOfServiceInstance(injectionPoint, discovered.getFirst()); - } else { - injectionPlan.bindOptional(injectionPoint, discovered.getFirst()); - } - } - } else if (ipType.isSupplier()) { - // one of the supplier options - TypeName typeOfSupplier = ipType.typeArguments().getFirst(); - if (typeOfSupplier.isOptional()) { - // inject Supplier> - injectionPlan.bindSupplierOfOptional(injectionPoint, discovered.toArray(new ServiceInfo[0])); - } else if (typeOfSupplier.isList()) { - // inject Supplier> - injectionPlan.bindSupplierOfList(injectionPoint, discovered.toArray(new ServiceInfo[0])); - } else { - // inject Supplier - if (discovered.isEmpty()) { - // null binding is not supported at runtime - throw new ServiceRegistryException(injectionPoint.service().fqName() - + ": expected to resolve a service matching injection point " - + injectionPoint); - } - injectionPlan.bindSupplier(injectionPoint, discovered.getFirst()); - } - } else { - // inject Contract - if (discovered.isEmpty()) { - // null binding is not supported at runtime - throw new ServiceRegistryException(injectionPoint.service().fqName() - + ": expected to resolve a service matching injection point " - + injectionPoint); - } - if (ipType.equals(ServiceInstance.TYPE)) { - injectionPlan.bindServiceInstance(injectionPoint, discovered.getFirst()); - } else { - injectionPlan.bind(injectionPoint, discovered.getFirst()); - } - } - } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java index 3c79e6968ba..4e8891d21b5 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryConfigBlueprint.java @@ -117,9 +117,10 @@ interface ServiceRegistryConfigBlueprint { /** * Flag indicating whether compile-time generated {@link io.helidon.service.registry.Binding}'s - * should be used at initialization. - * Even if set to {@code true}, this is effective only if an {@link io.helidon.service.registry.Binding} - * was generated using Helidon Service Maven Plugin. + * should be used at initialization when starting the registry using + * {@link io.helidon.service.registry.ServiceRegistryManager#start(Binding)}. + *

+ * This option is ignored when starting the service registry in any other way. * * @return the flag indicating whether the provider is permitted to use binding generated code from compile-time, * defaults to {@code true} @@ -127,4 +128,29 @@ interface ServiceRegistryConfigBlueprint { */ @Option.DefaultBoolean(true) boolean useBinding(); + + /** + * Maximal run level to handle when starting from {@link io.helidon.service.registry.ServiceRegistryManager#start(Binding)}. + * This setting is ignored when starting registry using + * other means, as run levels are not handled by default. + * + * @return maximal run level to lookup during application startup when using generated binding + */ + @Option.DefaultDouble(Double.MAX_VALUE) + double maxRunLevel(); + + /** + * Run levels that should be initialized at startup. + * Generated {@link io.helidon.service.registry.Binding} will configure all declared run levels of services in the + * application. + *

+ * Note that the result WILL be ordered before use, so initialization will always be handled from the smallest run level + * to the highest. + * + * @return run levels to initialize, up to {@link #maxRunLevel()}, only used when starting the registry through + * {@link io.helidon.service.registry.ServiceRegistryManager#start(Binding)} + * or {@link io.helidon.service.registry.ServiceRegistryManager#start(Binding, ServiceRegistryConfig)} + */ + @Option.Singular + List runLevels(); } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryManager.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryManager.java index 9d1f32e4b3c..2b863aea3ec 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryManager.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceRegistryManager.java @@ -31,11 +31,9 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import io.helidon.common.types.Annotation; import io.helidon.common.types.ResolvedType; import io.helidon.common.types.TypeName; import io.helidon.common.types.TypeNames; -import io.helidon.common.types.TypedElementInfo; import io.helidon.service.metadata.DescriptorMetadata; /** @@ -63,7 +61,6 @@ public final class ServiceRegistryManager { }) .thenComparing(ServiceInfo::serviceType); private static final System.Logger LOGGER = System.getLogger(ServiceRegistryManager.class.getName()); - private static final InterceptionMetadata NO_OP_INTERCEPT_META = new NoOpInterceptMeta(); private final ReentrantReadWriteLock lifecycleLock = new ReentrantReadWriteLock(); private final ServiceRegistryConfig config; @@ -76,6 +73,71 @@ public final class ServiceRegistryManager { this.discovery = serviceDiscovery; } + /** + * Create a new manager based on the provided binding (usually code generated), and start the service registry + * services according to the configured run levels. Honor configured options. + *

+ * Configuration options are handled as follows: + *

    + *
  • {@link ServiceRegistryConfig#runLevels()} - if any run level is configured, it is honored; if no run levels + * are configured (the default), run levels are updated from generated bindings; to disable any run levels, set + * the {@link ServiceRegistryConfig#maxRunLevel()} to 0
  • + *
  • {@link ServiceRegistryConfig#discoverServices()} - honored as configured; as default is {@code true}, + * we recommend you set this to {@code false}, as all services should be registered explicitly via the generated + * binding
  • + *
  • {@link ServiceRegistryConfig#serviceDescriptors()} - honored, and additional descriptors are added via the + * generated binding; usually this should not be configured by hand, as there should not be additional descriptors + * that were not discovered by the plugin that generates build time binding
  • + *
  • All other configuration options are honored as configured, and not updated
  • + *
+ * + * @param binding generated binding + * @param config configuration to use (see rules above) + * @return a new registry manager with an initialized registry + */ + public static ServiceRegistryManager start(Binding binding, ServiceRegistryConfig config) { + ServiceRegistryConfig.Builder configBuilder = ServiceRegistryConfig.builder(config) + .update(binding::configure); + + if (!config.runLevels().isEmpty()) { + configBuilder.runLevels(config.runLevels()); + } + + ServiceRegistryConfig updatedConfig = configBuilder.build(); + ServiceRegistryManager manager = create(updatedConfig); + + return boundManager(binding, updatedConfig, manager); + } + + /** + * Start the service registry with no generated binding with the provided config. + * This method honors {@link ServiceRegistryConfig#maxRunLevel()} and {@link ServiceRegistryConfig#runLevels()} + * to initialize services that fit. + * + * @param config configuration of the service registry + * @return a new registry manager with initialized registry + */ + public static ServiceRegistryManager start(ServiceRegistryConfig config) { + return start(new NoOpBinding(), config); + } + + /** + * Create a new manager based on the provided binding (usually code generated), and start the service registry + * services according to the configured run levels. + * + * @param binding generated binding + * @return a new registry manager with an initialized registry + */ + public static ServiceRegistryManager start(Binding binding) { + ServiceRegistryConfig config = ServiceRegistryConfig.builder() + .discoverServices(false) + .update(binding::configure) + .build(); + + ServiceRegistryManager manager = create(config); + return boundManager(binding, config, manager); + } + /** * Create a new service registry manager with default configuration. * @@ -104,8 +166,90 @@ public static ServiceRegistryManager create(ServiceRegistryConfig config) { * * @return service registry ready to be used */ - @SuppressWarnings("checkstyle:methodLength") // already condensed; further extraction would decrease readability public ServiceRegistry registry() { + return registry(new NoOpBinding()); + } + + /** + * Shutdown the managed service registry. + */ + public void shutdown() { + Lock lock = lifecycleLock.writeLock(); + try { + lock.lock(); + if (registry == null) { + // registry was never requested, + return; + } + + registry.shutdown(); + registry = null; + } finally { + lock.unlock(); + } + } + + private static ServiceRegistryManager boundManager(Binding binding, + ServiceRegistryConfig config, + ServiceRegistryManager manager) { + ServiceRegistry registry = manager.registry(binding); + GlobalServiceRegistry.registry(registry); + + double maxRunLevel = config.maxRunLevel(); + List runLevels = new ArrayList<>(config.runLevels()); + Collections.sort(runLevels); + for (Double runLevel : runLevels) { + if (runLevel > maxRunLevel) { + // no more + break; + } + + List all = registry.all(Lookup.builder() + .addScope(Service.Singleton.TYPE) + .runLevel(runLevel) + .build()); + if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { + LOGGER.log(System.Logger.Level.DEBUG, "Starting services in run level: " + runLevel + ": "); + for (Object o : all) { + LOGGER.log(System.Logger.Level.DEBUG, "\t" + o); + } + } else if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { + LOGGER.log(System.Logger.Level.TRACE, "Starting services in run level: " + runLevel); + } + } + + return manager; + } + + @SuppressWarnings("rawtypes") + private static ServiceDescriptor virtualDescriptor(ServiceRegistryConfig config, + ServiceDiscovery discovery, + ServiceDescriptor descriptor) { + TypeName serviceType = descriptor.serviceType(); + var fromConfig = config.serviceDescriptors() + .stream() + .filter(registered -> registered.serviceType().equals(serviceType)) + .findFirst(); + + if (fromConfig.isPresent()) { + return fromConfig.get(); + } + + return discovery.allMetadata() + .stream() + .filter(handler -> contains(handler.contracts(), serviceType)) + .map(DescriptorHandler::descriptor) + .filter(desc -> desc.serviceType().equals(serviceType)) + .findFirst() + .map(it -> (ServiceDescriptor) it) + .orElse(descriptor); + } + + private static boolean contains(Set contracts, TypeName type) { + return contracts.stream().anyMatch(it -> it.type().equals(type)); + } + + private ServiceRegistry registry(Binding binding) { Lock readLock = lifecycleLock.readLock(); try { readLock.lock(); @@ -138,8 +282,6 @@ public ServiceRegistry registry() { Map> typedQualifiedProviders = new HashMap<>(); - List> bindings = new ArrayList<>(); - config.serviceInstances() .forEach((desc, instance) -> { var descriptor = desc; @@ -149,8 +291,7 @@ public ServiceRegistry registry() { } descriptors.add(descriptor); - bind(bindings, - scopeHandlers, + bind(scopeHandlers, servicesByType, servicesByContract, qualifiedProvidersByQualifier, @@ -161,8 +302,7 @@ public ServiceRegistry registry() { for (var descriptor : config.serviceDescriptors()) { descriptors.add(descriptor); - bind(bindings, - scopeHandlers, + bind(scopeHandlers, servicesByType, servicesByContract, qualifiedProvidersByQualifier, @@ -184,19 +324,14 @@ public ServiceRegistry registry() { } ServiceDescriptor descriptor = descriptorMeta.descriptor(); - if (contains(descriptor.contracts(), Binding.TYPE)) { - bindings.add((ServiceDescriptor) descriptor); - // applications are not bound to the registry - } else { - descriptors.add(descriptor); - bind(bindings, - scopeHandlers, - servicesByType, - servicesByContract, - qualifiedProvidersByQualifier, - typedQualifiedProviders, - descriptor); - } + + descriptors.add(descriptor); + bind(scopeHandlers, + servicesByType, + servicesByContract, + qualifiedProvidersByQualifier, + typedQualifiedProviders, + descriptor); } // add service registry information (service registry cannot be overridden in any way) @@ -206,8 +341,7 @@ public ServiceRegistry registry() { descriptors.add(scopesDescriptor); descriptors.add(registryDescriptor); - bind(bindings, - scopeHandlers, + bind(scopeHandlers, servicesByType, servicesByContract, qualifiedProvidersByQualifier, @@ -217,8 +351,7 @@ public ServiceRegistry registry() { ServiceDescriptor interceptDescriptor = InterceptionMetadata__ServiceDescriptor.INSTANCE; descriptors.add(interceptDescriptor); - bind(bindings, - scopeHandlers, + bind(scopeHandlers, servicesByType, servicesByContract, qualifiedProvidersByQualifier, @@ -246,14 +379,8 @@ public ServiceRegistry registry() { typedQualifiedProviders, accessedContracts); - // now check if we have an application, and if so, apply it if (config.useBinding()) { - for (ServiceDescriptor binding : bindings) { - // applications cannot have dependencies - Binding bindingInstance = (Binding) binding.instantiate(DependencyContext.create(Map.of()), - NO_OP_INTERCEPT_META); - bindingInstance.configure(new ApplicationPlanBinder(bindingInstance, registry)); - } + binding.binding(new ApplicationPlanBinder(binding, registry)); } // and if application was not bound using binding(s), we need to create the bindings now @@ -265,67 +392,13 @@ public ServiceRegistry registry() { } } - /** - * Shutdown the managed service registry. - */ - public void shutdown() { - Lock lock = lifecycleLock.writeLock(); - try { - lock.lock(); - if (registry == null) { - // registry was never requested, - return; - } - - registry.shutdown(); - registry = null; - } finally { - lock.unlock(); - } - } - - @SuppressWarnings("rawtypes") - private static ServiceDescriptor virtualDescriptor(ServiceRegistryConfig config, - ServiceDiscovery discovery, - ServiceDescriptor descriptor) { - TypeName serviceType = descriptor.serviceType(); - var fromConfig = config.serviceDescriptors() - .stream() - .filter(registered -> registered.serviceType().equals(serviceType)) - .findFirst(); - if (fromConfig.isPresent()) { - return fromConfig.get(); - } - - return discovery.allMetadata() - .stream() - .filter(handler -> contains(handler.contracts(), serviceType)) - .map(DescriptorHandler::descriptor) - .filter(desc -> desc.serviceType().equals(serviceType)) - .findFirst() - .map(it -> (ServiceDescriptor) it) - .orElse(descriptor); - } - - private static boolean contains(Set contracts, TypeName type) { - return contracts.stream().anyMatch(it -> it.type().equals(type)); - } - - @SuppressWarnings("unchecked") - private void bind(List> bindings, - Map scopeHandlers, + private void bind(Map scopeHandlers, Map servicesByType, Map> servicesByContract, Map> qualifiedProvidersByQualifier, Map> typedQualifiedProviders, ServiceDescriptor descriptor) { - if (contains(descriptor.contracts(), Binding.TYPE)) { - bindings.add((ServiceDescriptor) descriptor); - // application is not bound to the registry - return; - } - if (LOGGER.isLoggable(System.Logger.Level.TRACE)) { if (descriptor instanceof ServiceLoader__ServiceDescriptor sl) { LOGGER.log(System.Logger.Level.TRACE, @@ -418,21 +491,23 @@ private static class ApplicationPlanBinder implements DependencyPlanBinder { private final Binding appInstance; private final CoreServiceRegistry registry; + private final Bindings bindings; private ApplicationPlanBinder(Binding appInstance, CoreServiceRegistry registry) { this.appInstance = appInstance; this.registry = registry; + this.bindings = registry.bindings(); } @Override - public Binder bindTo(ServiceInfo descriptor) { - ServiceManager serviceManager = registry.serviceManager(descriptor); + public Binder service(ServiceInfo descriptor) { + Bindings.ServiceBindingPlan serviceBindingPlan = bindings.bindingPlan(descriptor); if (LOGGER.isLoggable(System.Logger.Level.DEBUG)) { - LOGGER.log(System.Logger.Level.DEBUG, "binding injection plan to " + serviceManager); + LOGGER.log(System.Logger.Level.DEBUG, "binding injection plan to " + descriptor.serviceType().fqName()); } - return serviceManager.servicePlanBinder(); + return new ServiceBinder(serviceBindingPlan); } @Override @@ -444,28 +519,36 @@ public void interceptors(ServiceInfo... descriptors) { public String toString() { return "Service binder for application: " + appInstance.name(); } + + private static class ServiceBinder implements Binder { + private final Bindings.ServiceBindingPlan serviceBindingPlan; + + ServiceBinder(Bindings.ServiceBindingPlan serviceBindingPlan) { + this.serviceBindingPlan = serviceBindingPlan; + } + + @Override + public Binder bind(Dependency dependency, ServiceInfo... descriptor) { + serviceBindingPlan.binding(dependency) + .bind(List.of(descriptor)); + + return this; + } + } } - private static class NoOpInterceptMeta implements InterceptionMetadata { + private static class NoOpBinding implements Binding { + @Override + public String name() { + return "no-op"; + } + @Override - public InterceptionInvoker createInvoker(ServiceInfo descriptor, - Set typeQualifiers, - List typeAnnotations, - TypedElementInfo element, - InterceptionInvoker targetInvoker, - Set> checkedExceptions) { - return targetInvoker; + public void binding(DependencyPlanBinder binder) { } @Override - public InterceptionInvoker createInvoker(Object serviceInstance, - ServiceInfo descriptor, - Set typeQualifiers, - List typeAnnotations, - TypedElementInfo element, - InterceptionInvoker targetInvoker, - Set> checkedExceptions) { - return targetInvoker; + public void configure(ServiceRegistryConfig.Builder builder) { } } } diff --git a/service/registry/src/main/java/io/helidon/service/registry/ServiceSupplies.java b/service/registry/src/main/java/io/helidon/service/registry/ServiceSupplies.java index 7d0977a4cee..dc55ae4e269 100644 --- a/service/registry/src/main/java/io/helidon/service/registry/ServiceSupplies.java +++ b/service/registry/src/main/java/io/helidon/service/registry/ServiceSupplies.java @@ -54,7 +54,29 @@ private static List> explodeFilterAndSort(Lookup lookup, result.addAll(thisManager); } - result.sort(RegistryInstanceComparator.instance()); + if (result.isEmpty() || result.size() == 1) { + traceLookupInstances(lookup, result); + return List.copyOf(result); + } + + /* + The instances are now ordered by weight of the service providers (implementation or factory) + We now need to update the ordering, to put unqualified instances first (unless a qualified lookup was done) + We cannot re-order them using the usual comparator, as the weight of the instance is never set + */ + if (lookup.qualifiers().isEmpty()) { + List> unqualified = new ArrayList<>(); + List> qualified = new ArrayList<>(); + for (ServiceInstance instance : result) { + if (instance.qualifiers().isEmpty()) { + unqualified.add(instance); + } else { + qualified.add(instance); + } + } + unqualified.addAll(qualified); + result = unqualified; + } traceLookupInstances(lookup, result); diff --git a/service/registry/src/main/java/io/helidon/service/registry/SupplierDependencyContext.java b/service/registry/src/main/java/io/helidon/service/registry/SupplierDependencyContext.java new file mode 100644 index 00000000000..efeb7b1b479 --- /dev/null +++ b/service/registry/src/main/java/io/helidon/service/registry/SupplierDependencyContext.java @@ -0,0 +1,18 @@ +package io.helidon.service.registry; + +import java.util.Map; +import java.util.function.Supplier; + +class SupplierDependencyContext implements DependencyContext { + private final Map> dependencyPlan; + + SupplierDependencyContext(Map> dependencyPlan) { + this.dependencyPlan = dependencyPlan; + } + + @SuppressWarnings("unchecked") + @Override + public T dependency(Dependency dependency) { + return (T) dependencyPlan.get(dependency).get(); + } +} diff --git a/service/registry/src/main/java/module-info.java b/service/registry/src/main/java/module-info.java index c3e620f291a..46e95c92d74 100644 --- a/service/registry/src/main/java/module-info.java +++ b/service/registry/src/main/java/module-info.java @@ -32,9 +32,13 @@ requires io.helidon.service.metadata; requires io.helidon.metadata.hson; + requires io.helidon; requires transitive io.helidon.builder.api; requires transitive io.helidon.common.types; exports io.helidon.service.registry; + + provides io.helidon.spi.HelidonStartupProvider + with io.helidon.service.registry.RegistryStartupProvider; } \ No newline at end of file diff --git a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java index 923c13cfaf1..bb6b76fa664 100644 --- a/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java +++ b/service/tests/codegen/src/test/java/io/helidon/service/tests/codegen/ServiceCodegenTypesTest.java @@ -34,6 +34,7 @@ import io.helidon.service.codegen.ServiceCodegenTypes; import io.helidon.service.registry.Binding; import io.helidon.service.registry.Dependency; +import io.helidon.service.registry.DependencyCardinality; import io.helidon.service.registry.DependencyContext; import io.helidon.service.registry.DependencyPlanBinder; import io.helidon.service.registry.Event; @@ -45,9 +46,11 @@ import io.helidon.service.registry.InterceptionInvoker; import io.helidon.service.registry.InterceptionMetadata; import io.helidon.service.registry.Qualifier; +import io.helidon.service.registry.RegistryStartupProvider; import io.helidon.service.registry.Service; import io.helidon.service.registry.ServiceDescriptor; import io.helidon.service.registry.ServiceInstance; +import io.helidon.service.registry.ServiceLoader__ServiceDescriptor; import io.helidon.service.registry.ServiceRegistry; import io.helidon.service.registry.ServiceRegistryConfig; import io.helidon.service.registry.ServiceRegistryManager; @@ -93,6 +96,7 @@ void testTypes() { checkField(toCheck, checked, fields, "SERVICE_DEPENDENCY", Dependency.class); checkField(toCheck, checked, fields, "SERVICE_DEPENDENCY_CONTEXT", DependencyContext.class); checkField(toCheck, checked, fields, "SERVICE_DESCRIPTOR", ServiceDescriptor.class); + checkField(toCheck, checked, fields, "REGISTRY_STARTUP_PROVIDER", RegistryStartupProvider.class); checkField(toCheck, checked, fields, "BUILDER_BLUEPRINT", Prototype.Blueprint.class); checkField(toCheck, checked, fields, "GENERATED_ANNOTATION", Generated.class); @@ -114,8 +118,11 @@ void testTypes() { checkField(toCheck, checked, fields, "SERVICE_QUALIFIED_FACTORY", Service.QualifiedFactory.class); checkField(toCheck, checked, fields, "SERVICE_CONFIG", ServiceRegistryConfig.class); + checkField(toCheck, checked, fields, "SERVICE_CONFIG_BUILDER", ServiceRegistryConfig.Builder.class); checkField(toCheck, checked, fields, "SERVICE_REGISTRY", ServiceRegistry.class); checkField(toCheck, checked, fields, "SERVICE_REGISTRY_MANAGER", ServiceRegistryManager.class); + checkField(toCheck, checked, fields, "DEPENDENCY_CARDINALITY", DependencyCardinality.class); + checkField(toCheck, checked, fields, "SERVICE_LOADER_DESCRIPTOR", ServiceLoader__ServiceDescriptor.class); // api.Interception.* checkField(toCheck, checked, fields, "INTERCEPTION_INTERCEPTED", Interception.Intercepted.class); @@ -169,6 +176,10 @@ void testTypes() { .type(List.class) .addTypeArgument(TypeName.create(Dependency.class)) .build()); + checkField(toCheck, checked, fields, "LIST_OF_DOUBLES", TypeName.builder() + .type(List.class) + .addTypeArgument(TypeName.create(Double.class)) + .build()); checkField(toCheck, checked, fields, "SET_OF_QUALIFIERS", TypeName.builder() .type(Set.class) .addTypeArgument(TypeName.create(Qualifier.class)) diff --git a/service/tests/maven-plugin/src/it/projects/test1/pom.xml b/service/tests/maven-plugin/src/it/projects/test1/pom.xml index 5f7b01e6f61..475692cf740 100644 --- a/service/tests/maven-plugin/src/it/projects/test1/pom.xml +++ b/service/tests/maven-plugin/src/it/projects/test1/pom.xml @@ -81,6 +81,7 @@ true + true diff --git a/service/tests/maven-plugin/src/it/projects/test2/pom.xml b/service/tests/maven-plugin/src/it/projects/test2/pom.xml index 4e13b33360d..8b2a5732465 100644 --- a/service/tests/maven-plugin/src/it/projects/test2/pom.xml +++ b/service/tests/maven-plugin/src/it/projects/test2/pom.xml @@ -80,6 +80,7 @@ + true my.updated UpdatedMain UpdatedBinding diff --git a/service/tests/maven-plugin/src/it/projects/test3/pom.xml b/service/tests/maven-plugin/src/it/projects/test3/pom.xml index ecb517ed452..2abe8151597 100644 --- a/service/tests/maven-plugin/src/it/projects/test3/pom.xml +++ b/service/tests/maven-plugin/src/it/projects/test3/pom.xml @@ -81,7 +81,6 @@ false - false true diff --git a/service/tests/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java b/service/tests/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java index e3782e06c1f..878909e5961 100644 --- a/service/tests/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java +++ b/service/tests/maven-plugin/src/test/java/io/helidon/service/tests/inject/maven/plugin/ProjectsTestIT.java @@ -42,28 +42,26 @@ void test1(String basedir) { Path compiledClasses = projectPath.resolve("target/classes/my/module"); Path generatedSources = projectPath.resolve("target/generated-sources/annotations/my/module"); - assertThat("Generated by the Maven plugin", generatedSources.resolve("Injection__Binding.java"), fileExists()); - assertThat("Generated by the Maven plugin", - generatedSources.resolve("Injection__Binding__ServiceDescriptor.java"), - fileExists()); + assertThat("Generated by the Maven plugin", generatedSources.resolve("ApplicationBinding.java"), fileExists()); + assertThat("Should nto be generated by the Maven plugin", + generatedSources.resolve("ApplicationBinding__ServiceDescriptor.java"), + not(fileExists())); assertThat("Generated by the service inject codegen during compilation", generatedSources.resolve("ServiceType__ServiceDescriptor.java"), fileExists()); - // Main class not yet ready - // assertThat("Generated by the Maven plugin", generatedSources.resolve("ApplicationMain.java"), fileExists()); + assertThat("Generated by the Maven plugin", generatedSources.resolve("ApplicationMain.java"), fileExists()); assertThat("Not a generated type, exists in project sources only", generatedSources.resolve("ServiceType.java"), not(fileExists())); - assertThat("Generated by the Maven plugin", compiledClasses.resolve("Injection__Binding.class"), fileExists()); - assertThat("Generated by the Maven plugin", - compiledClasses.resolve("Injection__Binding__ServiceDescriptor.class"), - fileExists()); + assertThat("Generated by the Maven plugin", compiledClasses.resolve("ApplicationBinding.class"), fileExists()); + assertThat("Should nto be generated by the Maven plugin", + generatedSources.resolve("ApplicationBinding__ServiceDescriptor.java"), + not(fileExists())); assertThat("Generated by the service inject codegen during compilation", compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), fileExists()); - // Main class not yet ready - // assertThat("Generated by the Maven plugin", compiledClasses.resolve("ApplicationMain.class"), fileExists()); + assertThat("Generated by the Maven plugin", compiledClasses.resolve("ApplicationMain.class"), fileExists()); assertThat("Compiled service", compiledClasses.resolve("ServiceType.class"), fileExists()); } @@ -85,11 +83,10 @@ void test2(String basedir) { fileExists()); assertThat("Generated by the Maven plugin", generatedCustomSources.resolve("UpdatedBinding__ServiceDescriptor.java"), + not(fileExists())); + assertThat("Generated by the Maven plugin", + generatedCustomSources.resolve("UpdatedMain.java"), fileExists()); - // Main class not yet ready - // assertThat("Generated by the Maven plugin", - // generatedCustomSources.resolve("UpdatedMain.java"), - // fileExists()); assertThat("Generated by the service inject codegen during compilation", generatedSources.resolve("ServiceType__ServiceDescriptor.java"), fileExists()); @@ -102,11 +99,10 @@ void test2(String basedir) { fileExists()); assertThat("Generated by the Maven plugin", compiledCustomClasses.resolve("UpdatedBinding__ServiceDescriptor.class"), + not(fileExists())); + assertThat("Generated by the Maven plugin", + compiledCustomClasses.resolve("UpdatedMain.class"), fileExists()); - // Main class not yet ready - // assertThat("Generated by the Maven plugin", - // compiledCustomClasses.resolve("UpdatedMain.class"), - // fileExists()); assertThat("Generated by the service inject codegen during compilation", compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), fileExists()); @@ -125,10 +121,10 @@ void test3(String basedir) { Path generatedSources = projectPath.resolve("target/generated-sources/annotations/my/module"); assertThat("Should not be generated by the Maven plugin", - generatedSources.resolve("Injection__Binding.java"), + generatedSources.resolve("ApplicationBinding.java"), not(fileExists())); assertThat("Should nto be generated by the Maven plugin", - generatedSources.resolve("Injection__Binding__ServiceDescriptor.java"), + generatedSources.resolve("ApplicationBinding__ServiceDescriptor.java"), not(fileExists())); assertThat("Generated by the service inject codegen during compilation", generatedSources.resolve("ServiceType__ServiceDescriptor.java"), @@ -141,10 +137,10 @@ void test3(String basedir) { not(fileExists())); assertThat("Should not be generated by the Maven plugin", - compiledClasses.resolve("Injection__Binding.class"), + compiledClasses.resolve("ApplicationBinding.class"), not(fileExists())); assertThat("Should nto be generated by the Maven plugin", - compiledClasses.resolve("Injection__Binding__ServiceDescriptor.class"), + compiledClasses.resolve("ApplicationBinding__ServiceDescriptor.class"), not(fileExists())); assertThat("Generated by the service inject codegen during compilation", compiledClasses.resolve("ServiceType__ServiceDescriptor.class"), diff --git a/service/tests/registry/src/main/java/io/helidon/service/test/registry/ServiceSupplier.java b/service/tests/registry/src/main/java/io/helidon/service/test/registry/ServiceSupplier.java index ee764915071..73fd8b072fb 100644 --- a/service/tests/registry/src/main/java/io/helidon/service/test/registry/ServiceSupplier.java +++ b/service/tests/registry/src/main/java/io/helidon/service/test/registry/ServiceSupplier.java @@ -21,7 +21,7 @@ import io.helidon.service.registry.Service; -@Service.Provider +@Service.PerLookup class ServiceSupplier implements Supplier { private static final AtomicInteger COUNTER = new AtomicInteger(); diff --git a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/AbstractSaw.java b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/AbstractSaw.java index cbca92c23db..b133ecae846 100644 --- a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/AbstractSaw.java +++ b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/AbstractSaw.java @@ -33,12 +33,12 @@ public abstract class AbstractSaw extends Verification implements Tool { @Service.Inject Supplier fieldInjectedPkgPrivateProviderInAbstractBase; @Service.Inject Optional fieldInjectedPkgPrivateOptionalInAbstractBase; @Service.Inject List fieldInjectedPkgPrivateListInAbstractBase; - @Service.Inject List> fieldInjectedPkgPrivateProviderListInAbstractBase; + @Service.Inject Supplier> fieldInjectedPkgPrivateProviderListInAbstractBase; Supplier setterInjectedPkgPrivateProviderInAbstractBase; Optional setterInjectedPkgPrivateOptionalInAbstractBase; List setterInjectedPkgPrivateListInAbstractBase; - List> setterInjectedPkgPrivateProviderListInAbstractBase; + Supplier> setterInjectedPkgPrivateProviderListInAbstractBase; int setterInjectedPkgPrivateProviderInAbstractBaseInjectedCount; int setterInjectedPkgPrivateOptionalInAbstractBaseInjectedCount; @@ -46,7 +46,7 @@ public abstract class AbstractSaw extends Verification implements Tool { int setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount; @Service.Inject - public void setBladeProviders(List> blades) { + public void setBladeProviders(Supplier> blades) { setterInjectedPkgPrivateProviderListInAbstractBase = blades; setterInjectedPkgPrivateProviderListInAbstractBaseInjectedCount++; } @@ -59,8 +59,16 @@ public void verifyState() { + ".fieldInjectedProtectedProviderInAbstractBase", null, false, DullBlade.class); verifyInjected(fieldInjectedProtectedListInAbstractBase, getClass() + ".fieldInjectedProtectedListInAbstractBase", null, 1, AbstractBlade.class); - verifyInjected(setterInjectedPkgPrivateProviderListInAbstractBase, getClass() - + ".setterInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + verifyInjected(setterInjectedPkgPrivateProviderListInAbstractBase, + getClass() + ".setterInjectedPkgPrivateProviderListInAbstractBase", + null, + false, + List.class); + verifyInjected(setterInjectedPkgPrivateProviderListInAbstractBase.get(), + getClass() + ".setterInjectedPkgPrivateProviderListInAbstractBase", + null, + 1, + AbstractBlade.class); // we use cardinality of the InjectionPointProvider verifyInjected(fieldInjectedPkgPrivateProviderInAbstractBase, getClass() @@ -69,8 +77,16 @@ public void verifyState() { + ".fieldInjectedPkgPrivateOptionalInAbstractBase", null, true, DullBlade.class); verifyInjected(fieldInjectedPkgPrivateListInAbstractBase, getClass() + ".fieldInjectedPkgPrivateListInAbstractBase", null, 1, DullBlade.class); - verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() - + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, + getClass() + ".fieldInjectedPkgPrivateProviderListInAbstractBase", + null, + false, + List.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase.get(), + getClass() + ".fieldInjectedPkgPrivateProviderListInAbstractBase", + null, + 1, + AbstractBlade.class); // we use cardinality of the InjectionPointProvider verifyInjected(setterInjectedPkgPrivateProviderInAbstractBase, getClass() @@ -82,8 +98,16 @@ public void verifyState() { verifyInjected(setterInjectedPkgPrivateListInAbstractBase, getClass() + ".setBladeList(List blades)", setterInjectedPkgPrivateListInAbstractBaseInjectedCount, 1, DullBlade.class); - verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, getClass() - + ".fieldInjectedPkgPrivateProviderListInAbstractBase", null, 1, Supplier.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase, + getClass() + ".fieldInjectedPkgPrivateProviderListInAbstractBase", + null, + false, + List.class); + verifyInjected(fieldInjectedPkgPrivateProviderListInAbstractBase.get(), + getClass() + ".fieldInjectedPkgPrivateProviderListInAbstractBase", + null, + 1, + AbstractBlade.class); } @Service.Inject diff --git a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/TableSaw.java b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/TableSaw.java index e28160108cf..e6aff654bde 100644 --- a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/TableSaw.java +++ b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/TableSaw.java @@ -44,12 +44,12 @@ class TableSaw extends AbstractSaw { @Service.Inject @Service.Named(CoarseBlade.NAME) - List> coarseBladeFieldInjectedPkgPrivateProviderListInSubClass; + Supplier> coarseBladeFieldInjectedPkgPrivateProviderListInSubClass; Supplier setterInjectedPkgPrivateProviderInSubClass; Optional setterInjectedPkgPrivateOptionalInSubClass; List setterInjectedPkgPrivateListInSubClass; - List> setterInjectedPkgPrivateProviderListInSubClass; + Supplier> setterInjectedPkgPrivateProviderListInSubClass; int setterInjectedPkgPrivateProviderInSubClassInjectedCount; int setterInjectedPkgPrivateOptionalInSubClassInjectedCount; int setterInjectedPkgPrivateListInSubClassInjectedCount; @@ -136,7 +136,7 @@ void setAllBladesInSubclass(@Service.Named("*") List blades) { } @Service.Inject - void setBladeProviderListInSubclass(List> blades) { + void setBladeProviderListInSubclass(Supplier> blades) { setterInjectedPkgPrivateProviderListInSubClass = blades; setterInjectedPkgPrivateProviderListInSubClassInjectedCount++; } diff --git a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/ToolBox.java b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/ToolBox.java index 7581ca9ecfc..564fb82a93d 100644 --- a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/ToolBox.java +++ b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/ToolBox.java @@ -32,7 +32,7 @@ public interface ToolBox { * * @return for testing */ - List> toolsInBox(); + Supplier> toolsInBox(); /** * Testing. diff --git a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/impl/MainToolBox.java b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/impl/MainToolBox.java index a8658e127f8..5e542ffab7d 100644 --- a/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/impl/MainToolBox.java +++ b/service/tests/toolbox/src/main/java/io/helidon/service/tests/toolbox/impl/MainToolBox.java @@ -30,8 +30,8 @@ @Service.Singleton public class MainToolBox implements ToolBox { - private final List> allTools; - private final List> allHammers; + private final Supplier> allTools; + private final Supplier> allHammers; private final Supplier bigHammer; private final Screwdriver screwdriver; public int postConstructCallCount; @@ -43,10 +43,10 @@ public class MainToolBox implements ToolBox { private Supplier setPreferredHammer; @Service.Inject - MainToolBox(List> allTools, + MainToolBox(Supplier> allTools, Screwdriver screwdriver, @Service.Named("big") Supplier bigHammer, - List> allHammers) { + Supplier> allHammers) { this.allTools = Objects.requireNonNull(allTools); this.screwdriver = Objects.requireNonNull(screwdriver); this.bigHammer = bigHammer; @@ -54,7 +54,7 @@ public class MainToolBox implements ToolBox { } @Override - public List> toolsInBox() { + public Supplier> toolsInBox() { return allTools; } @@ -63,7 +63,7 @@ public Supplier preferredHammer() { return preferredHammer; } - public List> allHammers() { + public Supplier> allHammers() { return allHammers; } diff --git a/service/tests/toolbox/src/test/java/io/helidon/service/tests/toolbox/ToolBoxTest.java b/service/tests/toolbox/src/test/java/io/helidon/service/tests/toolbox/ToolBoxTest.java index 58b5c1f62b3..b9e54cc645b 100644 --- a/service/tests/toolbox/src/test/java/io/helidon/service/tests/toolbox/ToolBoxTest.java +++ b/service/tests/toolbox/src/test/java/io/helidon/service/tests/toolbox/ToolBoxTest.java @@ -84,7 +84,7 @@ void toolbox() { assertThat(mtb.preDestroyCallCount, equalTo(0)); assertThat(mtb.setterCallCount, equalTo(1)); - List> allTools = mtb.toolsInBox(); + List allTools = mtb.toolsInBox().get(); assertThat(allTools, hasSize(7)); assertThat(mtb.screwdriver(), notNullValue()); @@ -94,22 +94,21 @@ void toolbox() { assertThat(hammer.get(), instanceOf(BigHammer.class)); List toolTypes = allTools.stream() - .map(Supplier::get) .map(Object::getClass) .map(Class::getSimpleName) .toList(); - assertThat(toolTypes, contains("SledgeHammer", // weight + 2, tbox.impl.SledgeHammer - "BigHammer", // weight + 1, tbox.impl.BigHammer - "TableSaw", // tbox.TableSaw - "AwlImpl", // tbox.impl.AwlImpl - "HandSaw", // tbox.impl.HandSaw - "Screwdriver", // tbox.impl.Screwdriver - "LittleHammer" // tbox.impl.LittleHammer, has qualifier Named + assertThat(toolTypes, contains("SledgeHammer", // unqualified, weight + 2, tbox.impl.SledgeHammer + "TableSaw", // unqualified, default weight, tbox.TableSaw + "AwlImpl", // unqualified, default weight, tbox.impl.AwlImpl + "HandSaw", // unqualified, default weight, tbox.impl.HandSaw + "Screwdriver", // unqualified, default weight, tbox.impl.Screwdriver + "BigHammer", // qualified - named, preferred, weight + 1, tbox.impl.BigHammer + "LittleHammer" // qualified - named, default weight, tbox.impl.LittleHammer )); List hammers = mtb.allHammers() + .get() .stream() - .map(Supplier::get) .map(Object::getClass) .map(Class::getSimpleName) .toList();