You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@logging.apache.org by gg...@apache.org on 2024/12/07 21:02:50 UTC

(logging-log4j2) branch 2.x updated: Add a Jakarta JMS Appender #2295 (#3247)

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

ggregory pushed a commit to branch 2.x
in repository https://gitbox.apache.org/repos/asf/logging-log4j2.git


The following commit(s) were added to refs/heads/2.x by this push:
     new 43a0e29781 Add a Jakarta JMS Appender #2295 (#3247)
43a0e29781 is described below

commit 43a0e29781d1ca0be9306124d360ab451e531ddd
Author: Gary Gregory <ga...@users.noreply.github.com>
AuthorDate: Sat Dec 7 16:02:44 2024 -0500

    Add a Jakarta JMS Appender #2295 (#3247)
---
 .github/dependabot.yaml                            |   1 +
 .../log4j/core/appender/mom/JmsAppender.java       |  11 +-
 .../log4j/core/appender/mom/JmsManager.java        |   6 +-
 .../log4j/core/appender/mom/package-info.java      |   2 +-
 .../.log4j-plugin-processing-activator             |   1 +
 log4j-jakarta-jms/pom.xml                          |  71 ++++++++
 .../core/appender/mom/jakarta}/JmsAppender.java    |  60 ++-----
 .../core/appender/mom/jakarta}/JmsManager.java     |  51 +++---
 .../core/appender/mom/jakarta}/package-info.java   |   8 +-
 .../core/appender/mom/jakarta/JmsAppenderTest.java | 196 +++++++++++++++++++++
 .../src/test/resources/JmsJakartaAppenderTest.xml  |  46 +++++
 pom.xml                                            |   7 +
 .../.2.x.x/2295_add_JMS_Jakarta_Appender.xml       |   8 +
 .../ROOT/pages/manual/appenders/message-queue.adoc |  12 +-
 .../antora/modules/ROOT/pages/manual/messages.adoc |   2 +-
 15 files changed, 385 insertions(+), 97 deletions(-)

diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml
index ee3d096332..6471296876 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 8d2c87c4b8..b6df7dd08d 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.config.plugins.validation.constraints.Requi
 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<B extends Builder<B>> extends AbstractAppender.Builder<B>
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 3d2872a9fd..178d1f428a 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 @@ import org.apache.logging.log4j.status.StatusLogger;
  * Consider this class <b>private</b>; it is only <b>public</b> for access by integration tests.
  *
  * <p>
- * 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.
  * </p>
+ *
+ * @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 6ed35287fd..c12d834375 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 0000000000..ba133f3696
--- /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 0000000000..57b7b2c14b
--- /dev/null
+++ b/log4j-jakarta-jms/pom.xml
@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+  <parent>
+    <groupId>org.apache.logging.log4j</groupId>
+    <artifactId>log4j</artifactId>
+    <version>${revision}</version>
+    <relativePath>../log4j-parent</relativePath>
+  </parent>
+  <artifactId>log4j-jakarta-jms</artifactId>
+  <name>Apache Log4j Jakarta JMS</name>
+  <description>Apache Log4j Java Message Service (JMS), version for Jakarta.</description>
+  <properties>
+    <!-- This is a new module in 2.25.0, remove this property after the release. -->
+    <bnd.baseline.skip>true</bnd.baseline.skip>
+    <!-- OSGi and JPMS options -->
+    <bnd-module-name>org.apache.logging.log4j.jakarta.jms</bnd-module-name>
+    <Fragment-Host>org.apache.logging.log4j.core</Fragment-Host>
+  </properties>
+  <dependencies>
+    <dependency>
+      <groupId>jakarta.jms</groupId>
+      <artifactId>jakarta.jms-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-api</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core</artifactId>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.vintage</groupId>
+      <artifactId>junit-vintage-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.junit.jupiter</groupId>
+      <artifactId>junit-jupiter-engine</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>org.apache.logging.log4j</groupId>
+      <artifactId>log4j-core-test</artifactId>
+      <scope>test</scope>
+    </dependency>
+    <dependency>
+      <groupId>commons-logging</groupId>
+      <artifactId>commons-logging</artifactId>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+</project>
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java
similarity index 80%
copy from log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java
copy to log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java
index 8d2c87c4b8..661cfd0329 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsAppender.java
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsAppender.java
@@ -14,19 +14,19 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.appender.mom;
+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 javax.jms.JMSException;
 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.JmsManager.JmsManagerConfiguration;
