You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by ma...@apache.org on 2026/10/21 00:50:46 UTC

(logging-log4j2) 01/02: Restore LoggerContext property change listeners

This is an automated email from the ASF dual-hosted git repository.

jvz pushed a commit to branch fix/logger-context-property-change-listener
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git

commit 1c0e61a970d0aabc16462b22172862722a3ba917
Author: Matt Sicker <ms...@apple.com>
AuthorDate: Tue Oct 20 19:46:30 2026 -0500

    Restore LoggerContext property change listeners
    
    Log4j Core 2 consumers such as Micrometer's `Log4j2Metrics` register a
    `java.beans.PropertyChangeListener` on `LoggerContext` to learn about
    reconfiguration, and fail with a `NoSuchMethodError` against 3.x since
    `addPropertyChangeListener` was removed.
    
    Restore `PROPERTY_CONFIG`, `addPropertyChangeListener` and
    `removePropertyChangeListener` as deprecated methods that fire the same
    events as in 2.x. The `java.beans` types are confined to a lazily created
    helper and `java.desktop` stays optional, so only callers of the
    deprecated methods need that module.
---
 .../logging/log4j/core/LoggerUpdateTest.java       | 50 ++++++++++++++++++++
 log4j-core/pom.xml                                 |  3 ++
 .../log4j/core/LegacyPropertyChangeSupport.java    | 53 ++++++++++++++++++++++
 .../apache/logging/log4j/core/LoggerContext.java   | 52 +++++++++++++++++++++
 4 files changed, 158 insertions(+)

diff --git a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerUpdateTest.java b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerUpdateTest.java
index 64ec97fd74..972908b237 100644
--- a/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerUpdateTest.java
+++ b/log4j-core-test/src/test/java/org/apache/logging/log4j/core/LoggerUpdateTest.java
@@ -17,12 +17,17 @@
 package org.apache.logging.log4j.core;
 
 import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
 import static org.junit.jupiter.api.Assertions.assertSame;
 
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.ArrayList;
 import java.util.List;
 import org.apache.logging.log4j.Level;
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.core.config.Configuration;
+import org.apache.logging.log4j.core.config.DefaultConfiguration;
 import org.apache.logging.log4j.core.config.LoggerConfig;
 import org.apache.logging.log4j.core.test.appender.ListAppender;
 import org.apache.logging.log4j.core.test.junit.LoggerContextSource;
@@ -65,4 +70,49 @@ public class LoggerUpdateTest {
         context.addConfigurationStartedListener(configuration -> assertSame(config, configuration));
         context.updateLoggers();
     }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    void propertyChangeListenerReceivesOldAndNewConfigurationOnReconfiguration(final LoggerContext context) {
+        // Given a java.beans listener, as registered by Log4j Core 2 consumers such as Micrometer
+        final List<PropertyChangeEvent> events = new ArrayList<>();
+        context.addPropertyChangeListener(events::add);
+        final Configuration previous = context.getConfiguration();
+        final Configuration replacement = new DefaultConfiguration();
+
+        // When the configuration is replaced
+        context.setConfiguration(replacement);
+
+        // Then the listener sees the old and the new configuration
+        assertThat(events).isNotEmpty();
+        final PropertyChangeEvent last = events.get(events.size() - 1);
+        assertThat(last.getSource()).isSameAs(context);
+        assertThat(last.getPropertyName()).isEqualTo(LoggerContext.PROPERTY_CONFIG);
+        assertThat(last.getOldValue()).isSameAs(previous);
+        assertThat(last.getNewValue()).isSameAs(replacement);
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    void removedPropertyChangeListenerIsNotNotified(final LoggerContext context) {
+        // Given a listener that was registered and then removed
+        final List<PropertyChangeEvent> events = new ArrayList<>();
+        final PropertyChangeListener listener = events::add;
+        context.addPropertyChangeListener(listener);
+        context.removePropertyChangeListener(listener);
+
+        // When the loggers are updated
+        context.updateLoggers();
+
+        // Then the listener is not called
+        assertThat(events).isEmpty();
+    }
+
+    @Test
+    @SuppressWarnings("deprecation")
+    void nullPropertyChangeListenerIsRejected(final LoggerContext context) {
+        assertThatThrownBy(() -> context.addPropertyChangeListener(null))
+                .isInstanceOf(NullPointerException.class)
+                .hasMessage("listener");
+    }
 }
diff --git a/log4j-core/pom.xml b/log4j-core/pom.xml
index b2b00b30d8..6e88d3f23e 100644
--- a/log4j-core/pom.xml
+++ b/log4j-core/pom.xml
@@ -47,6 +47,8 @@
       <!-- java.management: used by ProcessIdPatternConverter -->
       java.lang.management;resolution:=optional,
       javax.management.*;resolution:=optional,
+      <!-- java.desktop: used by the deprecated LoggerContext property change listener methods -->
+      java.beans;resolution:=optional,
       <!-- java.sql -->
       java.sql.*;resolution:=optional,
       <!-- java.xml -->
@@ -58,6 +60,7 @@
     </bnd-extra-package-options>
     <bnd-extra-module-options>
       <!-- Optional dependencies can not be transitive -->
