This is an automated email from the ASF dual-hosted git repository. rmaucher pushed a commit to branch 11.0.x in repository https://gitbox.apache.org/repos/asf/tomcat.git
commit 67ceed2eae1038cc92d18a7784ea1ffe3edf5936 Author: opencode <[email protected]> AuthorDate: Wed Sep 30 11:18:51 2026 +0200 Make FrameworkListener registration idempotent so repeated Server start events do not add duplicate container listeners or orphan previous per-context lifecycle listeners that can no longer be removed --- .../apache/catalina/core/FrameworkListener.java | 28 +++++- .../catalina/core/TestFrameworkListener.java | 99 ++++++++++++++++++++++ 2 files changed, 124 insertions(+), 3 deletions(-) diff --git a/java/org/apache/catalina/core/FrameworkListener.java b/java/org/apache/catalina/core/FrameworkListener.java index c8b8c95cf3..e755cb325b 100644 --- a/java/org/apache/catalina/core/FrameworkListener.java +++ b/java/org/apache/catalina/core/FrameworkListener.java @@ -83,12 +83,29 @@ public abstract class FrameworkListener implements LifecycleListener, ContainerL for (Service service : server.findServices()) { Engine engine = service.getContainer(); if (engine != null) { - engine.addContainerListener(this); + addContainerListenerIfAbsent(engine); registerListenersForEngine(engine); } } } + + /** + * Registers this as a container listener on the given container unless it is already registered, so that repeated + * Server start events do not result in duplicate registrations. + * + * @param container The container on which to register this listener + */ + private void addContainerListenerIfAbsent(Container container) { + for (ContainerListener listener : container.findContainerListeners()) { + if (listener == this) { + return; + } + } + container.addContainerListener(this); + } + + /** * Registers listeners on all hosts of the given engine. * @@ -97,11 +114,10 @@ public abstract class FrameworkListener implements LifecycleListener, ContainerL protected void registerListenersForEngine(Engine engine) { for (Container hostContainer : engine.findChildren()) { Host host = (Host) hostContainer; - host.addContainerListener(this); + addContainerListenerIfAbsent(host); registerListenersForHost(host); } } - /** * Registers listeners on all contexts of the given host. * @@ -120,6 +136,12 @@ public abstract class FrameworkListener implements LifecycleListener, ContainerL * @param context The context to register a listener for */ protected void registerContextListener(Context context) { + // Reuse an existing listener if one is already registered, so that + // repeated Server start events do not create duplicate listeners that + // stay attached to the context after the map reference is replaced + if (contextListeners.containsKey(context)) { + return; + } LifecycleListener listener = createLifecycleListener(context); contextListeners.put(context, listener); context.addLifecycleListener(listener); diff --git a/test/org/apache/catalina/core/TestFrameworkListener.java b/test/org/apache/catalina/core/TestFrameworkListener.java new file mode 100644 index 0000000000..d8b59df1c6 --- /dev/null +++ b/test/org/apache/catalina/core/TestFrameworkListener.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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 org.apache.catalina.core; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + +import org.junit.Assert; +import org.junit.Test; + +import org.apache.catalina.ContainerListener; +import org.apache.catalina.Context; +import org.apache.catalina.Lifecycle; +import org.apache.catalina.LifecycleEvent; +import org.apache.catalina.LifecycleListener; + +public class TestFrameworkListener { + + @Test + public void testRegistrationIsIdempotent() throws Exception { + StandardServer server = new StandardServer(); + StandardService service = new StandardService(); + service.setName("service"); + StandardEngine engine = new StandardEngine(); + engine.setName("engine"); + service.setContainer(engine); + server.addService(service); + + StandardHost host = new StandardHost(); + host.setName("localhost"); + engine.addChild(host); + + StandardContext context = new StandardContext(); + context.setPath("/ctx"); + host.addChild(context); + + TesterListener listener = new TesterListener(); + + LifecycleEvent beforeStart = new LifecycleEvent(server, Lifecycle.BEFORE_START_EVENT, null); + + // Simulate repeated in-place Server starts + listener.lifecycleEvent(beforeStart); + listener.lifecycleEvent(beforeStart); + listener.lifecycleEvent(beforeStart); + + Assert.assertEquals(1, countListener(engine.findContainerListeners(), listener)); + Assert.assertEquals(1, countListener(host.findContainerListeners(), listener)); + Assert.assertEquals(1, listener.getContextListenerCount()); + Assert.assertEquals(1, listener.getCreatedListeners().size()); + // Exactly one listener — the one created for this context — must be attached + LifecycleListener created = listener.getCreatedListeners().get(0); + long attached = Stream.of(context.findLifecycleListeners()).filter(l -> l == created).count(); + Assert.assertEquals(1, attached); + } + + + private static long countListener(ContainerListener[] listeners, ContainerListener expected) { + return Stream.of(listeners).filter(l -> l == expected).count(); + } + + + private static class TesterListener extends FrameworkListener { + + private final List<LifecycleListener> createdListeners = new ArrayList<>(); + + @Override + protected LifecycleListener createLifecycleListener(Context context) { + // Return a new instance on each call, as OpenWebBeansListener does + LifecycleListener listener = event -> { + // No-Op + }; + createdListeners.add(listener); + return listener; + } + + int getContextListenerCount() { + return contextListeners.size(); + } + + List<LifecycleListener> getCreatedListeners() { + return createdListeners; + } + } +} --------------------------------------------------------------------- To unsubscribe, e-mail: [email protected] For additional commands, e-mail: [email protected]
