From 2ba7e1347e366d61f0b6c2e0059296dd667d698f Mon Sep 17 00:00:00 2001 From: Tobias Polley Date: Mon, 5 Jan 2026 17:00:24 +0100 Subject: [PATCH 1/4] Support @PostConstruct and @PreDestroy --- annot/pom.xml | 5 + .../annot/beanregistry/BeanRegistry.java | 6 + .../BeanRegistryImplementation.java | 23 ++ .../beanregistry/SpringContextAdapter.java | 68 ++++++ .../annot/yaml/GenericYamlParser.java | 28 ++- .../membrane/annot/yaml/ParsingContext.java | 3 +- .../predic8/membrane/annot/ParsingTest.java | 207 ++++++++++++++++++ .../membrane/annot/YAMLParsingTest.java | 69 ++++++ .../membrane/annot/util/CompilerHelper.java | 17 +- .../core/config/spring/k8s/EnvelopeTest.java | 3 +- 10 files changed, 417 insertions(+), 12 deletions(-) create mode 100644 annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java diff --git a/annot/pom.xml b/annot/pom.xml index 2bc5c5082b..d3b473853c 100644 --- a/annot/pom.xml +++ b/annot/pom.xml @@ -61,6 +61,11 @@ spotbugs-annotations 4.9.8 + + jakarta.annotation + jakarta.annotation-api + 3.0.0 + diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java index d562b2ea33..a417d50be8 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistry.java @@ -15,6 +15,7 @@ import com.predic8.membrane.annot.*; +import java.lang.reflect.Method; import java.util.*; import java.util.function.*; @@ -56,4 +57,9 @@ public interface BeanRegistry { * @return the existing or newly created and registered bean instance */ T registerIfAbsent(Class type, Supplier supplier); + + /** + * Release all resources. + */ + void close(); } diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java index 8ea53231a7..d1e40fdb2f 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java @@ -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.*; @@ -57,9 +58,14 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector { */ private final Object uniqueClassInitialization = new Object(); + private final LinkedHashSet preDestroyCallbacks = new LinkedHashSet<>(); + 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; @@ -198,4 +204,21 @@ public T registerIfAbsent(Class type, Supplier supplier) { return beanName != null ? beanName : "#" + uuid; } + /** + * Registers a @PreDestroy callback for the given bean. + */ + public void addPreDestroyCallback(Object bean, Method method) { + preDestroyCallbacks.add(new PreDestroyCallback(bean, method)); + } + + public void close() { + preDestroyCallbacks.reversed().forEach(pc -> { + try { + pc.method.invoke(pc.bean); + } catch (Exception e) { + log.error("Could not invoke preDestroy method of {}: {}", pc.bean, e.getMessage()); + } + }); + } + } diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java new file mode 100644 index 0000000000..7e7eec355f --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java @@ -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 ApplicationContext ac; + + public SpringContextAdapter(ApplicationContext ac) { + this.ac = ac; + } + + @Override + public Object resolve(String url) { + throw new UnsupportedOperationException(); + } + + @Override + public List getBeans() { + return List.of(ac.getBeanDefinitionNames()).stream().map(ac::getBean).toList(); + } + + @Override + public Grammar getGrammar() { + throw new UnsupportedOperationException(); + } + + @Override + public List getBeans(Class clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public Optional getBean(Class clazz) { + throw new UnsupportedOperationException(); + } + + @Override + public void register(String beanName, Object bean) { + throw new UnsupportedOperationException(); + } + + @Override + public T registerIfAbsent(Class type, Supplier supplier) { + throw new UnsupportedOperationException(); + } + + @Override + public void close() { + ((AbstractRefreshableApplicationContext)ac).close(); + } + + public ApplicationContext getApplicationContext() { + return ac; + } +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java index d586e4d2b4..f43428bee3 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java @@ -22,8 +22,12 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.annot.beanregistry.BeanDefinition; import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation; +import jakarta.annotation.PostConstruct; +import jakarta.annotation.PreDestroy; import org.jetbrains.annotations.*; import org.slf4j.*; +import org.springframework.util.ReflectionUtils; import java.io.*; import java.lang.reflect.*; @@ -138,7 +142,7 @@ private static void validate(Grammar grammar, JsonNode input) throws YamlSchemaV *

Ensures the node contains exactly one key (the kind), resolves the Java class via the * grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.

*/ - public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, BeanRegistry registry) throws ParsingException { + public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, BeanRegistryImplementation registry) throws ParsingException { ensureSingleKey(node); Class clazz = grammar.getElement(kind); if (clazz == null) @@ -188,7 +192,7 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js } if (!required.isEmpty()) throw new ParsingException("Missing required fields: " + required.stream().map(McYamlIntrospector::getSetterName).toList(), node); - return configObj; + return handlePostConstructAndPreDestroy(ctx, configObj); } catch (Throwable cause) { throw new ParsingException(cause, node); } @@ -290,4 +294,24 @@ private static Object parseMapToObj(ParsingContext ctx, JsonNode node, String ke 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 handlePostConstructAndPreDestroy(ParsingContext ctx, T bean) { + ReflectionUtils.doWithMethods(bean.getClass(), method -> { + if (method.isAnnotationPresent(PostConstruct.class)) { + try { + method.invoke(bean); + } catch (InvocationTargetException e) { + throw new RuntimeException(e); + } + } + if (method.isAnnotationPresent(PreDestroy.class)) { + ctx.registry().addPreDestroyCallback(bean, method); + } + }); + return bean; + } } \ No newline at end of file diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java index 9dd2636173..a6d5b4e772 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java @@ -16,6 +16,7 @@ import com.predic8.membrane.annot.*; import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation; /** * Immutable parsing state passed down while traversing YAML. @@ -23,7 +24,7 @@ * - 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(String context, BeanRegistryImplementation registry, Grammar grammar) { ParsingContext updateContext(String context) { return new ParsingContext(context, registry, grammar); diff --git a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java index 77325e596b..ba3e92d850 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/ParsingTest.java @@ -14,11 +14,19 @@ package com.predic8.membrane.annot; +import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import com.predic8.membrane.annot.util.CompilerHelper; +import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; + +import java.util.List; import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO; import static com.predic8.membrane.annot.util.CompilerHelper.*; +import static com.predic8.membrane.annot.util.StructureAssertionUtil.*; +import static com.predic8.membrane.annot.util.StructureAssertionUtil.clazz; public class ParsingTest { @@ -90,4 +98,203 @@ public class Child2 extends AbstractDemoChildElement { """)); } + + @Test + public void afterPropertiesSetAndDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import org.springframework.beans.factory.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement implements InitializingBean, DisposableBean { + int value = 0; + public int getValue() { return value; } + + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + """)); + + assertStructure( + br, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + @Test + public void afterPropertiesSetAndDestroyAndPostConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import org.springframework.beans.factory.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement implements InitializingBean, DisposableBean { + int value = 0; + public int getValue() { return value; } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + + + """)); + + assertStructure( + br, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + @Test + public void postConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root") + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement { + int value = 0; + public int getValue() { + return value; + } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseXML(result, wrapSpring(""" + + + + + + """)); + + assertStructure( + br, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + ((ConfigurableApplicationContext) ((SpringContextAdapter)br).getApplicationContext()).close(); + + assertStructure( + beans, + clazz("CommonAnnotationBeanPostProcessor"), + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + } diff --git a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java index ac3ae8befe..52e9b5cfc6 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java +++ b/annot/src/test/java/com/predic8/membrane/annot/YAMLParsingTest.java @@ -14,12 +14,16 @@ package com.predic8.membrane.annot; +import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import com.predic8.membrane.annot.util.CompilerHelper; import com.predic8.membrane.annot.yaml.YamlSchemaValidationException; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; +import org.springframework.context.ConfigurableApplicationContext; import java.lang.reflect.InvocationTargetException; +import java.util.List; import static com.predic8.membrane.annot.SpringConfigurationXSDGeneratingAnnotationProcessorTest.MC_MAIN_DEMO; import static com.predic8.membrane.annot.util.CompilerHelper.*; @@ -601,4 +605,69 @@ private Throwable getCause(Throwable e) { return e; } + @Test + public void postConstructAndPreDestroy() { + var sources = splitSources(MC_MAIN_DEMO + """ + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import java.util.List; + @MCElement(name="root", topLevel=true, component=false) + public class DemoElement { + ChildElement child; + + public ChildElement getChild() { return child; } + @MCChildElement + public void setChild(ChildElement child) { this.child = child; } + } + --- + package com.predic8.membrane.demo; + import com.predic8.membrane.annot.*; + import jakarta.annotation.*; + import static org.junit.jupiter.api.Assertions.assertEquals; + @MCElement(name="child") + public class ChildElement { + int value = 0; + public int getValue() { + return value; + } + + @PostConstruct + public void afterPropertiesSet() throws Exception { + assertEquals(0, value); + value = 1; + } + @PreDestroy + public void destroy() throws Exception { + assertEquals(1, value); + value = 2; + } + } + """); + var result = CompilerHelper.compile(sources, false); + assertCompilerResult(true, result); + + BeanRegistry br = parseYAML(result, """ + root: + child: {} + """); + + assertStructure( + br, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(1)))))); + + List beans = br.getBeans(); // 'list of beans' must be retrieved before closing the context + + br.close(); + + assertStructure( + beans, + clazz("DemoElement", + property("child", clazz("ChildElement", + property("value", value(2)))))); + + } + + } diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java index 224b8ffb2f..e91567edb8 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java +++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java @@ -13,15 +13,21 @@ limitations under the License. */ package com.predic8.membrane.annot.util; +import com.predic8.membrane.annot.Grammar; import com.predic8.membrane.annot.beanregistry.BeanRegistry; +import com.predic8.membrane.annot.beanregistry.SpringContextAdapter; import org.hamcrest.*; import org.hamcrest.collection.*; import org.jetbrains.annotations.*; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.CommonAnnotationBeanPostProcessor; +import org.springframework.context.support.AbstractRefreshableApplicationContext; import javax.tools.*; import java.io.*; import java.lang.reflect.*; import java.util.*; +import java.util.function.Supplier; import java.util.regex.*; import java.util.regex.Matcher; import java.util.stream.*; @@ -82,17 +88,12 @@ public static BeanRegistry parseYAML(CompilerResult cr, String yamlConfig) { }); } - public static void parseXML(CompilerResult cr, String xmlSpringConfig) { + public static BeanRegistry parseXML(CompilerResult cr, String xmlSpringConfig) { CompositeClassLoader cl = xmlClassLoader(cr, xmlSpringConfig); - withContextClassLoader(cl, () -> { + return withContextClassLoader(cl, () -> { Class ctx = cl.loadClass(APPLICATION_CONTEXT_CLASSNAME); Object context = ctx.getConstructor(String.class).newInstance("demo.xml"); - try { - // Context successfully created - validation passed - } finally { - ctx.getMethod("close").invoke(context); - } - return null; + return new SpringContextAdapter((ApplicationContext) context); }); } diff --git a/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java b/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java index 2f6891835e..1b2c9d055a 100644 --- a/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java +++ b/core/src/test/java/com/predic8/membrane/core/config/spring/k8s/EnvelopeTest.java @@ -14,6 +14,7 @@ package com.predic8.membrane.core.config.spring.k8s; +import com.predic8.membrane.annot.beanregistry.BeanRegistryImplementation; import com.predic8.membrane.annot.yaml.GenericYamlParser; import com.predic8.membrane.core.config.spring.GrammarAutoGenerated; import com.predic8.membrane.core.interceptor.Interceptor; @@ -209,7 +210,7 @@ void missingKindDefaultsToApi() { assertEquals(1001, ((APIProxy) e.getSpec()).getPort()); } - private static List parseEnvelopes(String yaml, BeanRegistry registry) { + private static List parseEnvelopes(String yaml, BeanRegistryImplementation registry) { GrammarAutoGenerated generator = new GrammarAutoGenerated(); try { return new GenericYamlParser(generator, yaml) From 5371d8d2ef8e9879e0dec8057fdfda2ce1143cc7 Mon Sep 17 00:00:00 2001 From: Tobias Polley Date: Tue, 6 Jan 2026 09:59:19 +0100 Subject: [PATCH 2/4] fix: compilation error --- .../beanregistry/BeanLifecycleManager.java | 7 +++++ .../BeanRegistryImplementation.java | 2 +- .../annot/yaml/GenericYamlParser.java | 14 ++++----- .../membrane/annot/yaml/ParsingContext.java | 3 +- .../kubernetes/GenericYamlParserTest.java | 31 ++++++++++++------- 5 files changed, 36 insertions(+), 21 deletions(-) create mode 100644 annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java new file mode 100644 index 0000000000..06195e21f8 --- /dev/null +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java @@ -0,0 +1,7 @@ +package com.predic8.membrane.annot.beanregistry; + +import java.lang.reflect.Method; + +public interface BeanLifecycleManager { + void addPreDestroyCallback(Object bean, Method method); +} diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java index d1e40fdb2f..7b6df0621a 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java @@ -37,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); diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java index f43428bee3..ef8ad59b23 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java @@ -236,7 +236,7 @@ private static List extractComponentBeanDefinitions(JsonNode com * into the parent object via the matching @MCChildElement setter. * Rejects "$ref" if the same child is already configured inline. */ - private static void applyObjectLevelRef(ParsingContext ctx, Class parentClass, JsonNode parentNode, JsonNode refNode, T obj) throws ParsingException { + private static void applyObjectLevelRef(ParsingContext ctx, Class 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()); @@ -258,7 +258,7 @@ private static void applyObjectLevelRef(ParsingContext ctx, Class 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) { @@ -266,12 +266,12 @@ private static Object getReferenced(ParsingContext ctx, JsonNode refNode) { } } - public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + public static List parseListIncludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { ensureArray(node); return parseListExcludingStartEvent(context, node); } - private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { + private static @NotNull List parseListExcludingStartEvent(ParsingContext context, JsonNode node) throws ParsingException { List res = new ArrayList<>(); for (int i = 0; i < node.size(); i++) { res.add(parseMapToObj(context, node.get(i))); @@ -283,13 +283,13 @@ public static List 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); @@ -299,7 +299,7 @@ private static Object parseMapToObj(ParsingContext ctx, JsonNode node, String ke * 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 handlePostConstructAndPreDestroy(ParsingContext ctx, T bean) { + private static T handlePostConstructAndPreDestroy(ParsingContext ctx, T bean) { ReflectionUtils.doWithMethods(bean.getClass(), method -> { if (method.isAnnotationPresent(PostConstruct.class)) { try { diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java index a6d5b4e772..a3a80e2f88 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/ParsingContext.java @@ -15,6 +15,7 @@ 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; @@ -24,7 +25,7 @@ * - 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, BeanRegistryImplementation registry, Grammar grammar) { +public record ParsingContext(String context, T registry, Grammar grammar) { ParsingContext updateContext(String context) { return new ParsingContext(context, registry, grammar); diff --git a/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java b/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java index 26e03653fc..f18651f893 100644 --- a/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java +++ b/core/src/test/java/com/predic8/membrane/core/kubernetes/GenericYamlParserTest.java @@ -38,6 +38,7 @@ import org.junit.jupiter.params.provider.*; import java.io.*; +import java.lang.reflect.Method; import java.util.*; import java.util.function.*; import java.util.stream.*; @@ -71,15 +72,15 @@ Stream successCases() { setHost("localhost"); setPort(3000); }}; - BeanRegistry memReg = new TestRegistry().with("mem", mem); + TestBeanRegistry memReg = new TestBeanRegistry().with("mem", mem); Target target = new Target() {{ setUrl("https://ref.example"); }}; - BeanRegistry targetReg = new TestRegistry().with("target", target); + TestBeanRegistry targetReg = new TestBeanRegistry().with("target", target); ResponseInterceptor responseInterceptor = new ResponseInterceptor(); - BeanRegistry responseReg = new TestRegistry().with("response", responseInterceptor); + TestBeanRegistry responseReg = new TestBeanRegistry().with("response", responseInterceptor); return Stream.of( ok( @@ -292,7 +293,7 @@ Stream successCases() { } Stream errorCases() { - BeanRegistry empty = new TestRegistry(); + TestBeanRegistry empty = new TestBeanRegistry(); return Stream.of( err( "invalid_ref", @@ -311,28 +312,28 @@ Stream errorCases() { ); } - record Case(String testName, String yaml, BeanRegistry reg, java.util.function.Consumer check) { + record Case(String testName, String yaml, TestBeanRegistry reg, java.util.function.Consumer check) { @Override public @NotNull String toString() { return testName; } } - record ErrCase(String testName, String yaml, BeanRegistry reg, Class expected) { + record ErrCase(String testName, String yaml, TestBeanRegistry reg, Class expected) { @Override public @NotNull String toString() { return testName; } } private static Case ok(String testName, String yaml, java.util.function.Consumer check) { return new Case(testName, yaml, null, check); } - private static Case ok(String testName, String yaml, BeanRegistry reg, java.util.function.Consumer check) { + private static Case ok(String testName, String yaml, TestBeanRegistry reg, java.util.function.Consumer check) { return new Case(testName, yaml, reg, check); } - private static ErrCase err(String testName, String yaml, BeanRegistry reg, Class expected) { + private static ErrCase err(String testName, String yaml, TestBeanRegistry reg, Class expected) { return new ErrCase(testName, yaml, reg, expected); } - static class TestRegistry implements BeanRegistry, BeanCollector { + static class TestBeanRegistry implements BeanRegistry, BeanCollector, BeanLifecycleManager { private final Map refs = new HashMap<>(); - TestRegistry with(String key, Object v) { refs.put(key, v); return this; } + TestBeanRegistry with(String key, Object v) { refs.put(key, v); return this; } @Override public Object resolve(String ref) { return refs.get(ref); } @Override @@ -373,10 +374,16 @@ public void register(String beanName, Object object) {} public T registerIfAbsent(Class type, Supplier supplier) { return type.cast(null); } + + @Override + public void close() {} + + @Override + public void addPreDestroyCallback(Object bean, Method method) {} } - private static APIProxy parse(String yaml, BeanRegistry reg) { - return GenericYamlParser.createAndPopulateNode(new ParsingContext("api", reg, K8S_HELPER), APIProxy.class, parse(yaml)); + private static APIProxy parse(String yaml, TestBeanRegistry reg) { + return GenericYamlParser.createAndPopulateNode(new ParsingContext("api", reg, K8S_HELPER), APIProxy.class, parse(yaml)); } public static JsonNode parse(String yaml) { From e7167ab57e2e72641a277e59d79474358023821a Mon Sep 17 00:00:00 2001 From: Tobias Polley Date: Tue, 6 Jan 2026 10:21:16 +0100 Subject: [PATCH 3/4] doc --- .../annot/beanregistry/BeanLifecycleManager.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java index 06195e21f8..ebc39e8d2d 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanLifecycleManager.java @@ -2,6 +2,22 @@ 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 method should be called on the bean when the registry is + * closed. + * + * The registry should call all pre-destroy-callbacks in reverse order in which they were registered. + */ void addPreDestroyCallback(Object bean, Method method); } From e6029f755eb80d09a0e0a7c311fff58c518ffb16 Mon Sep 17 00:00:00 2001 From: Tobias Polley Date: Tue, 6 Jan 2026 13:34:19 +0100 Subject: [PATCH 4/4] refactor --- .../beanregistry/BeanRegistryImplementation.java | 14 +++++++++++--- .../annot/beanregistry/SpringContextAdapter.java | 6 +++--- .../membrane/annot/yaml/GenericYamlParser.java | 13 +++++++++---- .../membrane/annot/util/CompilerHelper.java | 2 +- 4 files changed, 24 insertions(+), 11 deletions(-) diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java index 7b6df0621a..5aab389bdf 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/BeanRegistryImplementation.java @@ -58,7 +58,8 @@ public class BeanRegistryImplementation implements BeanRegistry, BeanCollector, */ private final Object uniqueClassInitialization = new Object(); - private final LinkedHashSet preDestroyCallbacks = new LinkedHashSet<>(); + @GuardedBy("preDestroyCallbacks") + private final List preDestroyCallbacks = new ArrayList<>(); record UidAction(String uid, WatchAction action) { } @@ -208,11 +209,18 @@ public T registerIfAbsent(Class type, Supplier supplier) { * Registers a @PreDestroy callback for the given bean. */ public void addPreDestroyCallback(Object bean, Method method) { - preDestroyCallbacks.add(new PreDestroyCallback(bean, method)); + synchronized (preDestroyCallbacks) { + preDestroyCallbacks.add(new PreDestroyCallback(bean, method)); + } } public void close() { - preDestroyCallbacks.reversed().forEach(pc -> { + List callbacks; + synchronized (preDestroyCallbacks) { + callbacks = new ArrayList<>(preDestroyCallbacks); + preDestroyCallbacks.clear(); + } + callbacks.reversed().forEach(pc -> { try { pc.method.invoke(pc.bean); } catch (Exception e) { diff --git a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java index 7e7eec355f..98cb0d2f95 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java +++ b/annot/src/main/java/com/predic8/membrane/annot/beanregistry/SpringContextAdapter.java @@ -16,9 +16,9 @@ */ public class SpringContextAdapter implements BeanRegistry { - private final ApplicationContext ac; + private final AbstractRefreshableApplicationContext ac; - public SpringContextAdapter(ApplicationContext ac) { + public SpringContextAdapter(AbstractRefreshableApplicationContext ac) { this.ac = ac; } @@ -59,7 +59,7 @@ public T registerIfAbsent(Class type, Supplier supplier) { @Override public void close() { - ((AbstractRefreshableApplicationContext)ac).close(); + ac.close(); } public ApplicationContext getApplicationContext() { diff --git a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java index 7fbb49f951..d1c59f87d9 100644 --- a/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java +++ b/annot/src/main/java/com/predic8/membrane/annot/yaml/GenericYamlParser.java @@ -35,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 { @@ -139,12 +140,12 @@ private static void validate(Grammar grammar, JsonNode input) throws YamlSchemaV *

Ensures the node contains exactly one key (the kind), resolves the Java class via the * grammar and delegates to {@link #createAndPopulateNode(ParsingContext, Class, JsonNode)}.

*/ - public static Object readMembraneObject(String kind, Grammar grammar, JsonNode node, BeanRegistryImplementation registry) throws ParsingException { + public static 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)); } /** @@ -154,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 createAndPopulateNode(ParsingContext ctx, Class clazz, JsonNode node) throws ParsingException { + public static T createAndPopulateNode(ParsingContext ctx, Class clazz, JsonNode node) throws ParsingException { try { T configObj = clazz.getConstructor().newInstance(); if (node.isArray()) { @@ -205,7 +206,7 @@ public static T createAndPopulateNode(ParsingContext ctx, Class clazz, Js private static List extractComponentBeanDefinitions(JsonNode componentsNode) { if (componentsNode == null || componentsNode.isNull()) - return List.of(); + return of(); if (!componentsNode.isObject()) throw new ParsingException("Expected object for 'components'.", componentsNode); @@ -308,12 +309,16 @@ private static T handlePostConstructAndPreDestroy(ParsingContext ctx, T b 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); } }); diff --git a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java index e91567edb8..6c4b9f0bc5 100644 --- a/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java +++ b/annot/src/test/java/com/predic8/membrane/annot/util/CompilerHelper.java @@ -93,7 +93,7 @@ public static BeanRegistry parseXML(CompilerResult cr, String xmlSpringConfig) { return withContextClassLoader(cl, () -> { Class ctx = cl.loadClass(APPLICATION_CONTEXT_CLASSNAME); Object context = ctx.getConstructor(String.class).newInstance("demo.xml"); - return new SpringContextAdapter((ApplicationContext) context); + return new SpringContextAdapter((AbstractRefreshableApplicationContext) context); }); }