Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions annot/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,11 @@
<artifactId>spotbugs-annotations</artifactId>
<version>4.9.8</version>
</dependency>
<dependency>
<groupId>jakarta.annotation</groupId>
<artifactId>jakarta.annotation-api</artifactId>
<version>3.0.0</version>
</dependency>

<!-- Test dependencies -->
<dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.predic8.membrane.annot.beanregistry;

import java.lang.reflect.Method;

/**
* A BeanLifecycleManager supports the init-destroy lifecycle of beans.
*
* Beans are inited (@PostConstruct method called) when they are being defined, that is before their instance is
* published via the registry.
*
* Beans are destroyed (@PreDestroy method called) when close() is called on the registry.
*
* The registry implements this interface.
*/
public interface BeanLifecycleManager {
/**
* Tells the registry that the <code>method</code> should be called on the <code>bean</code> when the registry is
* closed.
*
* The registry should call all pre-destroy-callbacks in <b>reverse</b> order in which they were registered.
*/
void addPreDestroyCallback(Object bean, Method method);
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

import com.predic8.membrane.annot.*;

import java.lang.reflect.Method;
import java.util.*;
import java.util.function.*;

Expand Down Expand Up @@ -56,4 +57,9 @@ public interface BeanRegistry {
* @return the existing or newly created and registered bean instance
*/
<T> T registerIfAbsent(Class<T> type, Supplier<T> supplier);

/**
* Release all resources.
*/
void close();
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.slf4j.*;

import javax.annotation.concurrent.*;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;
import java.util.function.*;
Expand All @@ -36,7 +37,7 @@
* For K8S UUID and name is needed cause name is only unique within a namespace.
*
*/
public class BeanRegistryImplementation implements BeanRegistry, BeanCollector {
public class BeanRegistryImplementation implements BeanRegistry, BeanCollector, BeanLifecycleManager {

private static final Logger log = LoggerFactory.getLogger(BeanRegistryImplementation.class);

Expand All @@ -57,9 +58,15 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector {
*/
private final Object uniqueClassInitialization = new Object();

@GuardedBy("preDestroyCallbacks")
private final List<PreDestroyCallback> preDestroyCallbacks = new ArrayList<>();

record UidAction(String uid, WatchAction action) {
}

record PreDestroyCallback(Object bean, Method method) {
}

public BeanRegistryImplementation(BeanCacheObserver observer, BeanRegistryAware registryAware, Grammar grammar) {
this.observer = observer;
this.grammar = grammar;
Expand Down Expand Up @@ -198,4 +205,28 @@ public <T> T registerIfAbsent(Class<T> type, Supplier<T> supplier) {
return beanName != null ? beanName : "#" + uuid;
}

/**
* Registers a @PreDestroy callback for the given bean.
*/
public void addPreDestroyCallback(Object bean, Method method) {
synchronized (preDestroyCallbacks) {
preDestroyCallbacks.add(new PreDestroyCallback(bean, method));
}
}

public void close() {
List<PreDestroyCallback> callbacks;
synchronized (preDestroyCallbacks) {
callbacks = new ArrayList<>(preDestroyCallbacks);
preDestroyCallbacks.clear();
}
callbacks.reversed().forEach(pc -> {
try {
pc.method.invoke(pc.bean);
} catch (Exception e) {
log.error("Could not invoke preDestroy method of {}: {}", pc.bean, e.getMessage());
}
});
}
Comment thread
rrayst marked this conversation as resolved.

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package com.predic8.membrane.annot.beanregistry;

import com.predic8.membrane.annot.Grammar;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.AbstractRefreshableApplicationContext;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Optional;
import java.util.function.Supplier;

/**
* Adapter between Membrane's BeanRegistry and Spring's ApplicationContext.
*
* Methods are only implemented on a need-to-use basis.
*/
public class SpringContextAdapter implements BeanRegistry {

private final AbstractRefreshableApplicationContext ac;

public SpringContextAdapter(AbstractRefreshableApplicationContext ac) {
this.ac = ac;
}

@Override
public Object resolve(String url) {
throw new UnsupportedOperationException();
}

@Override
public List<Object> getBeans() {
return List.of(ac.getBeanDefinitionNames()).stream().map(ac::getBean).toList();
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

@Override
public Grammar getGrammar() {
throw new UnsupportedOperationException();
}

@Override
public <T> List<T> getBeans(Class<T> clazz) {
throw new UnsupportedOperationException();
}

@Override
public <T> Optional<T> getBean(Class<T> clazz) {
throw new UnsupportedOperationException();
}

@Override
public void register(String beanName, Object bean) {
throw new UnsupportedOperationException();
}

@Override
public <T> T registerIfAbsent(Class<T> type, Supplier<T> supplier) {
throw new UnsupportedOperationException();
}

@Override
public void close() {
ac.close();
}
Comment thread
rrayst marked this conversation as resolved.

public ApplicationContext getApplicationContext() {
return ac;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,12 @@
import com.networknt.schema.*;
import com.networknt.schema.Error;
import com.predic8.membrane.annot.*;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;
import com.predic8.membrane.annot.beanregistry.*;
import org.jetbrains.annotations.*;
import org.slf4j.*;
import org.springframework.util.ReflectionUtils;

import java.io.*;
import java.lang.reflect.*;
Expand All @@ -32,6 +35,7 @@
import static com.predic8.membrane.annot.yaml.MethodSetter.*;
import static com.predic8.membrane.annot.yaml.NodeValidationUtils.*;
import static java.nio.charset.StandardCharsets.*;
import static java.util.List.of;
import static java.util.UUID.*;

public class GenericYamlParser {
Expand Down Expand Up @@ -136,12 +140,12 @@ private static void validate(Grammar grammar, JsonNode input) throws YamlSchemaV
* <p>Ensures the node contains exactly one key (the kind), resolves the Java class via the
* grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.</p>
*/
public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, BeanRegistry registry) throws ParsingException {
public static <R extends BeanRegistry & BeanLifecycleManager> Object readMembraneObject(String kind, Grammar grammar, JsonNode node, R registry) throws ParsingException {
ensureSingleKey(node);
Class<?> clazz = grammar.getElement(kind);
if (clazz == null)
throw new ParsingException("Did not find java class for kind '%s'.".formatted(kind), node);
return createAndPopulateNode(new ParsingContext(kind, registry, grammar), clazz, node.get(kind));
return createAndPopulateNode(new ParsingContext<>(kind, registry, grammar), clazz, node.get(kind));
}

/**
Expand All @@ -151,7 +155,7 @@ public static Object readMembraneObject(String kind, Grammar grammar, JsonNode n
* values are produced by {@link MethodSetter#getMethodSetter(ParsingContext, Class, String)}. A top-level {@code "$ref"} injects a previously defined bean.
* All failures are wrapped in a {@link ParsingException} with location information.
*/
public static <T> T createAndPopulateNode(ParsingContext ctx, Class<T> clazz, JsonNode node) throws ParsingException {
public static <T> T createAndPopulateNode(ParsingContext<?> ctx, Class<T> clazz, JsonNode node) throws ParsingException {
try {
T configObj = clazz.getConstructor().newInstance();
if (node.isArray()) {
Expand Down Expand Up @@ -186,25 +190,23 @@ public static <T> T createAndPopulateNode(ParsingContext ctx, Class<T> clazz, Js
}
if (!required.isEmpty())
throw new ParsingException("Missing required fields: " + required.stream().map(McYamlIntrospector::getSetterName).toList(), node);
return configObj;
}
catch (NoClassDefFoundError e) {
return handlePostConstructAndPreDestroy(ctx, configObj);
} catch (NoClassDefFoundError e) {
if (e.getCause() != null) {
var missingClass = e.getCause().getMessage(); // TODO: Better use ExceptionUtil.getRootCause() but it isn't visible in annot.
var msg = "Could not create bean with class: %s\nMissing class: %s\n".formatted(clazz, missingClass);
log.error(msg);
throw new ParsingException(msg, node); // TODO: Cause we know the reason, shorten output.
}
throw new ParsingException(e, node);
}
catch (Throwable cause) {
} catch (Throwable cause) {
throw new ParsingException(cause, node);
}
}

private static List<BeanDefinition> extractComponentBeanDefinitions(JsonNode componentsNode) {
if (componentsNode == null || componentsNode.isNull())
return List.of();
return of();

if (!componentsNode.isObject())
throw new ParsingException("Expected object for 'components'.", componentsNode);
Expand Down Expand Up @@ -240,7 +242,7 @@ private static List<BeanDefinition> extractComponentBeanDefinitions(JsonNode com
* into the parent object via the matching @MCChildElement setter.
* Rejects "$ref" if the same child is already configured inline.
*/
private static <T> void applyObjectLevelRef(ParsingContext ctx, Class<T> parentClass, JsonNode parentNode, JsonNode refNode, T obj) throws ParsingException {
private static <T> void applyObjectLevelRef(ParsingContext<?> ctx, Class<T> parentClass, JsonNode parentNode, JsonNode refNode, T obj) throws ParsingException {
ensureTextual(refNode, "Expected a string after the '$ref' key.");
Object referenced = getReferenced(ctx, refNode);
String refKey = getElementName(referenced.getClass());
Expand All @@ -262,20 +264,20 @@ private static <T> void applyObjectLevelRef(ParsingContext ctx, Class<T> parentC
}
}

private static Object getReferenced(ParsingContext ctx, JsonNode refNode) {
private static Object getReferenced(ParsingContext<?> ctx, JsonNode refNode) {
try {
return ctx.registry().resolve(refNode.asText());
} catch (RuntimeException e) {
throw new ParsingException(e, refNode);
}
}

public static List<Object> parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException {
public static List<Object> parseListIncludingStartEvent(ParsingContext<?> context, JsonNode node) throws ParsingException {
ensureArray(node);
return parseListExcludingStartEvent(context, node);
}

private static @NotNull List<Object> parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException {
private static @NotNull List<Object> parseListExcludingStartEvent(ParsingContext<?> context, JsonNode node) throws ParsingException {
List<Object> res = new ArrayList<>();
for (int i = 0; i < node.size(); i++) {
res.add(parseMapToObj(context, node.get(i)));
Expand All @@ -287,15 +289,39 @@ public static List<Object> parseListIncludingStartEvent(ParsingContext context,
* Parses a single-item map node like { kind: {...} } by extracting the only key and
* delegating to {@link #parseMapToObj(ParsingContext, JsonNode, String)}.
*/
private static Object parseMapToObj(ParsingContext context, JsonNode node) throws ParsingException {
private static Object parseMapToObj(ParsingContext<?> context, JsonNode node) throws ParsingException {
ensureSingleKey(node);
String key = node.fieldNames().next();
return parseMapToObj(context, node.get(key), key);
}

private static Object parseMapToObj(ParsingContext ctx, JsonNode node, String key) throws ParsingException {
private static Object parseMapToObj(ParsingContext<?> ctx, JsonNode node, String key) throws ParsingException {
if ("$ref".equals(key))
return ctx.registry().resolve(node.asText());
return createAndPopulateNode(ctx.updateContext(key), ctx.resolveClass(key), node);
}

/**
* Calls the @PostConstruct method on the bean and returns it. If there are @PreDestroy methods, they will be
* registered within the registry.
*/
private static <T> T handlePostConstructAndPreDestroy(ParsingContext<?> ctx, T bean) {
ReflectionUtils.doWithMethods(bean.getClass(), method -> {
if (method.isAnnotationPresent(PostConstruct.class)) {
try {
method.setAccessible(true);
method.invoke(bean);
} catch (InvocationTargetException e) {
throw new RuntimeException(e.getTargetException());
} catch (IllegalAccessException | IllegalArgumentException e) {
throw new RuntimeException(e);
}
}
if (method.isAnnotationPresent(PreDestroy.class)) {
method.setAccessible(true);
ctx.registry().addPreDestroyCallback(bean, method);
}
});
return bean;
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,17 @@
package com.predic8.membrane.annot.yaml;

import com.predic8.membrane.annot.*;
import com.predic8.membrane.annot.beanregistry.BeanLifecycleManager;
import com.predic8.membrane.annot.beanregistry.BeanRegistry;
import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation;

/**
* Immutable parsing state passed down while traversing YAML.
* - context: current element scope used for local type resolution in {@link Grammar}.
* - registry: access to already materialized beans (e.g., for $ref/reference attributes).
* - grammar: resolves element names to Java classes via local/global lookups.
*/
public record ParsingContext(String context, BeanRegistry registry, Grammar grammar) {
public record ParsingContext<T extends BeanRegistry & BeanLifecycleManager>(String context, T registry, Grammar grammar) {

ParsingContext updateContext(String context) {
return new ParsingContext(context, registry, grammar);
Expand Down
Loading
Loading