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

rmaucher pushed a commit to branch 10.1.x
in repository https://gitbox.apache.org/repos/asf/tomcat.git

commit 0e8d55b7ab40d6b7d48a0bde4bc11e86412d1358
Author: opencode <[email protected]>
AuthorDate: Sun Oct 25 22:00:37 2026 +0100

    Cache the connection proxy constructor per kind, not once per pool
    
    The single cached constructor made the xa flag of getProxyConstructor()
    meaningless after the first proxied connection. A pool whose backing
    data source is swapped at runtime then proxied later connections of the
    wrong kind: XA-backed connections lost the XAConnection interface, and
    plain connections falsely claimed it and threw NullPointerException on
    the first XA method call. Keep one cached constructor per kind and let
    each borrow instantiate from the constructor matching its own physical
    connection.
---
 .../apache/tomcat/jdbc/pool/ConnectionPool.java    |  38 +++--
 .../pool/TestProxyKindAfterDataSourceSwap.java     | 155 +++++++++++++++++++++
 webapps/docs/changelog.xml                         |   9 ++
 3 files changed, 191 insertions(+), 11 deletions(-)

diff --git 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
index a4699e37e2..5202bc93e9 100644
--- 
a/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
+++ 
b/modules/jdbc-pool/src/main/java/org/apache/tomcat/jdbc/pool/ConnectionPool.java
@@ -108,9 +108,17 @@ public class ConnectionPool {
 
     /**
      * Since newProxyInstance performs the same operation, over and over
-     * again, it is much more optimized if we simply store the constructor 
ourselves.
+     * again, it is much more optimized if we simply store the constructor
+     * ourselves. One constructor is cached per proxy kind (with and without
+     * the XAConnection interface), so that a pool whose backing data source
+     * changes kind at runtime keeps proxying each connection correctly.
      */
-    private Constructor<?> proxyClassConstructor;
+    private volatile Constructor<?> proxyClassConstructor;
+
+    /**
+     * Cached constructor for proxies that also implement XAConnection.
+     */
+    private volatile Constructor<?> xaProxyClassConstructor;
 
     /**
      * Executor service used to cancel Futures
@@ -344,14 +352,14 @@ public class ConnectionPool {
             handler = new StatementFacade(handler);
         }
         try {
-            getProxyConstructor(con.getXAConnection() != null);
+            Constructor<?> proxyConstructor = 
getProxyConstructor(con.getXAConnection() != null);
             //create the proxy
             //TODO possible optimization, keep track if this connection was 
returned properly, and don't generate a new facade
             Connection connection = null;
             if (getPoolProperties().getUseDisposableConnectionFacade() ) {
-                connection = (Connection)proxyClassConstructor.newInstance(new 
Object[] { new DisposableConnectionFacade(handler) });
+                connection = (Connection)proxyConstructor.newInstance(new 
Object[] { new DisposableConnectionFacade(handler) });
             } else {
-                connection = (Connection)proxyClassConstructor.newInstance(new 
Object[] {handler});
+                connection = (Connection)proxyConstructor.newInstance(new 
Object[] {handler});
             }
             //return the connection
             return connection;
@@ -383,8 +391,10 @@ public class ConnectionPool {
     }
 
     /**
-     * Creates and caches a {@link java.lang.reflect.Constructor} used to 
instantiate the proxy object.
-     * We cache this, since the creation of a constructor is fairly slow.
+     * Creates and caches a {@link java.lang.reflect.Constructor} used to
+     * instantiate the proxy object of the requested kind (with or without
+     * the XAConnection interface). We cache these, since the creation of a
+     * constructor is fairly slow.
      * @param xa Use a XA connection
      * @return constructor used to instantiate the wrapper object
      * @throws NoSuchMethodException Failed to get a constructor
@@ -397,16 +407,22 @@ public class ConnectionPool {
      */
     @SuppressWarnings("deprecation")
     public Constructor<?> getProxyConstructor(boolean xa) throws 
NoSuchMethodException {
-        //cache the constructor
-        if (proxyClassConstructor == null ) {
+        //cache the constructor per proxy kind
+        Constructor<?> constructor = xa ? xaProxyClassConstructor : 
proxyClassConstructor;
+        if (constructor == null) {
             Class<?> proxyClass = xa ?
                     Proxy.getProxyClass(ConnectionPool.class.getClassLoader(),
                             new Class[] {Connection.class, 
javax.sql.PooledConnection.class, XAConnection.class}) :
                     Proxy.getProxyClass(ConnectionPool.class.getClassLoader(),
                             new Class[] {Connection.class, 
javax.sql.PooledConnection.class});
-            proxyClassConstructor = proxyClass.getConstructor(new Class[] { 
InvocationHandler.class });
+            constructor = proxyClass.getConstructor(new Class[] { 
InvocationHandler.class });
+            if (xa) {
+                xaProxyClassConstructor = constructor;
+            } else {
+                proxyClassConstructor = constructor;
+            }
         }
-        return proxyClassConstructor;
+        return constructor;
     }
 
     /**
diff --git 
a/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestProxyKindAfterDataSourceSwap.java
 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestProxyKindAfterDataSourceSwap.java
new file mode 100644
index 0000000000..93e70bf019
--- /dev/null
+++ 
b/modules/jdbc-pool/src/test/java/org/apache/tomcat/jdbc/pool/TestProxyKindAfterDataSourceSwap.java
@@ -0,0 +1,155 @@
+/*
+ *  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.tomcat.jdbc.pool;
+
+import java.io.PrintWriter;
+import java.sql.Connection;
+import java.sql.DriverManager;
+import java.sql.SQLException;
+import java.sql.SQLFeatureNotSupportedException;
+import java.util.logging.Logger;
+
+import javax.sql.XAConnection;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import org.junit.jupiter.api.Test;
+
+import org.apache.tomcat.jdbc.JdbcPoolBaseTest;
+
+/**
+ * The proxy class cached for building connection facades must be selected
+ * per connection kind. Swapping the backing data source of a live pool
+ * (DataSourceProxy.setDataSource) can make later connections a different
+ * kind than the first one; they must neither lose the XAConnection
+ * capability nor claim it without an XA connection behind them.
+ */
+class TestProxyKindAfterDataSourceSwap extends JdbcPoolBaseTest {
+
+    /* A plain, non-XA javax.sql.DataSource for H2. */
+    private static final class PlainDataSource implements javax.sql.DataSource 
{
+
+        private static final long serialVersionUID = 1L;
+
+        private final String url;
+
+        PlainDataSource(String url) {
+            this.url = url;
+        }
+
+        @Override
+        public Connection getConnection() throws SQLException {
+            return DriverManager.getConnection(url, "sa", "");
+        }
+
+        @Override
+        public Connection getConnection(String user, String password)
+                throws SQLException {
+            return DriverManager.getConnection(url, user, password);
+        }
+
+        @Override
+        public PrintWriter getLogWriter() {
+            return null;
+        }
+
+        @Override
+        public void setLogWriter(PrintWriter out) {
+            // no logging
+        }
+
+        @Override
+        public void setLoginTimeout(int seconds) {
+            // no timeout
+        }
+
+        @Override
+        public int getLoginTimeout() {
+            return 0;
+        }
+
+        @Override
+        public Logger getParentLogger() throws SQLFeatureNotSupportedException 
{
+            throw new SQLFeatureNotSupportedException();
+        }
+
+        @Override
+        public <T> T unwrap(Class<T> iface) throws SQLException {
+            throw new SQLException("not a wrapper");
+        }
+
+        @Override
+        public boolean isWrapperFor(Class<?> iface) {
+            return false;
+        }
+    }
+
+    private org.h2.jdbcx.JdbcDataSource newH2XaDataSource() {
+        org.h2.jdbcx.JdbcDataSource inner = new org.h2.jdbcx.JdbcDataSource();
+        inner.setUrl(getUrl());
+        inner.setUser("sa");
+        inner.setPassword("");
+        return inner;
+    }
+
+    @Test
+    void connectionsCreatedAfterSwapToXaExposeXaCapability() throws Exception {
+        DataSource ds = newDataSource();
+        // hold the only pooled connection so the next borrow must create a
+        // fresh physical connection through the swapped data source
+        Connection held = ds.getConnection();
+
+        ds.setDataSource(newH2XaDataSource());
+        try (Connection c2 = ds.getConnection()) {
+            assertTrue(c2 instanceof XAConnection,
+                    "a connection created through an XA data source must be "
+                            + "proxied as an XAConnection even though the "
+                            + "pool started out non-XA");
+            assertNotNull(((XAConnection) c2).getXAResource(),
+                    "the XA dispatch must reach the real XA connection");
+        }
+        held.close();
+    }
+
+    @Test
+    void connectionsCreatedAfterSwapToNonXaDoNotClaimXaCapability()
+            throws Exception {
+        XADataSource ds = manage(new XADataSource());
+        ds.setDriverClassName("org.h2.Driver");
+        ds.getPoolProperties().setJmxEnabled(false);
+        ds.getPoolProperties().setInitialSize(1);
+        ds.getPoolProperties().setMinIdle(0);
+        ds.getPoolProperties().setDataSource(newH2XaDataSource());
+        // first borrow caches the proxy class for an XA connection
+        Connection held = ds.getConnection();
+
+        ds.setDataSource(new PlainDataSource(getUrl()));
+        try (Connection c2 = ds.getConnection()) {
+            assertFalse(c2 instanceof XAConnection,
+                    "a connection created through a plain data source must "
+                            + "not be proxied as an XAConnection just "
+                            + "because the pool started out XA");
+        }
+        assertThrows(SQLException.class, ds::getXAConnection,
+                "the XA data source facade must refuse to hand out a "
+                        + "connection that no longer has an XA connection "
+                        + "behind it");
+        held.close();
+    }
+}
diff --git a/webapps/docs/changelog.xml b/webapps/docs/changelog.xml
index 8fe874a73b..8f1fbfb189 100644
--- a/webapps/docs/changelog.xml
+++ b/webapps/docs/changelog.xml
@@ -770,6 +770,15 @@
         transaction. These options now act on the actual transaction state
         of the connection being returned. (remm)
       </fix>
+      <fix>
+        Correct a defect in the JDBC connection pool where the cached proxy
+        constructor ignored the connection kind after the first proxy was
+        created, so swapping the data source backing a running pool could
+        proxy later connections of the wrong kind: connections backed by an
+        XA connection no longer implemented <code>XAConnection</code>, and
+        plain connections falsely did, with XA method calls on them failing
+        with a NullPointerException. (remm)
+      </fix>
     </changelog>
   </subsection>
   <subsection name="Other">


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to