Skip to content

Commit 483b833

Browse files
fixes #1294 add support for HikariCredentialsProvider class
1 parent 33692d2 commit 483b833

File tree

6 files changed

+189
-57
lines changed

6 files changed

+189
-57
lines changed

‎pom.xml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454

5555
<groupId>com.zaxxer</groupId>
5656
<artifactId>HikariCP</artifactId>
57-
<version>6.3.3-SNAPSHOT</version>
57+
<version>7.0.0-SNAPSHOT</version>
5858
<packaging>bundle</packaging>
5959

6060
<name>HikariCP</name>

‎src/main/java/com/zaxxer/hikari/HikariConfig.java‎

Lines changed: 61 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ public class HikariConfig implements HikariConfigMXBean
7474
private long initializationFailTimeout;
7575
private String connectionInitSql;
7676
private String connectionTestQuery;
77+
private String credentialsProviderClassName;
7778
private String dataSourceClassName;
7879
private String dataSourceJndiName;
7980
private String driverClassName;
@@ -88,6 +89,7 @@ public class HikariConfig implements HikariConfigMXBean
8889
private boolean isIsolateInternalQueries;
8990
private boolean isRegisterMbeans;
9091
private boolean isAllowPoolSuspension;
92+
private HikariCredentialsProvider credentialsProvider;
9193
private DataSource dataSource;
9294
private Properties dataSourceProperties;
9395
private ThreadFactory threadFactory;
@@ -500,26 +502,12 @@ public void setDriverClassName(String driverClassName)
500502
{
501503
checkIfSealed();
502504

503-
var driverClass = attemptFromContextLoader(driverClassName);
504505
try {
505-
if (driverClass == null) {
506-
driverClass = this.getClass().getClassLoader().loadClass(driverClassName);
507-
LOGGER.debug("Driver class {} found in the HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
508-
}
509-
} catch (ClassNotFoundException e) {
510-
LOGGER.error("Failed to load driver class {} from HikariConfig class classloader {}", driverClassName, this.getClass().getClassLoader());
511-
}
512-
513-
if (driverClass == null) {
514-
throw new RuntimeException("Failed to load driver class " + driverClassName + " in either of HikariConfig class loader or Thread context classloader");
515-
}
516-
517-
try {
518-
driverClass.getConstructor().newInstance();
506+
createInstance(driverClassName, java.sql.Driver.class);
519507
this.driverClassName = driverClassName;
520508
}
521509
catch (Exception e) {
522-
throw new RuntimeException("Failed to instantiate class " + driverClassName, e);
510+
throw new RuntimeException("Failed to load driver class " + driverClassName, e);
523511
}
524512
}
525513

@@ -879,6 +867,59 @@ public void setSchema(String schema)
879867
this.schema = schema;
880868
}
881869