+      java.desktop;transitive=false,
       java.management;transitive=false,
       java.sql;transitive=false,
       java.xml;transitive=false,
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/LegacyPropertyChangeSupport.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/LegacyPropertyChangeSupport.java
new file mode 100644
index 0000000000..b6f0845207
--- /dev/null
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LegacyPropertyChangeSupport.java
@@ -0,0 +1,53 @@
+/*
+ * 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.logging.log4j.core;
+
+import java.beans.PropertyChangeEvent;
+import java.beans.PropertyChangeListener;
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+import org.apache.logging.log4j.core.config.Configuration;
+
+/**
+ * Backs the deprecated {@code java.beans} listener methods of {@link LoggerContext}.
+ * <p>
+ *   This class is the only place that touches {@code java.beans} types at runtime, and {@link LoggerContext} creates
+ *   it lazily. A runtime without the {@code java.desktop} module therefore only fails if a caller registers a
+ *   {@link PropertyChangeListener}.
+ * </p>
+ */
+final class LegacyPropertyChangeSupport {
+
+    private final List<PropertyChangeListener> listeners = new CopyOnWriteArrayList<>();
+
+    void add(final PropertyChangeListener listener) {
+        listeners.add(listener);
+    }
+
+    void remove(final PropertyChangeListener listener) {
+        listeners.remove(listener);
+    }
+
+    void fireConfigurationChange(
+            final LoggerContext source, final Configuration oldConfiguration, final Configuration newConfiguration) {
+        final PropertyChangeEvent event =
+                new PropertyChangeEvent(source, LoggerContext.PROPERTY_CONFIG, oldConfiguration, newConfiguration);
+        for (final PropertyChangeListener listener : listeners) {
+            listener.propertyChange(event);
+        }
+    }
+}
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
index ea16b98e59..f7fed68c47 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/LoggerContext.java
@@ -20,6 +20,7 @@ import static org.apache.logging.log4j.core.util.ShutdownCallbackRegistry.SHUTDO
 
 import aQute.bnd.annotation.Cardinality;
 import aQute.bnd.annotation.spi.ServiceConsumer;
+import java.beans.PropertyChangeListener;
 import java.io.File;
 import java.net.URI;
 import java.util.ArrayList;
@@ -85,6 +86,15 @@ public class LoggerContext extends AbstractLifeCycle
                 Consumer<Reconfigurable>,
                 LoggerContextShutdownEnabled {
 
+    /**
+     * Property name of the property change event fired if the configuration is changed.
+     *
+     * @deprecated since 3.0.0, use {@link #addConfigurationStartedListener(Consumer)} and
+     *     {@link #addConfigurationStoppedListener(Consumer)} instead.
+     */
+    @Deprecated
+    public static final String PROPERTY_CONFIG = "config";
+
     public static final Key<LoggerContext> KEY = Key.forClass(LoggerContext.class);
 
     private final MessageFactory defaultMessageFactory;
@@ -92,6 +102,8 @@ public class LoggerContext extends AbstractLifeCycle
 
     private final Collection<Consumer<Configuration>> configurationStartedListeners = new ArrayList<>();
     private final Collection<Consumer<Configuration>> configurationStoppedListeners = new ArrayList<>();
+    // Created on first use so that java.desktop is only needed by callers of the deprecated java.beans methods
+    private final Lazy<LegacyPropertyChangeSupport> propertyChangeSupport = Lazy.lazy(LegacyPropertyChangeSupport::new);
     private final Lazy<List<LoggerContextShutdownAware>> listeners = Lazy.lazy(CopyOnWriteArrayList::new);
     private final ConfigurableInstanceFactory instanceFactory;
     private final PropertyEnvironment environment;
@@ -675,6 +687,7 @@ public class LoggerContext extends AbstractLifeCycle
                 prev.stop();
                 notifyConfigurationStopped(prev);
             }
+            firePropertyChangeEvent(prev, config);
 
             return prev;
         } finally {
@@ -682,6 +695,43 @@ public class LoggerContext extends AbstractLifeCycle
         }
     }
 
+    /**
+     * Adds a listener that is notified with a {@code PropertyChangeEvent} named {@link #PROPERTY_CONFIG} whenever the
+     * configuration is replaced or the loggers are updated.
+     * <p>
+     *   The old and new values of the event are the previous and the new {@link Configuration}.
+     *   This method only exists for binary compatibility with Log4j Core 2 consumers such as Micrometer.
+     * </p>
+     *
+     * @param listener a listener, not {@code null}
+     * @deprecated since 3.0.0, use {@link #addConfigurationStartedListener(Consumer)} and
+     *     {@link #addConfigurationStoppedListener(Consumer)} instead.
+     */
+    @Deprecated
+    public void addPropertyChangeListener(final PropertyChangeListener listener) {
+        propertyChangeSupport.get().add(Objects.requireNonNull(listener, "listener"));
+    }
+
+    /**
+     * Removes a listener added with {@link #addPropertyChangeListener(PropertyChangeListener)}.
+     *
+     * @param listener a listener
+     * @deprecated since 3.0.0, use {@link #removeConfigurationStartedListener(Consumer)} and
+     *     {@link #removeConfigurationStoppedListener(Consumer)} instead.
+     */
+    @Deprecated
+    public void removePropertyChangeListener(final PropertyChangeListener listener) {
+        if (propertyChangeSupport.isInitialized()) {
+            propertyChangeSupport.get().remove(listener);
+        }
+    }
+
+    private void firePropertyChangeEvent(final Configuration oldConfiguration, final Configuration newConfiguration) {
+        if (propertyChangeSupport.isInitialized()) {
+            propertyChangeSupport.get().fireConfigurationChange(this, oldConfiguration, newConfiguration);
+        }
+    }
+
     public void addConfigurationStartedListener(final Consumer<Configuration> listener) {
         configLock.lock();
         try {
@@ -832,10 +882,12 @@ public class LoggerContext extends AbstractLifeCycle
      * @param config The Configuration.
      */
     public void updateLoggers(final Configuration config) {
+        final Configuration old = this.configuration;
         for (final Logger logger : loggerRegistry.getLoggers()) {
             logger.updateConfiguration(config);
         }
         notifyConfigurationStarted(config);
+        firePropertyChangeEvent(old, config);
     }
 
     /**