+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;
@@ -37,14 +37,12 @@ import org.apache.logging.log4j.core.config.plugins.validation.constraints.Requi
 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.
+ * Jakarta JMS Appender plugin for both queues and topics.
  */
-@Plugin(name = "JMS", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
-@PluginAliases({"JMSQueue", "JMSTopic"})
-public class JmsAppender extends AbstractAppender {
+@Plugin(name = "JMS-Jakarta", category = Node.CATEGORY, elementType = Appender.ELEMENT_TYPE, printObject = true)
+public final class JmsAppender extends AbstractAppender {
 
-    public static class Builder<B extends Builder<B>> extends AbstractAppender.Builder<B>
+    public static final class Builder extends AbstractAppender.Builder<Builder>
             implements org.apache.logging.log4j.core.util.Builder<JmsAppender> {
 
         public static final int DEFAULT_RECONNECT_INTERVAL_MILLIS = 5000;
@@ -65,12 +63,12 @@ public class JmsAppender extends AbstractAppender {
         private String securityCredentials;
 
         @PluginBuilderAttribute
-        @Required(message = "A javax.jms.ConnectionFactory JNDI name must be specified")
+        @Required(message = "A jakarta.jms.ConnectionFactory JNDI name must be specified")
         private String factoryBindingName;
 
         @PluginBuilderAttribute
         @PluginAliases({"queueBindingName", "topicBindingName"})
-        @Required(message = "A javax.jms.Destination JNDI name must be specified")
+        @Required(message = "A jakarta.jms.Destination JNDI name must be specified")
         private String destinationBindingName;
 
         @PluginBuilderAttribute
@@ -104,7 +102,7 @@ public class JmsAppender extends AbstractAppender {
                         destinationBindingName,
                         userName,
                         password,
-                        false,
+                        immediateFail,
                         reconnectIntervalMillis);
                 actualJmsManager = AbstractManager.getManager(getName(), JmsManager.FACTORY, configuration);
             }
@@ -156,15 +154,6 @@ public class JmsAppender extends AbstractAppender {
             return this;
         }
 
-        /**
-         * @deprecated Use setPassword(char[])
-         */
-        @Deprecated
-        public Builder setPassword(final String password) {
-            this.password = password == null ? null : password.toCharArray();
-            return this;
-        }
-
         public Builder setProviderUrl(final String providerUrl) {
             this.providerUrl = providerUrl;
             return this;
@@ -190,15 +179,6 @@ public class JmsAppender extends AbstractAppender {
             return this;
         }
 
-        /**
-         * @deprecated Use {@link #setUserName(String)}.
-         */
-        @Deprecated
-        public Builder setUsername(final String username) {
-            this.userName = username;
-            return this;
-        }
-
         public Builder setUserName(final String userName) {
             this.userName = userName;
             return this;
@@ -230,7 +210,7 @@ public class JmsAppender extends AbstractAppender {
      *
      * @throws JMSException not thrown as of 2.9 but retained in the signature for compatibility, will be removed in 3.0
      */
-    protected JmsAppender(
+    private JmsAppender(
             final String name,
             final Filter filter,
             final Layout<? extends Serializable> layout,
@@ -242,24 +222,6 @@ public class JmsAppender extends AbstractAppender {
         this.manager = 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
-     * @deprecated Use {@link #JmsAppender(String, Filter, Layout, boolean, Property[], JmsManager)}.
-     */
-    @Deprecated
-    protected JmsAppender(
-            final String name,
-            final Filter filter,
-            final Layout<? extends Serializable> layout,
-            final boolean ignoreExceptions,
-            final JmsManager manager)
-            throws JMSException {
-        super(name, filter, layout, ignoreExceptions, Property.EMPTY_ARRAY);
-        this.manager = manager;
-    }
-
     @Override
     public void append(final LogEvent event) {
         this.manager.send(event, toSerializable(event));
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java
similarity index 92%
copy from log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java
copy to log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java
index 3d2872a9fd..81b915dafe 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/JmsManager.java
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/JmsManager.java
@@ -14,21 +14,20 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package org.apache.logging.log4j.core.appender.mom;
-
+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.jms.Connection;
-import javax.jms.ConnectionFactory;
-import javax.jms.Destination;
-import javax.jms.JMSException;
-import javax.jms.MapMessage;
-import javax.jms.Message;
-import javax.jms.MessageConsumer;
-import javax.jms.MessageProducer;
-import javax.jms.Session;
 import javax.naming.NamingException;
 import org.apache.logging.log4j.core.LogEvent;
 import org.apache.logging.log4j.core.appender.AbstractManager;
@@ -46,9 +45,9 @@ import org.apache.logging.log4j.status.StatusLogger;
  * involving a configured ConnectionFactory and Destination.
  * </p>
  */
-public class JmsManager extends AbstractManager {
+public final class JmsManager extends AbstractManager {
 
-    public static class JmsManagerConfiguration {
+    public static final class JmsManagerConfiguration {
         private final Properties jndiProperties;
         private final String connectionFactoryName;
         private final String destinationName;
@@ -121,7 +120,7 @@ public class JmsManager extends AbstractManager {
         }
     }
 
-    private static class JmsManagerFactory implements ManagerFactory<JmsManager, JmsManagerConfiguration> {
+    private static final class JmsManagerFactory implements ManagerFactory<JmsManager, JmsManagerConfiguration> {
 
         @Override
         public JmsManager createManager(final String name, final JmsManagerConfiguration data) {
@@ -378,13 +377,13 @@ public class JmsManager extends AbstractManager {
      * @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.
      */
-    public Message createMessage(final Serializable object) throws JMSException {
+    private Message createMessage(final Serializable object) throws JMSException {
         if (object instanceof String) {
-            return this.session.createTextMessage((String) object);
+            return session.createTextMessage((String) object);
         } else if (object instanceof org.apache.logging.log4j.message.MapMessage) {
-            return map((org.apache.logging.log4j.message.MapMessage<?, ?>) object, this.session.createMapMessage());
+            return map((org.apache.logging.log4j.message.MapMessage<?, ?>) object, session.createMapMessage());
         }
-        return this.session.createObjectMessage(object);
+        return session.createObjectMessage(object);
     }
 
     private void createMessageAndSend(final LogEvent event, final Serializable serializable) throws JMSException {
@@ -393,16 +392,6 @@ public class JmsManager extends AbstractManager {
         messageProducer.send(message);
     }
 
-    /**
-     * Creates a MessageConsumer on this Destination using the current Session.
-     *
-     * @return A MessageConsumer on this Destination.
-     * @throws JMSException if the session fails to create a consumer due to some internal error.
-     */
-    public MessageConsumer createMessageConsumer() throws JMSException {
-        return this.session.createConsumer(this.destination);
-    }
-
     /**
      * Creates a MessageProducer on this Destination using the current Session.
      *
@@ -413,7 +402,7 @@ public class JmsManager extends AbstractManager {
      * @return A MessageProducer on this Destination.
      * @throws JMSException if the session fails to create a MessageProducer due to some internal error.
      */
-    public MessageProducer createMessageProducer(final Session session, final Destination destination)
+    private MessageProducer createMessageProducer(final Session session, final Destination destination)
             throws JMSException {
         return session.createProducer(destination);
     }
@@ -438,7 +427,7 @@ public class JmsManager extends AbstractManager {
     }
 
     <T> T lookup(final String destinationName) throws NamingException {
-        return this.jndiManager.lookup(destinationName);
+        return jndiManager.lookup(destinationName);
     }
 
     private MapMessage map(
@@ -470,7 +459,7 @@ public class JmsManager extends AbstractManager {
         closed &= closeMessageProducer();
         closed &= closeSession();
         closed &= closeConnection();
-        return closed && this.jndiManager.stop(timeout, timeUnit);
+        return closed && jndiManager.stop(timeout, timeUnit);
     }
 
     void send(final LogEvent event, final Serializable serializable) {
diff --git a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java
similarity index 87%
copy from log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java
copy to log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java
index 6ed35287fd..a5651b6275 100644
--- a/log4j-core/src/main/java/org/apache/logging/log4j/core/appender/mom/package-info.java
+++ b/log4j-jakarta-jms/src/main/java/org/apache/logging/log4j/core/appender/mom/jakarta/package-info.java
@@ -16,13 +16,11 @@
  */
 
 /**
- * Message oriented middleware appenders.
- *
- * @since 2.1
+ * Jakarta-based JMS Appender.
  */
 @Export
-@Version("2.20.1")
-package org.apache.logging.log4j.core.appender.mom;
+@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 0000000000..5d730fc47b
--- /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<String, Object> createBindings() {
+        final ConcurrentHashMap<String, Object> 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 0000000000..e1d660d1c1
--- /dev/null
+++ b/log4j-jakarta-jms/src/test/resources/JmsJakartaAppenderTest.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  -->
+<Configuration name="JmsAppenderTest" status="OFF">
+  <Appenders>
+    <JMS-Jakarta name="JmsAppender"
+         factoryBindingName="jms/connectionFactory"
+         destinationBindingName="jms/destination">
+      <PatternLayout pattern="%m"/>
+    </JMS-Jakarta>
+    <JMS-Jakarta name="JmsAppender-MessageLayout"
+         factoryBindingName="jms/connectionFactory"
+         destinationBindingName="jms/destination-ml">
+      <MessageLayout />
+    </JMS-Jakarta>
+    <JMS-Jakarta name="JmsQueueAppender"
+              factoryBindingName="jms/queues"
+              queueBindingName="jms/queue">
+      <SerializedLayout/>
+    </JMS-Jakarta>
+    <JMS-Jakarta name="JmsTopicAppender"
+              factoryBindingName="jms/topics"
+              topicBindingName="jms/topic">
+      <SerializedLayout/>
+    </JMS-Jakarta>
+  </Appenders>
+  <Loggers>
+    <Root level="info">
+      <AppenderRef ref="JmsQueueAppender"/>
+    </Root>
+  </Loggers>
+</Configuration>
diff --git a/pom.xml b/pom.xml
index cc0fed3561..74394de40c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -249,6 +249,7 @@
     <module>log4j-docker</module>
     <module>log4j-fuzz-test</module>
     <module>log4j-iostreams</module>
+    <module>log4j-jakarta-jms</module>
     <module>log4j-jakarta-smtp</module>
     <module>log4j-jakarta-web</module>
     <module>log4j-jcl</module>
@@ -446,6 +447,12 @@
         <version>${project.version}</version>
       </dependency>
 
+      <dependency>
+        <groupId>org.apache.logging.log4j</groupId>
+        <artifactId>log4j-jakarta-jms</artifactId>
+        <version>${project.version}</version>
+      </dependency>
+
       <dependency>
         <groupId>org.apache.logging.log4j</groupId>
         <artifactId>log4j-jakarta-smtp</artifactId>
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 0000000000..fd40164e1b
--- /dev/null
+++ b/src/changelog/.2.x.x/2295_add_JMS_Jakarta_Appender.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<entry xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xmlns="https://logging.apache.org/xml/ns"
+       xsi:schemaLocation="https://logging.apache.org/xml/ns https://logging.apache.org/xml/ns/log4j-changelog-0.xsd"
+       type="fixed">
+  <issue id="2995" link="https://github.com/apache/logging-log4j2/issues/2995"/>
+  <description format="asciidoc">Add a Jakarta-based JMS Appender module log4j-jakarta-jms and deprecate the Javax version.</description>
+</entry>
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 3adb9f8b90..d032c53de3 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 2e123a8022..d678fe8e11 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