Skip to content

Commit

Permalink
Support for service lifecycle (post-construct and pre-destroy annotat…
Browse files Browse the repository at this point in the history
…ions and implementation) (#9198)
  • Loading branch information
tomas-langer authored Sep 2, 2024
1 parent 2c8faac commit b3300da
Show file tree
Hide file tree
Showing 12 changed files with 320 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,8 @@ private ClassModel.Builder generate() {
dependenciesMethod(classModel, params, superType);
isAbstractMethod(classModel, superType, isAbstractClass);
instantiateMethod(classModel, serviceType, params, isAbstractClass);
postConstructMethod(typeInfo, classModel, serviceType);
preDestroyMethod(typeInfo, classModel, serviceType);
weightMethod(typeInfo, classModel, superType);

// service type is an implicit contract
Expand Down Expand Up @@ -759,6 +761,62 @@ private void instantiateMethod(ClassModel.Builder classModel,
.update(it -> createInstantiateBody(serviceType, it, params)));
}

private void postConstructMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) {
// postConstruct()
lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_POST_CONSTRUCT).ifPresent(method -> {
classModel.addMethod(postConstruct -> postConstruct.name("postConstruct")
.addAnnotation(Annotations.OVERRIDE)
.addParameter(instance -> instance.type(serviceType)
.name("instance"))
.addContentLine("instance." + method.elementName() + "();"));
});
}

private void preDestroyMethod(TypeInfo typeInfo, ClassModel.Builder classModel, TypeName serviceType) {
// preDestroy
lifecycleMethod(typeInfo, ServiceCodegenTypes.SERVICE_ANNOTATION_PRE_DESTROY).ifPresent(method -> {
classModel.addMethod(preDestroy -> preDestroy.name("preDestroy")
.addAnnotation(Annotations.OVERRIDE)
.addParameter(instance -> instance.type(serviceType)
.name("instance"))
.addContentLine("instance." + method.elementName() + "();"));
});
}

private Optional<TypedElementInfo> lifecycleMethod(TypeInfo typeInfo, TypeName annotationType) {
List<TypedElementInfo> list = typeInfo.elementInfo()
.stream()
.filter(ElementInfoPredicates.hasAnnotation(annotationType))
.toList();
if (list.isEmpty()) {
return Optional.empty();
}
if (list.size() > 1) {
throw new IllegalStateException("There is more than one method annotated with " + annotationType.fqName()
+ ", which is not allowed on type " + typeInfo.typeName().fqName());
}
TypedElementInfo method = list.getFirst();
if (method.accessModifier() == AccessModifier.PRIVATE) {
throw new CodegenException("Method annotated with " + annotationType.fqName()
+ ", is private, which is not supported: " + typeInfo.typeName().fqName()
+ "#" + method.elementName(),
method.originatingElement().orElseGet(method::elementName));
}
if (!method.parameterArguments().isEmpty()) {
throw new CodegenException("Method annotated with " + annotationType.fqName()
+ ", has parameters, which is not supported: " + typeInfo.typeName().fqName()
+ "#" + method.elementName(),
method.originatingElement().orElseGet(method::elementName));
}
if (!method.typeName().equals(TypeNames.PRIMITIVE_VOID)) {
throw new CodegenException("Method annotated with " + annotationType.fqName()
+ ", is not void, which is not supported: " + typeInfo.typeName().fqName()
+ "#" + method.elementName(),
method.originatingElement().orElseGet(method::elementName));
}
return Optional.of(method);
}

private void createInstantiateBody(TypeName serviceType,
Method.Builder method,
List<ParamDefinition> params) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
package io.helidon.service.codegen;

