From 0d16f10774cdc7bc96bfea7f007c7823e650795d Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Tue, 01 Sep 2026 13:47:43 +0000
Subject: [PATCH] [#879] Skip the validation of a pooled JDBC connection returned a moment ago (#883)
---
opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java | 422 +++++++++++++++++++++++++++++++++++++++++++++++++++-
1 files changed, 413 insertions(+), 9 deletions(-)
diff --git a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
index 0caad17..c98f280 100644
--- a/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
+++ b/opendj-server-legacy/src/test/java/org/opends/server/backends/jdbc/CachedConnectionTestCase.java
@@ -19,8 +19,14 @@
import org.testng.annotations.AfterClass;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeClass;
+import org.testng.annotations.BeforeMethod;
+import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.reflect.Field;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.sql.Connection;
@@ -52,11 +58,13 @@
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNotNull;
+import static org.testng.Assert.assertNotSame;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertSame;
import static org.testng.Assert.assertTrue;
@@ -83,6 +91,9 @@
private final StubDriver stub = new StubDriver();
+ /** The window as this JVM was started with it, put back after every test that varies it. */
+ private static final long CONFIGURED_ALIVE_BYPASS_NANOS = CachedConnection.aliveBypassNanos;
+
@BeforeClass
public void registerStubDriver() throws Exception {
DriverManager.registerDriver(stub);
@@ -93,13 +104,26 @@
DriverManager.deregisterDriver(stub);
}
+ /**
+ * Most of the tests below seed the pool by hand, and a connection built a moment ago is inside
+ * the alive window - they are about what the validation of a borrow does, so the window is off
+ * unless the test at hand is one of the window's own.
+ */
+ @BeforeMethod
+ public void validateEveryBorrow() {
+ CachedConnection.aliveBypassNanos = 0;
+ }
+
@AfterMethod
public void clearProperties() {
System.clearProperty(CachedConnection.CONNECT_TIMEOUT_PROPERTY);
System.clearProperty(CachedConnection.POOL_TIMEOUT_PROPERTY);
+ System.clearProperty(CachedConnection.TTL_PROPERTY);
+ System.clearProperty(CachedConnection.ALIVE_BYPASS_PROPERTY);
// what has been reported once is remembered for the life of the jvm: left standing, the key
// of one test is what the next one finds when it asserts that it reported something itself
CachedConnection.warnedOnce.clear();
+ CachedConnection.aliveBypassNanos = CONFIGURED_ALIVE_BYPASS_NANOS;
}
/**
@@ -546,7 +570,7 @@
final Connection pooled = mock(Connection.class);
when(pooled.isValid(anyInt())).thenReturn(true);
when(pooled.getNetworkTimeout()).thenReturn(0);
- CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
+ seedPool(url, pooled);
final Connection borrowed = CachedConnection.getConnection(url);
@@ -572,7 +596,7 @@
when(pooled.isValid(anyInt())).thenReturn(true);
doThrow(new SQLException("the driver took the bound and then failed"))
.when(pooled).setNetworkTimeout(any(Executor.class), anyInt());
- CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
+ seedPool(url, pooled);
final Connection fresh = mock(Connection.class);
stub.answerWith(fresh);
@@ -615,7 +639,7 @@
final Connection pooled = mock(Connection.class);
when(pooled.isValid(anyInt())).thenReturn(true);
when(pooled.getNetworkTimeout()).thenReturn(2000);
- CachedConnection.cached.get(url).add(new CachedConnection(url, pooled));
+ seedPool(url, pooled);
assertNotNull(CachedConnection.getConnection(url));
@@ -639,7 +663,7 @@
Thread.sleep(500); // a database that no longer answers: every validation waits out its bound
return false;
});
- CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
+ seedPool(url, stale);
}
final Connection fresh = mock(Connection.class);
stub.answerWith(fresh);
@@ -658,7 +682,7 @@
final String url = StubDriver.PREFIX + "broken-pooled";
final Connection stale = mock(Connection.class);
when(stale.isValid(anyInt())).thenReturn(false);
- CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
+ seedPool(url, stale);
final Connection fresh = mock(Connection.class);
when(fresh.isValid(anyInt())).thenReturn(true);
stub.answerWith(fresh);
@@ -682,7 +706,7 @@
final String url = StubDriver.PREFIX + "validation-unchecked";
final Connection broken = mock(Connection.class);
when(broken.isValid(anyInt())).thenThrow(new IllegalStateException("driver internal"));
- CachedConnection.cached.get(url).add(new CachedConnection(url, broken));
+ seedPool(url, broken);
final Connection fresh = mock(Connection.class);
stub.answerWith(fresh);
@@ -1273,8 +1297,7 @@
});
final Connection good = mock(Connection.class);
when(good.isValid(anyInt())).thenReturn(true);
- CachedConnection.cached.get(url).add(new CachedConnection(url, stale));
- CachedConnection.cached.get(url).add(new CachedConnection(url, good));
+ seedPool(url, stale, good);
final Connection fresh = mock(Connection.class);
stub.answerWith(fresh);
System.setProperty(CachedConnection.POOL_TIMEOUT_PROPERTY, "1");
@@ -1300,7 +1323,7 @@
final Connection reaped = mock(Connection.class);
when(reaped.getNetworkTimeout()).thenReturn(0);
when(reaped.isValid(anyInt())).thenReturn(false);
- CachedConnection.cached.get(url).add(new CachedConnection(url, reaped));
+ seedPool(url, reaped);
final Connection fresh = mock(Connection.class);
stub.answerWith(fresh);
@@ -1333,6 +1356,387 @@
"a connection still carrying the read bound of its login went back into the pool");
}
+ /**
+ * The case the window exists for: the connection this borrow takes out answered the database a
+ * moment ago, and asking it again costs the round trip the operation came to make.
+ */
+ @Test(timeOut = 120000)
+ public void testAConnectionProvenAliveIsNotValidatedAgainWithinTheWindow() throws Exception {
+ final String url = StubDriver.PREFIX + "within-the-window";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+
+ final Connection first = CachedConnection.getConnection(url); // established: it has just answered
+ first.close();
+ final Connection second = CachedConnection.getConnection(url);
+
+ assertSame(second, first, "the pooled connection was not the one handed back");
+ assertEquals(stub.attempts.get(), 1, "the pool established a second connection");
+ verify(parent, never()).isValid(anyInt());
+ }
+
+ /** Past the window it is the connection the database or a firewall may have dropped meanwhile. */
+ @Test(timeOut = 120000)
+ public void testAConnectionIsValidatedAgainOnceTheWindowHasPassed() throws Exception {
+ final String url = StubDriver.PREFIX + "past-the-window";
+ CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+
+ final Connection first = CachedConnection.getConnection(url);
+ first.close();
+ Thread.sleep(20);
+ final Connection second = CachedConnection.getConnection(url);
+
+ assertSame(second, first);
+ verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ }
+
+ /** The window switched off validates every borrow, the way this pool did before it existed. */
+ @Test(timeOut = 120000)
+ public void testAWindowOfZeroValidatesEveryBorrow() throws Exception {
+ final String url = StubDriver.PREFIX + "window-of-zero";
+ CachedConnection.aliveBypassNanos = 0;
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+
+ final Connection first = CachedConnection.getConnection(url);
+ first.close();
+ final Connection second = CachedConnection.getConnection(url);
+
+ assertSame(second, first);
+ verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ }
+
+ /**
+ * A connection is trusted for the window that follows the last answer it gave, never for the
+ * window that follows its return to the pool. pgjdbc short-circuits both rollback() and
+ * commit() when the transaction state is IDLE, so a borrow that issued no statement - the open
+ * of a backend, a configuration change that leaves the base DNs alone, an import of nothing -
+ * puts a connection back without a byte reaching the server: stamping the return would mark a
+ * connection the database dropped meanwhile as the freshest one in the pool.
+ */
+ @Test(timeOut = 120000)
+ public void testTheReturnToThePoolIsNotTakenForProofOfLife() throws Exception {
+ final String url = StubDriver.PREFIX + "silent-return";
+ CachedConnection.aliveBypassNanos = TimeUnit.MILLISECONDS.toNanos(50);
+ final Connection dropped = mock(Connection.class);
+ when(dropped.isValid(anyInt())).thenReturn(false); // dropped while it was out of the pool
+ stub.answerWith(dropped);
+
+ final Connection borrowed = CachedConnection.getConnection(url);
+ Thread.sleep(80); // the answer of the login ages out of the window
+ borrowed.close(); // and the rollback of this return never leaves the driver
+ final Connection fresh = mock(Connection.class);
+ when(fresh.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(fresh);
+
+ final Connection next = CachedConnection.getConnection(url);
+
+ verify(dropped).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ verify(dropped).close();
+ assertSame(((CachedConnection) next).parent, fresh,
+ "a connection the database dropped was handed out on the strength of its return to the pool");
+ }
+
+ /**
+ * A proof taken while the database was going away does not outlive the distrust that reported it.
+ * <p>
+ * The stamp stands for the moment the connection was asked, not the moment its answer was filed:
+ * a validation is given {@link CachedConnection#VALIDATION_TIMEOUT_SECONDS}, and one that started
+ * before another operation reported a drop and returned after it would otherwise be the younger of
+ * the two. The connection would then be handed out unvalidated for the rest of the window - and
+ * LIFO puts it at the head of the deque, so it is the very one the next borrow takes - by the
+ * check that exists to stop exactly that.
+ */
+ @Test(timeOut = 120000)
+ public void testAProofTakenWhileTheDatabaseWentAwayIsNotTrusted() throws Exception {
+ final String url = StubDriver.PREFIX + "proof-across-a-drop";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1); // nothing here ages out of the window
+ final AtomicInteger validations = new AtomicInteger();
+ final Connection parent = mock(Connection.class);
+ // the database goes away while this validation is in flight: another operation of the backend
+ // reports the drop of its own connection before this one has answered
+ when(parent.isValid(anyInt())).thenAnswer(invocation -> {
+ CachedConnection.distrustPool(url);
+ validations.incrementAndGet();
+ return true;
+ });
+ stub.answerWith(parent);
+
+ CachedConnection.getConnection(url).close(); // established and returned, proven by its login
+ CachedConnection.distrustPool(url); // an operation reports a drop: what the pool holds predates it
+ CachedConnection.getConnection(url).close(); // validated, and a second drop is reported while it is
+
+ final Connection borrowed = CachedConnection.getConnection(url);
+
+ assertEquals(validations.get(), 2,
+ "a connection was trusted on a proof that started before the drop it is compared against");
+ assertSame(((CachedConnection) borrowed).parent, parent, "the connection answered and was still discarded");
+ }
+
+ /**
+ * The pool hands out the connection returned last. Without it the window would rarely apply: a
+ * connection reached only after a whole cycle of the pool has been idle far longer than it.
+ */
+ @Test(timeOut = 120000)
+ public void testTheConnectionReturnedLastIsBorrowedFirst() throws Exception {
+ final String url = StubDriver.PREFIX + "returned-last";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection older = mock(Connection.class);
+ when(older.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(older);
+ final Connection first = CachedConnection.getConnection(url);
+ final Connection newer = mock(Connection.class);
+ when(newer.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(newer);
+ final Connection second = CachedConnection.getConnection(url);
+ assertNotSame(second, first);
+ first.close();
+ second.close();
+
+ final Connection borrowed = CachedConnection.getConnection(url);
+
+ assertSame(((CachedConnection) borrowed).parent, newer, "the pool cycled round to its coldest connection");
+ }
+
+ /**
+ * Whatever dropped one connection - a restart, a failover, a network that went away - dropped
+ * every connection established before it, and a borrow inside the window asks the database
+ * nothing: so the operation that saw the failure tells the pool, and the rest of that
+ * generation is validated once before it is trusted again.
+ */
+ @Test(timeOut = 120000)
+ public void testThePoolIsValidatedAgainAfterTheDatabaseDroppedAConnection() throws Exception {
+ final String url = StubDriver.PREFIX + "distrusted-generation";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+ CachedConnection.getConnection(url).close();
+
+ CachedConnection.distrustPool(url);
+ final Connection next = CachedConnection.getConnection(url);
+ next.close();
+ CachedConnection.getConnection(url).close();
+
+ // once for the generation the drop condemned, and not again for the borrow behind it
+ verify(parent, times(1)).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ }
+
+ /**
+ * The whole of the trade the window makes, end to end: a connection the database dropped
+ * inside the window is handed out unvalidated - that is the cost - the operation it broke
+ * reports the drop, and from there the pool validates the generation the drop condemned
+ * instead of handing out the rest of it the same way.
+ */
+ @Test(timeOut = 120000)
+ public void testAConnectionDroppedInsideTheWindowIsHandedOutOnceAndThenValidated() throws Exception {
+ final String url = StubDriver.PREFIX + "dropped-inside-the-window";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+ CachedConnection.getConnection(url).close();
+
+ when(parent.isValid(anyInt())).thenReturn(false); // the database dropped it where it lay
+ final Connection dropped = CachedConnection.getConnection(url);
+ assertSame(((CachedConnection) dropped).parent, parent, "the pooled connection was not the one handed back");
+ verify(parent, never()).isValid(anyInt()); // handed out on the strength of its last answer
+
+ // the statement of the caller is where the drop surfaces, and the caller reports it
+ CachedConnection.distrustPool(url);
+ dropped.close();
+ final Connection fresh = mock(Connection.class);
+ when(fresh.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(fresh);
+
+ final Connection next = CachedConnection.getConnection(url);
+
+ verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ verify(parent).close();
+ assertSame(((CachedConnection) next).parent, fresh, "the rest of the generation was handed out unvalidated");
+ }
+
+ /**
+ * Seeds the pool the way {@link CachedConnection#close()} fills it - at the end a borrow takes
+ * from - so that the connection named first here is the one the next borrow gets.
+ */
+ private static void seedPool(String url, Connection... parents) {
+ for (int i = parents.length - 1; i >= 0; i--) {
+ CachedConnection.cached.get(url).addFirst(new CachedConnection(url, parents[i]));
+ }
+ }
+
+ /**
+ * The borrows nothing compensates a dropped connection on - the open of a backend, the removal
+ * of its files, the start of an import - ask for a connection the pool validates whatever the
+ * window says. Each of them is one borrow of a cold path, and the one that opens a backend
+ * issues no statement at all: a connection dropped inside the window would surface there out of
+ * the rollback that releases it, with no statement to replay and nothing to tell the pool.
+ */
+ @Test(timeOut = 120000)
+ public void testTheBorrowsNothingCompensatesAreValidated() throws Exception {
+ final String url = StubDriver.PREFIX + "validated-borrow";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+ CachedConnection.getConnection(url).close();
+
+ final Connection borrowed = CachedConnection.getConnection(url, false);
+
+ assertSame(((CachedConnection) borrowed).parent, parent, "the pooled connection was not the one handed back");
+ verify(parent).isValid(CachedConnection.VALIDATION_TIMEOUT_SECONDS);
+ }
+
+ /**
+ * A connection closed under the borrow is not handed out on the strength of its last answer:
+ * the removal listener of the pool closes every connection it finds in the deque when the pool
+ * expires, and it iterates a weakly consistent view. The validation the window replaces
+ * answered that as well, out of a flag of the driver rather than out of a round trip.
+ */
+ @Test(timeOut = 120000)
+ public void testAConnectionThePoolClosedIsNotHandedOut() throws Exception {
+ final String url = StubDriver.PREFIX + "closed-inside-the-window";
+ CachedConnection.aliveBypassNanos = TimeUnit.HOURS.toNanos(1);
+ final Connection parent = mock(Connection.class);
+ when(parent.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(parent);
+ CachedConnection.getConnection(url).close();
+ // closed where it lay, by the expiry of the pool: a closed connection answers isValid() with
+ // false as well, which is what discards it once the window stops trusting it
+ when(parent.isClosed()).thenReturn(true);
+ when(parent.isValid(anyInt())).thenReturn(false);
+ final Connection fresh = mock(Connection.class);
+ when(fresh.isValid(anyInt())).thenReturn(true);
+ stub.answerWith(fresh);
+
+ final Connection borrowed = CachedConnection.getConnection(url);
+
+ assertSame(((CachedConnection) borrowed).parent, fresh, "a closed connection was handed out on its last answer");
+ }
+
+ /**
+ * The window is clamped twice: to the idle time the pool keeps a connection for, since a window
+ * longer than that is one the pool can never back - the connection it was meant for is gone
+ * before it closes - and to {@link CachedConnection#MAX_ALIVE_BYPASS_MS} behind it, since the
+ * ttl has no upper bound of its own and a value the unit conversion saturates on would leave
+ * every connection of the pool trusted for the life of the server.
+ * <p>
+ * About the value the class settles on at initialization: the field the pool reads is assigned
+ * once, so a ttl set after that changes neither it nor the idle time it was clamped to.
+ */
+ @Test(timeOut = 120000)
+ public void testTheWindowIsClampedToTheIdleTimeOfThePool() {
+ System.setProperty(CachedConnection.TTL_PROPERTY, "15000");
+ System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, "500");
+ assertEquals(CachedConnection.getAliveBypassMillis(), 500L, "a window inside the ttl was not left alone");
+
+ System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE));
+ assertEquals(CachedConnection.getAliveBypassMillis(), 15000L, "a window of Long.MAX_VALUE was not clamped");
+
+ System.setProperty(CachedConnection.TTL_PROPERTY, "100");
+ assertEquals(CachedConnection.getAliveBypassMillis(), 100L, "the clamp is the configured ttl, not the default");
+
+ // the ttl has no upper bound of its own, so the clamp to it does not bound the window either: both set
+ // to a value the conversion to nanoseconds saturates on would leave every connection of the pool
+ // trusted for the life of the server, which is the outcome this javadoc says the clamp rules out
+ System.setProperty(CachedConnection.TTL_PROPERTY, Long.toString(Long.MAX_VALUE));
+ System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, Long.toString(Long.MAX_VALUE));
+ assertEquals(CachedConnection.getAliveBypassMillis(), CachedConnection.MAX_ALIVE_BYPASS_MS,
+ "a window the ttl did not bound was left to saturate");
+ }
+
+ /**
+ * A setting worth warning about must leave the class usable. Both properties are read by the
+ * initializer of {@code aliveBypassNanos}, and both report an unusable value through the set of
+ * what has been said once already - which, declared below that initializer, would still be null
+ * when the initializer reaches it (JLS 12.4.2). A window longer than the ttl is exactly the
+ * tuning the javadoc of the property invites, and it would turn a log line into an
+ * ExceptionInInitializerError on the first borrow and a causeless NoClassDefFoundError on every
+ * one after it: no connection can be borrowed, so the backend cannot open at all.
+ * <p>
+ * Asserted on the class loaded afresh rather than on this one, which was initialized long
+ * before the property was set.
+ */
+ @Test(timeOut = 120000, dataProvider = "settingsWorthWarningAbout")
+ public void testASettingWorthWarningAboutStillInitializesTheClass(String ttl, String window, long expectedMillis)
+ throws Exception {
+ System.setProperty(CachedConnection.TTL_PROPERTY, ttl);
+ System.setProperty(CachedConnection.ALIVE_BYPASS_PROPERTY, window);
+
+ final Class<?> reloaded = loadedAfresh(CachedConnection.class);
+
+ assertNotSame(reloaded, CachedConnection.class, "the class under test was not loaded afresh");
+ final Field field = reloaded.getDeclaredField("aliveBypassNanos");
+ field.setAccessible(true);
+ assertEquals(field.getLong(null), TimeUnit.MILLISECONDS.toNanos(expectedMillis),
+ "the window the reloaded class settled on");
+ }
+
+ @DataProvider
+ public Object[][] settingsWorthWarningAbout() {
+ return new Object[][]{
+ // a window longer than the ttl: reported once and used as the ttl
+ {"15000", "60000", 15000L},
+ // not a number, and negative: reported once and ignored in favour of the default
+ {"15000", "half a second", CachedConnection.DEFAULT_ALIVE_BYPASS_MS},
+ {"15000", "-1", CachedConnection.DEFAULT_ALIVE_BYPASS_MS},
+ // the ttl is read by the same initializer, and reports its own value the same way
+ {"30s", "500", 500L}
+ };
+ }
+
+ /**
+ * The class again, defined by a loader of this test rather than taken from the one that has
+ * already initialized it: a static initializer runs once per loader, and this is about what it
+ * does. Every other class is delegated to the parent, so the reloaded one shares the types it
+ * is written against.
+ */
+ private static Class<?> loadedAfresh(Class<?> type) throws Exception {
+ final String name = type.getName();
+ final ClassLoader parent = type.getClassLoader();
+ final ClassLoader loader = new ClassLoader(parent) {
+ @Override
+ protected Class<?> loadClass(String candidate, boolean resolve) throws ClassNotFoundException {
+ if (!name.equals(candidate)) {
+ return super.loadClass(candidate, resolve);
+ }
+ Class<?> defined = findLoadedClass(candidate);
+ if (defined == null) {
+ final byte[] bytecode = bytecodeOf(candidate, parent);
+ defined = defineClass(candidate, bytecode, 0, bytecode.length);
+ }
+ if (resolve) {
+ resolveClass(defined);
+ }
+ return defined;
+ }
+ };
+ return Class.forName(name, true, loader);
+ }
+
+ private static byte[] bytecodeOf(String name, ClassLoader from) throws ClassNotFoundException {
+ try (final InputStream in = from.getResourceAsStream(name.replace('.', '/') + ".class")) {
+ if (in == null) {
+ throw new ClassNotFoundException(name);
+ }
+ final ByteArrayOutputStream bytecode = new ByteArrayOutputStream();
+ final byte[] chunk = new byte[8192];
+ for (int read; (read = in.read(chunk)) >= 0; ) {
+ bytecode.write(chunk, 0, read);
+ }
+ return bytecode.toByteArray();
+ } catch (IOException e) {
+ throw new ClassNotFoundException(name, e);
+ }
+ }
+
private static SQLException tooManyConnections() {
// 53300, too_many_connections, of the insufficient_resources class
return new SQLException("sorry, too many clients already", "53300");
--
Gitblit v1.10.0