870+
/**
871+
* Get the class name of the {@link HikariCredentialsProvider} that will be used to get credentials at runtime.
872+
*
873+
* @return the class name of the credentials provider
874+
* @see HikariCredentialsProvider
875+
*/
876+
public String getCredentialsProviderClassName()
877+
{
878+
return credentialsProviderClassName;
879+
}
880+
881+
/**
882+
* Set the class name of the {@link HikariCredentialsProvider} that will be used to get credentials at runtime. Use this method
883+
* or provide a {@link HikariCredentialsProvider} instance via the {@link #setCredentialsProvider(HikariCredentialsProvider)} method.
884+
*
885+
* @param credentialsProviderClassName the class name of the credentials provider
886+
* @see HikariCredentialsProvider
887+
*/
888+
public void setCredentialsProviderClassName(String credentialsProviderClassName) {
889+
checkIfSealed();
890+
891+
try {
892+
this.credentialsProvider = createInstance(credentialsProviderClassName, HikariCredentialsProvider.class);
893+
this.exceptionOverrideClassName = credentialsProviderClassName;
894+
}
895+
catch (Exception e) {
896+
throw new RuntimeException("Failed to instantiate class " + credentialsProviderClassName, e);
897+
}
898+
}
899+
900+
/**
901+
* Get the {@link HikariCredentialsProvider} instance created by {@link #setCredentialsProviderClassName(String)} or specified by
902+
* {@link #setCredentialsProvider(HikariCredentialsProvider)}.
903+
*
904+
* @return the HikariCredentialsProvider instance, or null
905+
* @see HikariCredentialsProvider
906+
*/
907+
public HikariCredentialsProvider getCredentialsProvider() {
908+
return credentialsProvider;
909+
}
910+
911+
/**
912+
* Set a user supplied {@link HikariCredentialsProvider} instance. If this method is used, then the {@link #setCredentialsProviderClassName(String)}
913+
* method should not be used. The {@link HikariCredentialsProvider} instance will be used to get credentials at runtime.
914+
*
915+
* @param credentialsProvider a user supplied HikariCredentialsProvider instance
916+
* @see HikariCredentialsProvider
917+
*/
918+
public void setCredentialsProvider(HikariCredentialsProvider credentialsProvider) {
919+
checkIfSealed();
920+
this.credentialsProvider = credentialsProvider;
921+
}
922+
882923
/**
883924
* Get the user supplied SQLExceptionOverride class name.
884925
*
@@ -900,38 +941,20 @@ public void setExceptionOverrideClassName(String exceptionOverrideClassName)
900941
{
901942
checkIfSealed();
902943

903-
var overrideClass = attemptFromContextLoader(exceptionOverrideClassName);
904-
try {
905-
if (overrideClass == null) {
906-
overrideClass = this.getClass().getClassLoader().loadClass(exceptionOverrideClassName);
907-
LOGGER.debug("SQLExceptionOverride class {} found in the HikariConfig class classloader {}", exceptionOverrideClassName, this.getClass().getClassLoader());
908-
}
909-
} catch (ClassNotFoundException e) {
910-
LOGGER.error("Failed to load SQLExceptionOverride class {} from HikariConfig class classloader {}", exceptionOverrideClassName, this.getClass().getClassLoader());
911-
}
912-
913-
if (overrideClass == null) {
914-
throw new RuntimeException("Failed to load SQLExceptionOverride class " + exceptionOverrideClassName + " in either of HikariConfig class loader or Thread context classloader");
915-
}
916-
917-
if (!SQLExceptionOverride.class.isAssignableFrom(overrideClass)) {
918-
throw new RuntimeException("Loaded SQLExceptionOverride class " + exceptionOverrideClassName + " does not implement " + SQLExceptionOverride.class.getName());
919-
}
920-
921944
try {
922-
this.exceptionOverride = (SQLExceptionOverride) overrideClass.getConstructor().newInstance();
945+
this.exceptionOverride = createInstance(exceptionOverrideClassName, SQLExceptionOverride.class);
923946
this.exceptionOverrideClassName = exceptionOverrideClassName;
924947
}
925948
catch (Exception e) {
926949
throw new RuntimeException("Failed to instantiate class " + exceptionOverrideClassName, e);
927950
}
928951
}
929952

930-
931953
/**
932-
* Get the SQLExceptionOverride instance created by {@link #setExceptionOverrideClassName(String)}.
954+
* Get the SQLExceptionOverride instance created by {@link #setExceptionOverrideClassName(String)} or specified by
955+
* {@link #setExceptionOverride(SQLExceptionOverride)}.
933956
*
934-
* @return the SQLExceptionOverride instance, or null if {@link #setExceptionOverrideClassName(String)} is not called
957+
* @return the SQLExceptionOverride instance, or null
935958
* @see SQLExceptionOverride
936959
*/
937960
public SQLExceptionOverride getExceptionOverride()
@@ -1018,22 +1041,6 @@ public void copyStateTo(HikariConfig other)
10181041
// Private methods
10191042
// ***********************************************************************
10201043

