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);
}
/**