diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index ee3d0963323..64712968763 100644
--- a/.github/dependabot.yaml
+++ b/.github/dependabot.yaml
@@ -59,6 +59,7 @@ updates:
- "/log4j-docker"
- "/log4j-fuzz-test"
- "/log4j-iostreams"
+ - "/log4j-jakarta-jms"
- "/log4j-jakarta-smtp"
- "/log4j-jakarta-web"
- "/log4j-jcl"
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java
index 8d2c87c4b8d..b6df7dd08dd 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java
@@ -37,11 +37,14 @@
import org.apache.logging.log4j.core.net.JndiManager;
/**
- * Generic JMS Appender plugin for both queues and topics. This Appender replaces the previous split ones. However,
- * configurations set up for the 2.0 version of the JMS appenders will still work.
+ * Javax JMS Appender plugin. This Appender replaces the previous split classes.
+ * Configurations set up for the 2.0 version of the JMS appenders will still work.
+ *
+ * @deprecated Use {@code org.apache.logging.log4j.core.appender.mom.jakarta.JmsAppender}.
*/
-@Plugin(name = "JMS", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
-@PluginAliases({"JMSQueue", "JMSTopic"})
+@Deprecated
+@Plugin(name = "JMS-Javax", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
+@PluginAliases({"JMS", "JMSQueue", "JMSTopic"})
public class JmsAppender extends AbstractAppender {
public static class Builder> extends AbstractAppender.Builder
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java
index 3d2872a9fd8..178d1f428af 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java
@@ -42,10 +42,12 @@
* Consider this class private; it is only public for access by integration tests.
*
*
- * JMS connection and session manager. Can be used to access MessageProducer, MessageConsumer, and Message objects
- * involving a configured ConnectionFactory and Destination.
+ * JMS connection and destination manager. Uses a MessageProducer to send log events to a JMS Destination.
*
+ *
+ * @deprecated Use {@code org.apache.logging.log4j.core.appender.mom.jakarta.JmsManager}.
*/
+@Deprecated
public class JmsManager extends AbstractManager {
public static class JmsManagerConfiguration {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java
index 6ed35287fdc..c12d8343757 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java
+++ b/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java
@@ -21,7 +21,7 @@
* @since 2.1
*/
@Export
-@Version("2.20.1")
+@Version("2.25.0")
package org.apache.logging.log4j.core.appender.mom;
import org.osgi.annotation.bundle.Export;
diff --git a/log4j-jakarta-jms/.log4j-plugin-processing-activator b/log4j-jakarta-jms/.log4j-plugin-processing-activator
new file mode 100644
index 00000000000..ba133f36961
--- /dev/null
+++ b/log4j-jakarta-jms/.log4j-plugin-processing-activator
@@ -0,0 +1 @@
+This file is here to activate the `plugin-processing` Maven profile.
diff --git a/log4j-jakarta-jms/pom.xml b/log4j-jakarta-jms/pom.xml
new file mode 100644
index 00000000000..57b7b2c14b8
--- /dev/null
+++ b/log4j-jakarta-jms/pom.xml
@@ -0,0 +1,71 @@
+
+
+
+ 4.0.0
+
+ org.apache.logging.log4j
+ log4j
+ ${revision}
+ ../log4j-parent
+
+ log4j-jakarta-jms
+ Apache Log4j Jakarta JMS
+ Apache Log4j Java Message Service (JMS), version for Jakarta.
+
+
+ true
+
+ org.apache.logging.log4j.jakarta.jms
+ org.apache.logging.log4j.core
+
+
+
+ jakarta.jms
+ jakarta.jms-api
+ provided
+
+
+ org.apache.logging.log4j
+ log4j-api
+
+
+ org.apache.logging.log4j
+ log4j-core
+
+
+ org.junit.vintage
+ junit-vintage-engine
+ test
+
+
+ org.junit.jupiter
+ junit-jupiter-engine
+ test
+
+
+ org.apache.logging.log4j
+ log4j-core-test
+ test
+
+
+ commons-logging
+ commons-logging
+ test
+
+
+
diff --git a/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java
new file mode 100644
index 00000000000..661cfd03295
--- /dev/null
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java
@@ -0,0 +1,242 @@
+/*
+ * 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.appender.mom.jakarta;
+
+import jakarta.jms.JMSException;
+import java.io.Serializable;
+import java.util.Properties;
+import java.util.concurrent.TimeUnit;
+import org.apache.logging.log4j.core.Appender;
+import org.apache.logging.log4j.core.Filter;
+import org.apache.logging.log4j.core.Layout;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractAppender;
+import org.apache.logging.log4j.core.appender.AbstractManager;
+import org.apache.logging.log4j.core.appender.mom.jakarta.JmsManager.JmsManagerConfiguration;
+import org.apache.logging.log4j.core.config.Node;
+import org.apache.logging.log4j.core.config.Property;
+import org.apache.logging.log4j.core.config.plugins.Plugin;
+import org.apache.logging.log4j.core.config.plugins.PluginAliases;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderAttribute;
+import org.apache.logging.log4j.core.config.plugins.PluginBuilderFactory;
+import org.apache.logging.log4j.core.config.plugins.validation.constraints.Required;
+import org.apache.logging.log4j.core.net.JndiManager;
+
+/**
+ * Jakarta JMS Appender plugin for both queues and topics.
+ */
+@Plugin(name = "JMS-Jakarta", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
+public final class JmsAppender extends AbstractAppender {
+
+ public static final class Builder extends AbstractAppender.Builder
+ implements org.apache.logging.log4j.core.util.Builder {
+
+ public static final int DEFAULT_RECONNECT_INTERVAL_MILLIS = 5000;
+
+ @PluginBuilderAttribute
+ private String factoryName;
+
+ @PluginBuilderAttribute
+ private String providerUrl;
+
+ @PluginBuilderAttribute
+ private String urlPkgPrefixes;
+
+ @PluginBuilderAttribute
+ private String securityPrincipalName;
+
+ @PluginBuilderAttribute(sensitive = true)
+ private String securityCredentials;
+
+ @PluginBuilderAttribute
+ @Required(message = "A jakarta.jms.ConnectionFactory JNDI name must be specified")
+ private String factoryBindingName;
+
+ @PluginBuilderAttribute
+ @PluginAliases({"queueBindingName", "topicBindingName"})
+ @Required(message = "A jakarta.jms.Destination JNDI name must be specified")
+ private String destinationBindingName;
+
+ @PluginBuilderAttribute
+ private String userName;
+
+ @PluginBuilderAttribute(sensitive = true)
+ private char[] password;
+
+ @PluginBuilderAttribute
+ private long reconnectIntervalMillis = DEFAULT_RECONNECT_INTERVAL_MILLIS;
+
+ @PluginBuilderAttribute
+ private boolean immediateFail;
+
+ // Programmatic access only for now.
+ private JmsManager jmsManager;
+
+ private Builder() {}
+
+ @SuppressWarnings("resource") // actualJmsManager and jndiManager are managed by the JmsAppender
+ @Override
+ public JmsAppender build() {
+ JmsManager actualJmsManager = jmsManager;
+ JmsManagerConfiguration configuration = null;
+ if (actualJmsManager == null) {
+ final Properties jndiProperties = JndiManager.createProperties(
+ factoryName, providerUrl, urlPkgPrefixes, securityPrincipalName, securityCredentials, null);
+ configuration = new JmsManagerConfiguration(
+ jndiProperties,
+ factoryBindingName,
+ destinationBindingName,
+ userName,
+ password,
+ immediateFail,
+ reconnectIntervalMillis);
+ actualJmsManager = AbstractManager.getManager(getName(), JmsManager.FACTORY, configuration);
+ }
+ if (actualJmsManager == null) {
+ // JmsManagerFactory has already logged an ERROR.
+ return null;
+ }
+ final Layout extends Serializable> layout = getLayout();
+ if (layout == null) {
+ LOGGER.error("No layout provided for JmsAppender");
+ return null;
+ }
+ try {
+ return new JmsAppender(
+ getName(), getFilter(), layout, isIgnoreExceptions(), getPropertyArray(), actualJmsManager);
+ } catch (final JMSException e) {
+ // Never happens since the ctor no longer actually throws a JMSException.
+ throw new IllegalStateException(e);
+ }
+ }
+
+ public Builder setDestinationBindingName(final String destinationBindingName) {
+ this.destinationBindingName = destinationBindingName;
+ return this;
+ }
+
+ public Builder setFactoryBindingName(final String factoryBindingName) {
+ this.factoryBindingName = factoryBindingName;
+ return this;
+ }
+
+ public Builder setFactoryName(final String factoryName) {
+ this.factoryName = factoryName;
+ return this;
+ }
+
+ public Builder setImmediateFail(final boolean immediateFail) {
+ this.immediateFail = immediateFail;
+ return this;
+ }
+
+ public Builder setJmsManager(final JmsManager jmsManager) {
+ this.jmsManager = jmsManager;
+ return this;
+ }
+
+ public Builder setPassword(final char[] password) {
+ this.password = password;
+ return this;
+ }
+
+ public Builder setProviderUrl(final String providerUrl) {
+ this.providerUrl = providerUrl;
+ return this;
+ }
+
+ public Builder setReconnectIntervalMillis(final long reconnectIntervalMillis) {
+ this.reconnectIntervalMillis = reconnectIntervalMillis;
+ return this;
+ }
+
+ public Builder setSecurityCredentials(final String securityCredentials) {
+ this.securityCredentials = securityCredentials;
+ return this;
+ }
+
+ public Builder setSecurityPrincipalName(final String securityPrincipalName) {
+ this.securityPrincipalName = securityPrincipalName;
+ return this;
+ }
+
+ public Builder setUrlPkgPrefixes(final String urlPkgPrefixes) {
+ this.urlPkgPrefixes = urlPkgPrefixes;
+ return this;
+ }
+
+ public Builder setUserName(final String userName) {
+ this.userName = userName;
+ return this;
+ }
+
+ /**
+ * Does not include the password.
+ */
+ @Override
+ public String toString() {
+ return "Builder [name=" + getName() + ", factoryName=" + factoryName + ", providerUrl=" + providerUrl
+ + ", urlPkgPrefixes=" + urlPkgPrefixes + ", securityPrincipalName=" + securityPrincipalName
+ + ", securityCredentials=" + securityCredentials + ", factoryBindingName=" + factoryBindingName
+ + ", destinationBindingName=" + destinationBindingName + ", username=" + userName + ", layout="
+ + getLayout() + ", filter=" + getFilter() + ", ignoreExceptions=" + isIgnoreExceptions()
+ + ", jmsManager=" + jmsManager + "]";
+ }
+ }
+
+ @PluginBuilderFactory
+ public static Builder newBuilder() {
+ return new Builder();
+ }
+
+ private volatile JmsManager manager;
+
+ /**
+ * Constructs a new instance.
+ *
+ * @throws JMSException not thrown as of 2.9 but retained in the signature for compatibility, will be removed in 3.0
+ */
+ private JmsAppender(
+ final String name,
+ final Filter filter,
+ final Layout extends Serializable> layout,
+ final boolean ignoreExceptions,
+ final Property[] properties,
+ final JmsManager manager)
+ throws JMSException {
+ super(name, filter, layout, ignoreExceptions, properties);
+ this.manager = manager;
+ }
+
+ @Override
+ public void append(final LogEvent event) {
+ this.manager.send(event, toSerializable(event));
+ }
+
+ public JmsManager getManager() {
+ return manager;
+ }
+
+ @Override
+ public boolean stop(final long timeout, final TimeUnit timeUnit) {
+ setStopping();
+ boolean stopped = super.stop(timeout, timeUnit, false);
+ stopped &= this.manager.stop(timeout, timeUnit);
+ setStopped();
+ return stopped;
+ }
+}
diff --git a/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java
new file mode 100644
index 00000000000..81b915dafeb
--- /dev/null
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java
@@ -0,0 +1,508 @@
+/*
+ * 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.appender.mom.jakarta;
+
+import jakarta.jms.Connection;
+import jakarta.jms.ConnectionFactory;
+import jakarta.jms.Destination;
+import jakarta.jms.JMSException;
+import jakarta.jms.MapMessage;
+import jakarta.jms.Message;
+import jakarta.jms.MessageProducer;
+import jakarta.jms.Session;
+import java.io.Serializable;
+import java.util.Properties;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import javax.naming.NamingException;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.appender.AbstractManager;
+import org.apache.logging.log4j.core.appender.AppenderLoggingException;
+import org.apache.logging.log4j.core.appender.ManagerFactory;
+import org.apache.logging.log4j.core.net.JndiManager;
+import org.apache.logging.log4j.core.util.Log4jThread;
+import org.apache.logging.log4j.status.StatusLogger;
+
+/**
+ * Consider this class private; it is only public for access by integration tests.
+ *
+ *
+ * JMS connection and session manager. Can be used to access MessageProducer, MessageConsumer, and Message objects
+ * involving a configured ConnectionFactory and Destination.
+ *
+ */
+public final class JmsManager extends AbstractManager {
+
+ public static final class JmsManagerConfiguration {
+ private final Properties jndiProperties;
+ private final String connectionFactoryName;
+ private final String destinationName;
+ private final String userName;
+ private final char[] password;
+ private final boolean immediateFail;
+ private final boolean retry;
+ private final long reconnectIntervalMillis;
+
+ JmsManagerConfiguration(
+ final Properties jndiProperties,
+ final String connectionFactoryName,
+ final String destinationName,
+ final String userName,
+ final char[] password,
+ final boolean immediateFail,
+ final long reconnectIntervalMillis) {
+ this.jndiProperties = jndiProperties;
+ this.connectionFactoryName = connectionFactoryName;
+ this.destinationName = destinationName;
+ this.userName = userName;
+ this.password = password;
+ this.immediateFail = immediateFail;
+ this.reconnectIntervalMillis = reconnectIntervalMillis;
+ this.retry = reconnectIntervalMillis > 0;
+ }
+
+ public String getConnectionFactoryName() {
+ return connectionFactoryName;
+ }
+
+ public String getDestinationName() {
+ return destinationName;
+ }
+
+ public JndiManager getJndiManager() {
+ return JndiManager.getJndiManager(getJndiProperties());
+ }
+
+ public Properties getJndiProperties() {
+ return jndiProperties;
+ }
+
+ public char[] getPassword() {
+ return password;
+ }
+
+ public long getReconnectIntervalMillis() {
+ return reconnectIntervalMillis;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public boolean isImmediateFail() {
+ return immediateFail;
+ }
+
+ public boolean isRetry() {
+ return retry;
+ }
+
+ @Override
+ public String toString() {
+ return "JmsManagerConfiguration [jndiProperties=" + jndiProperties + ", connectionFactoryName="
+ + connectionFactoryName + ", destinationName=" + destinationName + ", userName=" + userName
+ + ", immediateFail=" + immediateFail + ", retry=" + retry + ", reconnectIntervalMillis="
+ + reconnectIntervalMillis + "]";
+ }
+ }
+
+ private static final class JmsManagerFactory implements ManagerFactory {
+
+ @Override
+ public JmsManager createManager(final String name, final JmsManagerConfiguration data) {
+ if (JndiManager.isJndiJmsEnabled()) {
+ try {
+ return new JmsManager(name, data);
+ } catch (final Exception e) {
+ logger().error("Error creating JmsManager using JmsManagerConfiguration [{}]", data, e);
+ return null;
+ }
+ }
+ logger().error("JNDI must be enabled by setting log4j2.enableJndiJms=true");
+ return null;
+ }
+ }
+
+ /**
+ * Handles reconnecting to JMS on a Thread.
+ */
+ private final class Reconnector extends Log4jThread {
+
+ private final CountDownLatch latch = new CountDownLatch(1);
+
+ private volatile boolean shutdown;
+
+ private final Object owner;
+
+ private Reconnector(final Object owner) {
+ super("JmsManager-Reconnector");
+ this.owner = owner;
+ }
+
+ public void latch() {
+ try {
+ latch.await();
+ } catch (final InterruptedException ex) {
+ // Ignore the exception.
+ }
+ }
+
+ void reconnect() throws NamingException, JMSException {
+ final JndiManager jndiManager2 = getJndiManager();
+ final Connection connection2 = createConnection(jndiManager2);
+ final Session session2 = createSession(connection2);
+ final Destination destination2 = createDestination(jndiManager2);
+ final MessageProducer messageProducer2 = createMessageProducer(session2, destination2);
+ connection2.start();
+ synchronized (owner) {
+ jndiManager = jndiManager2;
+ connection = connection2;
+ session = session2;
+ destination = destination2;
+ messageProducer = messageProducer2;
+ reconnector = null;
+ shutdown = true;
+ }
+ logger().debug("Connection reestablished to {}", configuration);
+ }
+
+ @Override
+ public void run() {
+ while (!shutdown) {
+ try {
+ sleep(configuration.getReconnectIntervalMillis());
+ reconnect();
+ } catch (final InterruptedException | JMSException | NamingException e) {
+ logger().debug(
+ "Cannot reestablish JMS connection to {}: {}",
+ configuration,
+ e.getLocalizedMessage(),
+ e);
+ } finally {
+ latch.countDown();
+ }
+ }
+ }
+
+ public void shutdown() {
+ shutdown = true;
+ }
+ }
+
+ static final JmsManagerFactory FACTORY = new JmsManagerFactory();
+
+ /**
+ * Gets a JmsManager using the specified configuration parameters.
+ *
+ * @param name
+ * The name to use for this JmsManager.
+ * @param connectionFactoryName
+ * The binding name for the {@link javax.jms.ConnectionFactory}.
+ * @param destinationName
+ * The binding name for the {@link javax.jms.Destination}.
+ * @param userName
+ * The userName to connect with or {@code null} for no authentication.
+ * @param password
+ * The password to use with the given userName or {@code null} for no authentication.
+ * @param immediateFail
+ * Whether or not to fail immediately with a {@link AppenderLoggingException} when connecting to JMS
+ * fails.
+ * @param reconnectIntervalMillis
+ * How to log sleep in milliseconds before trying to reconnect to JMS.
+ * @param jndiProperties
+ * JNDI properties.
+ * @return The JmsManager as configured.
+ */
+ public static JmsManager getJmsManager(
+ final String name,
+ final Properties jndiProperties,
+ final String connectionFactoryName,
+ final String destinationName,
+ final String userName,
+ final char[] password,
+ final boolean immediateFail,
+ final long reconnectIntervalMillis) {
+ final JmsManagerConfiguration configuration = new JmsManagerConfiguration(
+ jndiProperties,
+ connectionFactoryName,
+ destinationName,
+ userName,
+ password,
+ immediateFail,
+ reconnectIntervalMillis);
+ return getManager(name, FACTORY, configuration);
+ }
+
+ private final JmsManagerConfiguration configuration;
+
+ private volatile Reconnector reconnector;
+ private volatile JndiManager jndiManager;
+ private volatile Connection connection;
+ private volatile Session session;
+ private volatile Destination destination;
+ private volatile MessageProducer messageProducer;
+
+ private JmsManager(final String name, final JmsManagerConfiguration configuration) {
+ super(null, name);
+ this.configuration = configuration;
+ this.jndiManager = configuration.getJndiManager();
+ try {
+ this.connection = createConnection(this.jndiManager);
+ this.session = createSession(this.connection);
+ this.destination = createDestination(this.jndiManager);
+ this.messageProducer = createMessageProducer(this.session, this.destination);
+ this.connection.start();
+ } catch (NamingException | JMSException e) {
+ this.reconnector = createReconnector();
+ this.reconnector.start();
+ }
+ }
+
+ private boolean closeConnection() {
+ if (connection == null) {
+ return true;
+ }
+ final Connection temp = connection;
+ connection = null;
+ try {
+ temp.close();
+ return true;
+ } catch (final JMSException e) {
+ StatusLogger.getLogger()
+ .debug(
+ "Caught exception closing JMS Connection: {} ({}); continuing JMS manager shutdown",
+ e.getLocalizedMessage(),
+ temp,
+ e);
+ return false;
+ }
+ }
+
+ private boolean closeJndiManager() {
+ if (jndiManager == null) {
+ return true;
+ }
+ final JndiManager tmp = jndiManager;
+ jndiManager = null;
+ tmp.close();
+ return true;
+ }
+
+ private boolean closeMessageProducer() {
+ if (messageProducer == null) {
+ return true;
+ }
+ final MessageProducer temp = messageProducer;
+ messageProducer = null;
+ try {
+ temp.close();
+ return true;
+ } catch (final JMSException e) {
+ StatusLogger.getLogger()
+ .debug(
+ "Caught exception closing JMS MessageProducer: {} ({}); continuing JMS manager shutdown",
+ e.getLocalizedMessage(),
+ temp,
+ e);
+ return false;
+ }
+ }
+
+ private boolean closeSession() {
+ if (session == null) {
+ return true;
+ }
+ final Session temp = session;
+ session = null;
+ try {
+ temp.close();
+ return true;
+ } catch (final JMSException e) {
+ StatusLogger.getLogger()
+ .debug(
+ "Caught exception closing JMS Session: {} ({}); continuing JMS manager shutdown",
+ e.getLocalizedMessage(),
+ temp,
+ e);
+ return false;
+ }
+ }
+
+ private Connection createConnection(final JndiManager jndiManager) throws NamingException, JMSException {
+ final ConnectionFactory connectionFactory = jndiManager.lookup(configuration.getConnectionFactoryName());
+ if (configuration.getUserName() != null && configuration.getPassword() != null) {
+ return connectionFactory.createConnection(
+ configuration.getUserName(),
+ configuration.getPassword() == null ? null : String.valueOf(configuration.getPassword()));
+ }
+ return connectionFactory.createConnection();
+ }
+
+ private Destination createDestination(final JndiManager jndiManager) throws NamingException {
+ return jndiManager.lookup(configuration.getDestinationName());
+ }
+
+ /**
+ * Creates a TextMessage, MapMessage, or ObjectMessage from a Serializable object.
+ *
+ * For instance, when using a text-based {@link org.apache.logging.log4j.core.Layout} such as
+ * {@link org.apache.logging.log4j.core.layout.PatternLayout}, the {@link org.apache.logging.log4j.core.LogEvent}
+ * message will be serialized to a String.
+ *
+ *
+ * When using a layout such as {@link org.apache.logging.log4j.core.layout.SerializedLayout}, the LogEvent message
+ * will be serialized as a Java object.
+ *
+ *
+ * When using a layout such as {@link org.apache.logging.log4j.core.layout.MessageLayout} and the LogEvent message
+ * is a Log4j MapMessage, the message will be serialized as a JMS MapMessage.
+ *
+ *
+ * @param object
+ * The LogEvent or String message to wrap.
+ * @return A new JMS message containing the provided object.
+ * @throws JMSException if the JMS provider fails to create this message due to some internal error.
+ */
+ private Message createMessage(final Serializable object) throws JMSException {
+ if (object instanceof String) {
+ return session.createTextMessage((String) object);
+ } else if (object instanceof org.apache.logging.log4j.message.MapMessage) {
+ return map((org.apache.logging.log4j.message.MapMessage, ?>) object, session.createMapMessage());
+ }
+ return session.createObjectMessage(object);
+ }
+
+ private void createMessageAndSend(final LogEvent event, final Serializable serializable) throws JMSException {
+ final Message message = createMessage(serializable);
+ message.setJMSTimestamp(event.getTimeMillis());
+ messageProducer.send(message);
+ }
+
+ /**
+ * Creates a MessageProducer on this Destination using the current Session.
+ *
+ * @param session
+ * The JMS Session to use to create the MessageProducer
+ * @param destination
+ * The JMS Destination for the MessageProducer
+ * @return A MessageProducer on this Destination.
+ * @throws JMSException if the session fails to create a MessageProducer due to some internal error.
+ */
+ private MessageProducer createMessageProducer(final Session session, final Destination destination)
+ throws JMSException {
+ return session.createProducer(destination);
+ }
+
+ private Reconnector createReconnector() {
+ final Reconnector recon = new Reconnector(this);
+ recon.setDaemon(true);
+ recon.setPriority(Thread.MIN_PRIORITY);
+ return recon;
+ }
+
+ private Session createSession(final Connection connection) throws JMSException {
+ return connection.createSession(false, Session.AUTO_ACKNOWLEDGE);
+ }
+
+ public JmsManagerConfiguration getJmsManagerConfiguration() {
+ return configuration;
+ }
+
+ JndiManager getJndiManager() {
+ return configuration.getJndiManager();
+ }
+
+ T lookup(final String destinationName) throws NamingException {
+ return jndiManager.lookup(destinationName);
+ }
+
+ private MapMessage map(
+ final org.apache.logging.log4j.message.MapMessage, ?> log4jMapMessage, final MapMessage jmsMapMessage) {
+ // Map without calling org.apache.logging.log4j.message.MapMessage#getData() which makes a copy of the map.
+ log4jMapMessage.forEach((key, value) -> {
+ try {
+ jmsMapMessage.setObject(key, value);
+ } catch (final JMSException e) {
+ throw new IllegalArgumentException(
+ String.format(
+ "%s mapping key '%s' to value '%s': %s",
+ e.getClass(), key, value, e.getLocalizedMessage()),
+ e);
+ }
+ });
+ return jmsMapMessage;
+ }
+
+ @Override
+ protected boolean releaseSub(final long timeout, final TimeUnit timeUnit) {
+ if (reconnector != null) {
+ reconnector.shutdown();
+ reconnector.interrupt();
+ reconnector = null;
+ }
+ boolean closed = false;
+ closed &= closeJndiManager();
+ closed &= closeMessageProducer();
+ closed &= closeSession();
+ closed &= closeConnection();
+ return closed && jndiManager.stop(timeout, timeUnit);
+ }
+
+ void send(final LogEvent event, final Serializable serializable) {
+ if (messageProducer == null) {
+ if (reconnector != null && !configuration.isImmediateFail()) {
+ reconnector.latch();
+ if (messageProducer == null) {
+ throw new AppenderLoggingException(
+ "Error sending to JMS Manager '" + getName() + "': JMS message producer not available");
+ }
+ }
+ }
+ synchronized (this) {
+ try {
+ createMessageAndSend(event, serializable);
+ } catch (final JMSException causeEx) {
+ if (configuration.isRetry() && reconnector == null) {
+ reconnector = createReconnector();
+ try {
+ closeJndiManager();
+ reconnector.reconnect();
+ } catch (NamingException | JMSException reconnEx) {
+ logger().debug(
+ "Cannot reestablish JMS connection to {}: {}; starting reconnector thread {}",
+ configuration,
+ reconnEx.getLocalizedMessage(),
+ reconnector.getName(),
+ reconnEx);
+ reconnector.start();
+ throw new AppenderLoggingException(
+ String.format("JMS exception sending to %s for %s", getName(), configuration), causeEx);
+ }
+ try {
+ createMessageAndSend(event, serializable);
+ } catch (final JMSException e) {
+ throw new AppenderLoggingException(
+ String.format(
+ "Error sending to %s after reestablishing JMS connection for %s",
+ getName(), configuration),
+ causeEx);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java
new file mode 100644
index 00000000000..a5651b62751
--- /dev/null
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java
@@ -0,0 +1,26 @@
+/*
+ * 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.
+ */
+
+/**
+ * Jakarta-based JMS Appender.
+ */
+@Export
+@Version("2.25.0")
+package org.apache.logging.log4j.core.appender.mom.jakarta;
+
+import org.osgi.annotation.bundle.Export;
+import org.osgi.annotation.versioning.Version;
diff --git a/log4j-jakarta-jms/src/test/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppenderTest.java b/log4j-jakarta-jms/src/test/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppenderTest.java
new file mode 100644
index 00000000000..5d730fc47b4
--- /dev/null
+++ b/log4j-jakarta-jms/src/test/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppenderTest.java
@@ -0,0 +1,196 @@
+/*
+ * 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.appender.mom.jakarta;
+
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.ArgumentMatchers.isA;
+import static org.mockito.BDDMockito.given;
+import static org.mockito.BDDMockito.then;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
+
+import jakarta.jms.Connection;
+import jakarta.jms.ConnectionFactory;
+import jakarta.jms.Destination;
+import jakarta.jms.MapMessage;
+import jakarta.jms.MessageProducer;
+import jakarta.jms.ObjectMessage;
+import jakarta.jms.Session;
+import jakarta.jms.TextMessage;
+import java.io.Serializable;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import org.apache.logging.log4j.Level;
+import org.apache.logging.log4j.core.LogEvent;
+import org.apache.logging.log4j.core.impl.Log4jLogEvent;
+import org.apache.logging.log4j.core.test.categories.Appenders;
+import org.apache.logging.log4j.core.test.junit.JndiRule;
+import org.apache.logging.log4j.core.test.junit.LoggerContextRule;
+import org.apache.logging.log4j.message.Message;
+import org.apache.logging.log4j.message.SimpleMessage;
+import org.apache.logging.log4j.message.StringMapMessage;
+import org.junit.AfterClass;
+import org.junit.Before;
+import org.junit.BeforeClass;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.rules.RuleChain;
+
+@Category(Appenders.Jms.class)
+public class JmsAppenderTest {
+
+ private static final String CONNECTION_FACTORY_NAME = "jms/connectionFactory";
+ private static final String QUEUE_FACTORY_NAME = "jms/queues";
+ private static final String TOPIC_FACTORY_NAME = "jms/topics";
+ private static final String DESTINATION_NAME = "jms/destination";
+ private static final String DESTINATION_NAME_ML = "jms/destination-ml";
+ private static final String QUEUE_NAME = "jms/queue";
+ private static final String TOPIC_NAME = "jms/topic";
+ private static final String LOG_MESSAGE = "Hello, world!";
+
+ private final ConnectionFactory connectionFactory = mock(ConnectionFactory.class);
+ private final Connection connection = mock(Connection.class);
+ private final Session session = mock(Session.class);
+ private final Destination destination = mock(Destination.class);
+ private final Destination destinationMl = mock(Destination.class);
+ private final MessageProducer messageProducer = mock(MessageProducer.class);
+ private final MessageProducer messageProducerMl = mock(MessageProducer.class);
+ private final TextMessage textMessage = mock(TextMessage.class);
+ private final ObjectMessage objectMessage = mock(ObjectMessage.class);
+ private final MapMessage mapMessage = mock(MapMessage.class);
+
+ private final JndiRule jndiRule = new JndiRule(createBindings());
+ private final LoggerContextRule ctx = new LoggerContextRule("JmsJakartaAppenderTest.xml");
+
+ @Rule
+ public RuleChain rules = RuleChain.outerRule(jndiRule).around(ctx);
+
+ @AfterClass
+ public static void afterClass() throws Exception {
+ System.clearProperty("log4j2.enableJndiJms");
+ }
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+ System.setProperty("log4j2.enableJndiJms", "true");
+ }
+
+ public JmsAppenderTest() throws Exception {
+ // this needs to set up before LoggerContextRule
+ given(connectionFactory.createConnection()).willReturn(connection);
+ given(connectionFactory.createConnection(anyString(), anyString())).willThrow(IllegalArgumentException.class);
+ given(connection.createSession(eq(false), eq(Session.AUTO_ACKNOWLEDGE))).willReturn(session);
+ given(session.createProducer(eq(destination))).willReturn(messageProducer);
+ given(session.createProducer(eq(destinationMl))).willReturn(messageProducerMl);
+ given(session.createTextMessage(anyString())).willReturn(textMessage);
+ given(session.createObjectMessage(isA(Serializable.class))).willReturn(objectMessage);
+ given(session.createMapMessage()).willReturn(mapMessage);
+ }
+
+ private Map createBindings() {
+ final ConcurrentHashMap map = new ConcurrentHashMap<>();
+ map.put(CONNECTION_FACTORY_NAME, connectionFactory);
+ map.put(DESTINATION_NAME, destination);
+ map.put(DESTINATION_NAME_ML, destinationMl);
+ map.put(QUEUE_FACTORY_NAME, connectionFactory);
+ map.put(QUEUE_NAME, destination);
+ map.put(TOPIC_FACTORY_NAME, connectionFactory);
+ map.put(TOPIC_NAME, destination);
+ return map;
+ }
+
+ private Log4jLogEvent createLogEvent() {
+ return createLogEvent(new SimpleMessage(LOG_MESSAGE));
+ }
+
+ private Log4jLogEvent createLogEvent(final Message message) {
+ // @formatter:off
+ return Log4jLogEvent.newBuilder()
+ .setLoggerName(JmsAppenderTest.class.getName())
+ .setLoggerFqcn(JmsAppenderTest.class.getName())
+ .setLevel(Level.INFO)
+ .setMessage(message)
+ .build();
+ // @formatter:on
+ }
+
+ private Log4jLogEvent createMapMessageLogEvent() {
+ final StringMapMessage mapMessage = new StringMapMessage();
+ return createLogEvent(mapMessage.with("testMesage", LOG_MESSAGE));
+ }
+
+ @Before
+ public void setUp() throws Exception {
+ // we have 4 appenders all connecting to the same ConnectionFactory
+ then(connection).should(times(4)).start();
+ }
+
+ @Test
+ public void testAppendToQueue() throws Exception {
+ final JmsAppender appender = (JmsAppender) ctx.getRequiredAppender("JmsAppender");
+ final LogEvent event = createLogEvent();
+ appender.append(event);
+ then(session).should().createTextMessage(eq(LOG_MESSAGE));
+ then(textMessage).should().setJMSTimestamp(anyLong());
+ then(messageProducer).should().send(textMessage);
+ appender.stop();
+ then(session).should().close();
+ then(connection).should().close();
+ }
+
+ @Test
+ public void testAppendToQueueWithMessageLayout() throws Exception {
+ final JmsAppender appender = (JmsAppender) ctx.getRequiredAppender("JmsAppender-MessageLayout");
+ final LogEvent event = createMapMessageLogEvent();
+ appender.append(event);
+ then(session).should().createMapMessage();
+ then(mapMessage).should().setJMSTimestamp(anyLong());
+ then(messageProducerMl).should().send(mapMessage);
+ appender.stop();
+ then(session).should().close();
+ then(connection).should().close();
+ }
+
+ @Test
+ public void testJmsQueueAppenderCompatibility() throws Exception {
+ final JmsAppender appender = (JmsAppender) ctx.getRequiredAppender("JmsQueueAppender");
+ final LogEvent expected = createLogEvent();
+ appender.append(expected);
+ then(session).should().createObjectMessage(eq(expected));
+ then(objectMessage).should().setJMSTimestamp(anyLong());
+ then(messageProducer).should().send(objectMessage);
+ appender.stop();
+ then(session).should().close();
+ then(connection).should().close();
+ }
+
+ @Test
+ public void testJmsTopicAppenderCompatibility() throws Exception {
+ final JmsAppender appender = (JmsAppender) ctx.getRequiredAppender("JmsTopicAppender");
+ final LogEvent expected = createLogEvent();
+ appender.append(expected);
+ then(session).should().createObjectMessage(eq(expected));
+ then(objectMessage).should().setJMSTimestamp(anyLong());
+ then(messageProducer).should().send(objectMessage);
+ appender.stop();
+ then(session).should().close();
+ then(connection).should().close();
+ }
+}
diff --git a/log4j-jakarta-jms/src/test/resources/JmsJakartaAppenderTest.xml b/log4j-jakarta-jms/src/test/resources/JmsJakartaAppenderTest.xml
new file mode 100644
index 00000000000..e1d660d1c17
--- /dev/null
+++ b/log4j-jakarta-jms/src/test/resources/JmsJakartaAppenderTest.xml
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pom.xml b/pom.xml
index cc0fed3561b..74394de40c6 100644
--- a/pom.xml
+++ b/pom.xml
@@ -249,6 +249,7 @@
log4j-docker
log4j-fuzz-test
log4j-iostreams
+ log4j-jakarta-jms
log4j-jakarta-smtp
log4j-jakarta-web
log4j-jcl
@@ -446,6 +447,12 @@
${project.version}
+
+ org.apache.logging.log4j
+ log4j-jakarta-jms
+ ${project.version}
+
+
org.apache.logging.log4j
log4j-jakarta-smtp
diff --git a/src/changelog/.2.x.x/2295_add_JMS_Jakarta_Appender.xml b/src/changelog/.2.x.x/2295_add_JMS_Jakarta_Appender.xml
new file mode 100644
index 00000000000..fd40164e1b8
--- /dev/null
+++ b/src/changelog/.2.x.x/2295_add_JMS_Jakarta_Appender.xml
@@ -0,0 +1,8 @@
+
+
+
+ Add a Jakarta-based JMS Appender module log4j-jakarta-jms and deprecate the Javax version.
+
diff --git a/src/site/antora/modules/ROOT/pages/manual/appenders/message-queue.adoc b/src/site/antora/modules/ROOT/pages/manual/appenders/message-queue.adoc
index 3adb9f8b90d..d032c53de30 100644
--- a/src/site/antora/modules/ROOT/pages/manual/appenders/message-queue.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/appenders/message-queue.adoc
@@ -423,8 +423,8 @@ This example cannot be configured using Java properties.
[#JmsAppender]
== JMS Appender
-The JMS Appender sends the formatted log event to a
-https://jakarta.ee/specifications/messaging/2.0/[Jakarta Messaging API]
+The JMS Appender sends a formatted log event to a
+https://jakarta.ee/specifications/messaging/3.0/[Jakarta] or https://jakarta.ee/specifications/messaging/2.0/[Javax] Messaging API
destination.
[IMPORTANT]
@@ -432,10 +432,12 @@ destination.
As of Log4j `2.17.0` you need to enable the JMS Appender **explicitly** by setting the
xref:manual/systemproperties.adoc#log4j2.enableJndiJms[`log4j2.enableJndiJms`]
configuration property to `true`.
-
-Due to breaking changes in the underlying API, the JMS Appender cannot be used with Jakarta Messaging API 3.0 or later.
====
+For Jakarta, use the `JMS-Jakarta` element name in the `log4j-jakarta-jms` Maven module.
+
+For Javax, use the `JMS-Javax` element name; the names `JMS`, `JMSQueue`, and `JMSTopic` are provided for backward compatibility.
+
[#JmsAppender-attributes]
.JMS Appender configuration attributes
[cols="1m,1,1,5"]
@@ -601,6 +603,8 @@ https://jakarta.ee/specifications/platform/8/apidocs/javax/jms/objectmessage[`Ob
[#JmsAppender-examples]
=== Configuration examples
+In the examples below, to use Jakarta, replace `JMS` with `JMS-Jakarta`.
+
Here is a sample JMS Appender configuration:
[tabs]
diff --git a/src/site/antora/modules/ROOT/pages/manual/messages.adoc b/src/site/antora/modules/ROOT/pages/manual/messages.adoc
index 2e123a8022d..d678fe8e11a 100644
--- a/src/site/antora/modules/ROOT/pages/manual/messages.adoc
+++ b/src/site/antora/modules/ROOT/pages/manual/messages.adoc
@@ -233,7 +233,7 @@ It supports following formats:
Some appenders handle ``MapMessage``s differently when there is no layout:
-* JMS Appender converts to a JMS `javax.jms.MapMessage`
+* JMS Appender converts to a JMS `javax.jms.MapMessage` or `jakarta.jms.MapMessage`
* xref:manual/appenders/database.adoc#JdbcAppender[JDBC Appender] converts to values in an `SQL INSERT` statement
* xref:manual/appenders/database.adoc#MongoDbProvider[MongoDB NoSQL provider] converts to fields in a MongoDB object