/**
* Code generation extension for Helidon Service REgistry.
* Code generation extension for Helidon Service Registry.
*/
interface RegistryCodegenExtension {
/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ public final class ServiceCodegenTypes {
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.Provider}.
*/
public static final TypeName SERVICE_ANNOTATION_PROVIDER = TypeName.create("io.helidon.service.registry.Service.Provider");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.PreDestroy}.
*/
public static final TypeName SERVICE_ANNOTATION_PRE_DESTROY =
TypeName.create("io.helidon.service.registry.Service.PreDestroy");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.PostConstruct}.
*/
public static final TypeName SERVICE_ANNOTATION_POST_CONSTRUCT =
TypeName.create("io.helidon.service.registry.Service.PostConstruct");
/**
* {@link io.helidon.common.types.TypeName} for {@code io.helidon.service.registry.Service.Contract}.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.locks.ReentrantLock;
import java.util.function.Supplier;
import java.util.stream.Collectors;
Expand All @@ -46,11 +45,14 @@ class CoreServiceRegistry implements ServiceRegistry {
Comparator.comparing(ServiceProvider::weight).reversed()
.thenComparing(ServiceProvider::descriptorType);

private final Map<TypeName, Set<ServiceProvider>> providersByContract;
private final Map<TypeName, List<ServiceProvider>> providersByContract;
private final Map<ServiceInfo, ServiceProvider> providersByService;
private final List<ServiceProvider> allProviders;

@SuppressWarnings({"rawtypes", "unchecked"})
CoreServiceRegistry(ServiceRegistryConfig config, ServiceDiscovery serviceDiscovery) {
Map<TypeName, Set<ServiceProvider>> providers = new HashMap<>();
List<ServiceProvider> allProviders = new ArrayList<>();
Map<TypeName, List<ServiceProvider>> providers = new HashMap<>();
Map<ServiceInfo, ServiceProvider> providersByService = new IdentityHashMap<>();

// each just once
Expand All @@ -65,18 +67,22 @@ class CoreServiceRegistry implements ServiceRegistry {
config.serviceInstances().forEach((descriptor, instance) -> {
if (processedDescriptorTypes.add(descriptor.descriptorType())) {
BoundInstance bi = new BoundInstance(descriptor, Optional.of(instance));
allProviders.add(bi);
providersByService.put(descriptor, bi);
addContracts(providers, descriptor.contracts(), bi);
}
});

// add configured descriptors
for (Descriptor<?> descriptor : config.serviceDescriptors()) {
if (processedDescriptorTypes.add(descriptor.descriptorType())) {
BoundDescriptor bd = new BoundDescriptor(this, descriptor, LazyValue.create(() -> instance(descriptor)));
providersByService.put(descriptor, bd);
addContracts(providers, descriptor.contracts(), bd);
}
for (Descriptor descriptor : config.serviceDescriptors()) {
BoundDescriptor bd = new BoundDescriptor(this, descriptor, LazyValue.create(() -> {
var instance = instance(descriptor);
instance.ifPresent(descriptor::postConstruct);
return instance;
}));
allProviders.add(bd);
providersByService.put(descriptor, bd);
addContracts(providers, descriptor.contracts(), bd);
}

boolean logUnsupported = LOGGER.isLoggable(Level.TRACE);
Expand All @@ -95,12 +101,20 @@ class CoreServiceRegistry implements ServiceRegistry {
DiscoveredDescriptor dd = new DiscoveredDescriptor(this,
descriptorMeta,
instanceSupplier(descriptorMeta));
allProviders.add(dd);
providersByService.put(descriptorMeta.descriptor(), dd);
addContracts(providers, descriptorMeta.contracts(), dd);
}
}
// sort all the providers
providers.values()
.forEach(it -> it.sort(PROVIDER_COMPARATOR));
allProviders.sort(PROVIDER_COMPARATOR);
allProviders.reversed();

this.providersByContract = Map.copyOf(providers);
this.providersByService = providersByService;
this.allProviders = List.copyOf(allProviders);
}

@Override
Expand Down Expand Up @@ -157,39 +171,57 @@ public <T> Optional<T> get(ServiceInfo serviceInfo) {
@Override
public List<ServiceInfo> allServices(TypeName contract) {
return Optional.ofNullable(providersByContract.get(contract))
.orElseGet(Set::of)
.orElseGet(List::of)
.stream()
.map(ServiceProvider::descriptor)
.collect(Collectors.toUnmodifiableList());

}

private static void addContracts(Map<TypeName, Set<ServiceProvider>> providers,
void shutdown() {
allProviders.forEach(ServiceProvider::close);
}

private static void addContracts(Map<TypeName, List<ServiceProvider>> providers,
Set<TypeName> contracts,
ServiceProvider provider) {
for (TypeName contract : contracts) {
providers.computeIfAbsent(contract, it -> new TreeSet<>(PROVIDER_COMPARATOR))
providers.computeIfAbsent(contract, it -> new ArrayList<>())
.add(provider);
}
}

private Supplier<Optional<Object>> instanceSupplier(DescriptorHandler descriptorMeta) {
LazyValue<Optional<Object>> serviceInstance = LazyValue.create(() -> instance(descriptorMeta.descriptor()));
@SuppressWarnings({"rawtypes", "unchecked"})
private ServiceAndInstance instanceSupplier(DescriptorHandler descriptorMeta) {
LazyValue<Optional<Object>> serviceInstance = LazyValue.create(() -> {
Descriptor descriptor = descriptorMeta.descriptor();
var instance = instance(descriptor);
instance.ifPresent(descriptor::postConstruct);
return instance;
});

if (descriptorMeta.contracts().contains(TypeNames.SUPPLIER)) {
return () -> instanceFromSupplier(descriptorMeta.descriptor(), serviceInstance);
return new ServiceAndInstance(serviceInstance,
() -> instanceFromSupplier(descriptorMeta.descriptor(), serviceInstance));
} else {
return serviceInstance;
return new ServiceAndInstance(serviceInstance);
}
}

private record ServiceAndInstance(LazyValue<Optional<Object>> serviceSupplier,
Supplier<Optional<Object>> instanceSupplier) {
ServiceAndInstance(LazyValue<Optional<Object>> serviceSupplier) {
this(serviceSupplier, serviceSupplier);
}
}

private List<ServiceProvider> allProviders(TypeName contract) {
Set<ServiceProvider> serviceProviders = providersByContract.get(contract);
List<ServiceProvider> serviceProviders = providersByContract.get(contract);
if (serviceProviders == null) {
return List.of();
}

return new ArrayList<>(serviceProviders);
return List.copyOf(serviceProviders);
}

private Optional<Object> instanceFromSupplier(Descriptor<?> descriptor, LazyValue<Optional<Object>> serviceInstanceSupplier) {
Expand Down Expand Up @@ -264,6 +296,8 @@ private interface ServiceProvider {
double weight();

TypeName descriptorType();

void close();
}

private record BoundInstance(Descriptor<?> descriptor, Optional<Object> instance) implements ServiceProvider {
Expand All @@ -276,6 +310,11 @@ public double weight() {
public TypeName descriptorType() {
return descriptor.descriptorType();
}

@Override
public void close() {
// as the instance was provided from outside, we do not call pre-destroy
}
}

private record BoundDescriptor(CoreServiceRegistry registry,
Expand Down Expand Up @@ -316,17 +355,25 @@ public double weight() {
public TypeName descriptorType() {
return descriptor.descriptorType();
}

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public void close() {
if (lazyInstance.isLoaded()) {
lazyInstance.get().ifPresent(it -> ((Descriptor) descriptor).preDestroy(it));
}
}
}

private record DiscoveredDescriptor(CoreServiceRegistry registry,
DescriptorHandler metadata,
Supplier<Optional<Object>> instanceSupplier,
ServiceAndInstance instances,
ReentrantLock lock) implements ServiceProvider {

private DiscoveredDescriptor(CoreServiceRegistry registry,
DescriptorHandler metadata,
Supplier<Optional<Object>> instanceSupplier) {
this(registry, metadata, instanceSupplier, new ReentrantLock());
ServiceAndInstance instances) {
this(registry, metadata, instances, new ReentrantLock());
}

@Override
Expand All @@ -336,6 +383,7 @@ public Descriptor<?> descriptor() {

@Override
public Optional<Object> instance() {
var instanceSupplier = instances.instanceSupplier();
if ((instanceSupplier instanceof LazyValue<?> lv) && lv.isLoaded()) {
return instanceSupplier.get();
}
Expand All @@ -361,5 +409,14 @@ public double weight() {
public TypeName descriptorType() {
return metadata.descriptorType();
}

@SuppressWarnings({"unchecked", "rawtypes"})
@Override
public void close() {
var serviceSupplier = instances.serviceSupplier();
if (serviceSupplier.isLoaded()) {
serviceSupplier.get().ifPresent(it -> ((Descriptor) metadata.descriptor()).preDestroy(it));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public void shutdown() {
Lock lock = lifecycleLock.writeLock();
try {
lock.lock();
registry.shutdown();
registry = null;
} finally {
lock.unlock();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,22 @@ default Object instantiate(DependencyContext ctx) {
throw new IllegalStateException("Cannot instantiate type " + serviceType().fqName() + ", as it is either abstract,"
+ " or an interface.");
}

/**
* Invoke {@link io.helidon.service.registry.Service.PostConstruct} annotated method(s).
*
* @param instance instance to use
*/
default void postConstruct(T instance) {
}