1021-
private Class<?> attemptFromContextLoader(final String className) {
1022-
final var threadContextClassLoader = Thread.currentThread().getContextClassLoader();
1023-
if (threadContextClassLoader != null) {
1024-
try {
1025-
final var driverClass = threadContextClassLoader.loadClass(className);
1026-
LOGGER.debug("Class {} found in Thread context class loader {}", className, threadContextClassLoader);
1027-
return driverClass;
1028-
} catch (ClassNotFoundException e) {
1029-
LOGGER.debug("Class {} not found in Thread context class loader {}, trying classloader {}",
1030-
driverClassName, threadContextClassLoader, this.getClass().getClassLoader());
1031-
}
1032-
}
1033-
1034-
return null;
1035-
}
1036-
10371044
@SuppressWarnings("StatementWithEmptyBody")
10381045
public void validate()
10391046
{
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/*
2+
* Copyright (C) 2025 Brett Wooldridge
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.zaxxer.hikari;
18+
19+
import com.zaxxer.hikari.util.Credentials;
20+
21+
/**
22+
* Users can implement this interface to provide credentials for HikariCP.
23+
* This is useful when credentials need to be dynamically generated or retrieved
24+
* at runtime, rather than being hardcoded in the configuration.
25+
*/
26+
public interface HikariCredentialsProvider {
27+
/**
28+
* This method is called to retrieve the credentials for HikariCP.
29+
*
30+
* @return a {@link Credentials} object containing the username and password
31+
*/
32+
Credentials getCredentials();
33+
}

‎src/main/java/com/zaxxer/hikari/pool/PoolBase.java‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package com.zaxxer.hikari.pool;
1818

1919
import com.zaxxer.hikari.HikariConfig;
20+
import com.zaxxer.hikari.HikariCredentialsProvider;
2021
import com.zaxxer.hikari.SQLExceptionOverride;
2122
import com.zaxxer.hikari.metrics.IMetricsTracker;
2223
import com.zaxxer.hikari.pool.HikariPool.PoolInitializationException;
@@ -67,6 +68,7 @@ abstract class PoolBase
6768
long validationTimeout;
6869

6970
SQLExceptionOverride exceptionOverride;
71+
HikariCredentialsProvider credentialsProvider;
7072

7173
private static final String[] RESET_STATES = {"readOnly", "autoCommit", "isolation", "catalog", "netTimeout", "schema"};
7274
private static final int UNINITIALIZED = -1;
@@ -102,6 +104,7 @@ abstract class PoolBase
102104
this.isReadOnly = config.isReadOnly();
103105
this.isAutoCommit = config.isAutoCommit();
104106
this.exceptionOverride = config.getExceptionOverride();
107+
this.credentialsProvider = config.getCredentialsProvider();
105108
this.transactionIsolation = UtilityElf.getTransactionIsolation(config.getTransactionIsolation());
106109

107110
this.isQueryTimeoutSupported = UNINITIALIZED;
@@ -669,6 +672,10 @@ private String stringFromResetBits(final int bits)
669672

670673
private Credentials getCredentials()
671674
{
675+
if (credentialsProvider != null) {
676+
return credentialsProvider.getCredentials();
677+
}
678+
672679
var credentials = config.getCredentials();
673680
if (LEGACY_USERPASS_DS_OVERRIDE) {
674681
credentials = Credentials.of(config.getUsername(), config.getPassword());

‎src/main/java/com/zaxxer/hikari/util/UtilityElf.java‎

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package com.zaxxer.hikari.util;
1818

19+
import org.slf4j.Logger;
20+
import org.slf4j.LoggerFactory;
21+
1922
import java.util.Locale;
2023
import java.util.concurrent.*;
2124
import java.util.regex.Pattern;
@@ -34,6 +37,12 @@
3437
*/
3538
public final class UtilityElf
3639
{
40+
private static final Logger LOGGER = LoggerFactory.getLogger(UtilityElf.class);
41+
42+
/**
43+
* A pattern to match and mask passwords in JDBC URLs.
44+
* It looks for the "password" parameter in the URL and replaces its value with "<masked>".
45+
*/
3746
private static final Pattern PASSWORD_MASKING_PATTERN = Pattern.compile("([?&;][^&#;=]*[pP]assword=)[^&#;]*");
3847

3948
private UtilityElf()
@@ -88,6 +97,11 @@ public static boolean safeIsAssignableFrom(Object obj, String className) {
8897
}
8998
}
9099

100+
public static <T> T createInstance(final String className, final Class<T> clazz)
101+
{
102+
return createInstance(className, clazz, new Object[0]);
103+
}
104+
91105
/**
92106
* Create and instance of the specified class using the constructor matching the specified
93107
* arguments.
@@ -105,7 +119,11 @@ public static <T> T createInstance(final String className, final Class<T> clazz,
105119
}
106120

107121
try {
108-
var loaded = UtilityElf.class.getClassLoader().loadClass(className);
122+
var loaded = attemptFromContextLoader(className);
123+
if (loaded == null) {
124+
loaded = UtilityElf.class.getClassLoader().loadClass(className);
125+
LOGGER.debug("Class {} loaded from classloader {}", className, UtilityElf.class.getClassLoader());
126+
}
109127
var totalArgs = args.length;
110128

111129
if (totalArgs == 0) {
@@ -120,7 +138,7 @@ public static <T> T createInstance(final String className, final Class<T> clazz,
120138
return clazz.cast(constructor.newInstance(args));
121139
}
122140
catch (Exception e) {
123-
throw new RuntimeException(e);
141+
throw new RuntimeException("Failed to load class " + className, e);
124142
}
125143
}
126144

@@ -234,4 +252,24 @@ public Thread newThread(Runnable r) {
234252
return thread;
235253
}
236254
}
255+
256+
// ***********************************************************************
257+
// Private methods
258+
// ***********************************************************************
259+
260+
private static Class<?> attemptFromContextLoader(final String className) {
261+
final var threadContextClassLoader = Thread.currentThread().getContextClassLoader();
262+
if (threadContextClassLoader != null) {
263+
try {
264+
final var clazz = threadContextClassLoader.loadClass(className);
265+
LOGGER.debug("Class {} found in Thread context class loader {}", className, threadContextClassLoader);
266+
return clazz;
267+
} catch (ClassNotFoundException e) {
268+
LOGGER.debug("Class {} not found in Thread context class loader {}, trying classloader {}",
269+
className, threadContextClassLoader, UtilityElf.class.getClassLoader());
270+
}
271+
}
272+
273+
return null;
274+
}
237275
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.zaxxer.hikari.pool;
2+
3+
import com.zaxxer.hikari.HikariConfig;
4+
import com.zaxxer.hikari.HikariCredentialsProvider;
5+
import com.zaxxer.hikari.HikariDataSource;
6+
import com.zaxxer.hikari.util.Credentials;
7+
import org.junit.Test;
8+
9+
import java.time.Duration;
10+
import java.util.concurrent.atomic.AtomicBoolean;
11+
12+
import static com.zaxxer.hikari.pool.TestElf.newHikariConfig;
13+
import static org.apache.commons.lang3.ThreadUtils.sleepQuietly;
14+
import static org.junit.Assert.assertTrue;
15+
import static org.junit.Assert.fail;
16+
17+
public class TestCredentials {
18+
@Test
19+
public void testCredentialsProvider() {
20+
HikariConfig config = newHikariConfig();
21+
config.setMinimumIdle(0);
22+
config.setMaximumPoolSize(1);
23+
config.setConnectionTimeout(2500);
24+
config.setConnectionTestQuery("VALUES 1");
25+
config.setInitializationFailTimeout(Long.MAX_VALUE);
26+
config.setDataSourceClassName("com.zaxxer.hikari.mocks.StubDataSource");
27+
config.setCredentialsProvider(new TestCredentialsProvider());
28+
29+
try (HikariDataSource ds = new HikariDataSource(config)) {
30+
sleepQuietly(Duration.ofMillis(250L)); // Allow time for the credentials provider to be called
31+
32+
assertTrue("CredentialsProvider was not called", ((TestCredentialsProvider) ds.getCredentialsProvider()).called.get());
33+
} catch (Exception e) {
34+
fail("Exception occurred: " + e.getMessage());
35+
}
36+
}
37+
38+
public static class TestCredentialsProvider implements HikariCredentialsProvider {
39+
AtomicBoolean called = new AtomicBoolean();
40+
41+
@Override
42+
public Credentials getCredentials() {
43+
called.set(true);
44+
return new Credentials("user", "password");
45+
}
46+
}
47+
}

0 commit comments

Comments
 (0)