Skip to content

Commit

Permalink
New main class generator, binding generator. All tests passing in ser…
Browse files Browse the repository at this point in the history
…vices
  • Loading branch information
tomas-langer committed Dec 16, 2024
1 parent f5720f3 commit 28823e1
Show file tree
Hide file tree
Showing 42 changed files with 1,617 additions and 1,059 deletions.
13 changes: 13 additions & 0 deletions service/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,19 @@ Service instance based (to obtain registry metadata in addition to the instance)
2. `Optional<ServiceInstance<Contract>>`
3. `List<ServiceInstance<Contract>>`

## Why are some dependency options not supported

We do not support dependencies of types `List<Supplier<Contract>>` and `Optional<Supplier<Contract>>`, 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<Optional<Contract>>`), and the concept of `ServicesFactory<Contract>`, 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<Supplier<Contract>>`, we would still need to resolve all the instances, so just use `List<Contract>`
for this purpose. If a supplier is need to break dependency cycle, use `Supplier<List<Contract>>`, and you will get instances
resolved only once you call `get()` on the supplier.

# Glossary

| Term | Description |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*/
Expand All @@ -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}.
Expand Down Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand All @@ -334,3 +361,5 @@ public final class ServiceCodegenTypes {
private ServiceCodegenTypes() {
}
}


Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -815,7 +816,7 @@ private List<ParamDefinition> 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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -877,7 +878,7 @@ private void fieldParam(DescribedService describedService,
List<ParamDefinition> 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,
Expand Down Expand Up @@ -1069,6 +1070,7 @@ private void injectionPointFields(ClassModel.Builder classModel,
List<ParamDefinition> params) {
// constant for injection points
for (ParamDefinition param : params) {
DependencyMetadata dependencyMetadata = dependencyMetadata(param);
classModel.addField(field -> field
.accessModifier(AccessModifier.PUBLIC)
.isStatic(true)
Expand Down Expand Up @@ -1145,13 +1147,124 @@ 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();
}));
}
}

private DependencyMetadata dependencyMetadata(ParamDefinition param) {
TypeName declared = param.declaredType();

// supplier is honored only on the first level (i.e. we do not support Optional<Supplier> or List<Supplier>)
boolean isSupplier = declared.isSupplier();
boolean isServiceInstance;
/*
REQUIRED
OPTIONAL
LIST
*/
String cardinality;

if (isSupplier) {
// Supplier<List>, Supplier<ServiceInstance>, Supplier<Contract>, Supplier<Optional<ServiceInstance>> ...
declared = declared.typeArguments().getFirst();
}

if (declared.isSupplier()) {
throw new CodegenException("Dependency is declared as a Supplier<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<Supplier<>> - this is not supported, please use "
+ "Supplier<Optional>",
param.elementInfo().originatingElementValue());
}
if (actualContract.isOptional()) {
throw new CodegenException("Dependency has Optional<Optional<>> - this is not supported",
param.elementInfo().originatingElementValue());
}
if (actualContract.isList()) {
throw new CodegenException("Dependency has Optional<List<>> - 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<Supplier<>> - this is not supported, please use "
+ "Supplier<ServiceInstance<>>",
param.elementInfo().originatingElementValue());
}
if (actualContract.isOptional()) {
throw new CodegenException("Dependency has ServiceInstance<Optional<>> - this is not supported, please use "
+ "Optional<ServiceInstance<?>>",
param.elementInfo().originatingElementValue());
}
if (actualContract.isList()) {
throw new CodegenException("Dependency has ServiceInstance<List<>> - this is not supported, please use"
+ "List<ServiceInstance<>>",
param.elementInfo().originatingElementValue());
}
} else if (declared.isList()) {
cardinality = "LIST";
TypeName actualContract = declared.typeArguments().getFirst();
if (actualContract.isSupplier()) {
throw new CodegenException("Dependency has List<Supplier<>> - this is not supported, please use "
+ "Supplier<List<>>",
param.elementInfo().originatingElementValue());
}
if (actualContract.isOptional()) {
throw new CodegenException("Dependency has List<Optional<>> - this is not supported",
param.elementInfo().originatingElementValue());
}
if (actualContract.isList()) {
throw new CodegenException("Dependency has List<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 ");
Expand Down Expand Up @@ -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) {
}
Expand Down
21 changes: 19 additions & 2 deletions service/maven-plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading

0 comments on commit 28823e1

Please sign in to comment.