/**
* Invoke {@link io.helidon.service.registry.Service.PreDestroy} annotated method(s).
*
* @param instance instance to use
*/
default void preDestroy(T instance) {
}
}

private record TypeAndName(String type, String name) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ private Service() {
TypeName TYPE = TypeName.create(Provider.class);
}

/**
* A method annotated with this annotation will be invoked after the constructor is finished
* and all dependencies are satisfied.
* <p>
* The method must not have any parameters and must be accessible (not {@code private}).
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface PostConstruct {
}

/**
* A method annotated with this annotation will be invoked when the service registry shuts down.
* <p>
* Behavior of this annotation may differ based on the service registry implementation used. For example
* when using Helidon Service Inject (to be introduced), a pre-destroy method would be used when the scope
* a service is created in is finished. The core service registry behaves similar like a singleton scope - instance
* is created once, and pre-destroy is called when the registry is shut down.
* This also implies that instances that are NOT created within a scope cannot have their pre-destroy methods
* invoked, as we do not control their lifecycle.
* <p>
* The method must not have any parameters and must be accessible (not {@code private}).
*/
@Documented
@Retention(RetentionPolicy.CLASS)
@Target(ElementType.METHOD)
public @interface PreDestroy {
}

/**
* The {@code Contract} annotation is used to relay significance to the type that it annotates. While remaining optional in
* its use, it is typically placed on an interface definition to signify that the given type can be used for lookup in the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ void testTypes() {
}

checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_PROVIDER", Service.Provider.class);
checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_PRE_DESTROY", Service.PreDestroy.class);
checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_POST_CONSTRUCT", Service.PostConstruct.class);
checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_CONTRACT", Service.Contract.class);
checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_EXTERNAL_CONTRACTS", Service.ExternalContracts.class);
checkField(toCheck, checked, fields, "SERVICE_ANNOTATION_DESCRIPTOR", Service.Descriptor.class);
Expand Down
1 change: 0 additions & 1 deletion service/tests/registry/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@
<groupId>io.helidon.service</groupId>
<artifactId>helidon-service-registry</artifactId>
</dependency>

<dependency>
<groupId>io.helidon.config</groupId>
<artifactId>helidon-config</artifactId>
Expand Down
Loading

0 comments on commit b3300da

Please sign in to comment.