This is an automated email from the ASF dual-hosted git repository.
yuqi1129 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git
The following commit(s) were added to refs/heads/main by this push:
new 1347c18ecb [MINOR] improvement(common): cache Credential ServiceLoader
lookup in CredentialFactory (#12423)
1347c18ecb is described below
commit 1347c18ecba9fe75290ee420b5465ed5c6a8d4d6
Author: YangJie <[email protected]>
AuthorDate: Wed Aug 12 19:52:48 2026 +0800
[MINOR] improvement(common): cache Credential ServiceLoader lookup in
CredentialFactory (#12423)
### What changes were proposed in this pull request?
`CredentialFactory.create` called `ServiceLoader.load(Credential.class)`
on every invocation and iterated/instantiated every registered
`Credential` implementation just to resolve one type -> class mapping.
This caches the scan once into an immutable `type -> class` map
(initialization-on-demand holder idiom) and turns `lookupCredential`
into a single map get.
Lookup stays case-insensitive and still throws `No credential found for:
<type>` (preserving the caller's original casing) on a miss;
duplicate-type detection moves to cache-build time.
### Why are the changes needed?
`Credential` is a fixed, built-in SPI (custom credentials are added via
`CredentialProvider`, not by registering new `Credential` services), so
the `ServiceLoader` result is stable for the JVM lifetime and does not
need to be rescanned — nor all providers re-instantiated — on every
lookup.
A JMH micro-benchmark shows the type lookup dropping from **~380 µs/op
to ~64 ns/op**:
```
Benchmark Mode Cnt Score
Error Units
CredentialLookupBenchmark.newCachedLookup avgt 3 64.064 ±
8.449 ns/op
CredentialLookupBenchmark.oldPerCallScan avgt 3 380491.264 ±
35292.735 ns/op
```
<details>
<summary>Benchmark used (not part of this PR — drop under
<code>core/src/jmh/java/org/apache/gravitino/credential/</code> and run
<code>./gradlew :core:jmh</code>)</summary>
`oldPerCallScan` replays the previous `ServiceLoader.load` +
stream-filter per call; `newCachedLookup` does a single `get` on the
pre-built map. Only the type-lookup step is measured; the shared
`newInstance` of the resolved credential is excluded, since it is
unchanged by this PR.
```java
package org.apache.gravitino.credential;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import org.openjdk.jmh.annotations.Benchmark;
import org.openjdk.jmh.annotations.BenchmarkMode;
import org.openjdk.jmh.annotations.Mode;
import org.openjdk.jmh.annotations.OutputTimeUnit;
import org.openjdk.jmh.annotations.Scope;
import org.openjdk.jmh.annotations.Setup;
import org.openjdk.jmh.annotations.State;
import org.openjdk.jmh.infra.Blackhole;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
public class CredentialLookupBenchmark {
private Map<String, Class<? extends Credential>> cachedClasses;
private String[] types;
@Setup
public void setup() {
cachedClasses = new HashMap<>();
for (Credential credential : ServiceLoader.load(Credential.class)) {
cachedClasses.put(
credential.credentialType().toLowerCase(Locale.ROOT),
credential.getClass());
}
types = cachedClasses.keySet().toArray(new String[0]);
}
@Benchmark
public void oldPerCallScan(Blackhole bh) {
for (String type : types) {
bh.consume(oldLookup(type));
}
}
@Benchmark
public void newCachedLookup(Blackhole bh) {
for (String type : types) {
bh.consume(cachedClasses.get(type.toLowerCase(Locale.ROOT)));
}
}
private static Class<? extends Credential> oldLookup(String
credentialType) {
ServiceLoader<Credential> serviceLoader =
ServiceLoader.load(Credential.class);
List<Class<? extends Credential>> credentials =
StreamSupport.stream(serviceLoader.spliterator(), false)
.filter(credential ->
credentialType.equalsIgnoreCase(credential.credentialType()))
.map(Credential::getClass)
.collect(Collectors.toList());
return credentials.get(0);
}
}
```
</details>
### Does this PR introduce _any_ user-facing change?
No. Internal factory optimization; lookup semantics (case-insensitive
resolution, `No credential found for: <type>` on miss) are unchanged.
### How was this patch tested?
Existing `TestCredentialFactory` cases plus new ones for unknown-type
rejection (including that the error message preserves the original
casing) and case-insensitive lookup. `./gradlew :common:test --tests
"org.apache.gravitino.credential.TestCredentialFactory"` and spotless
pass.
---
.../gravitino/credential/CredentialFactory.java | 49 +++++++++++++++-------
.../credential/TestCredentialFactory.java | 36 ++++++++++++++++
2 files changed, 70 insertions(+), 15 deletions(-)
diff --git
a/common/src/main/java/org/apache/gravitino/credential/CredentialFactory.java
b/common/src/main/java/org/apache/gravitino/credential/CredentialFactory.java
index 0ba46301b9..9698132956 100644
---
a/common/src/main/java/org/apache/gravitino/credential/CredentialFactory.java
+++
b/common/src/main/java/org/apache/gravitino/credential/CredentialFactory.java
@@ -19,15 +19,41 @@
package org.apache.gravitino.credential;
-import com.google.common.collect.Iterables;
-import com.google.common.collect.Streams;
-import java.util.List;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Locale;
import java.util.Map;
import java.util.ServiceLoader;
-import java.util.stream.Collectors;
/** Create a specific credential according to the credential information. */
public class CredentialFactory {
+
+ /**
+ * Holder for the lazily-initialized, immutable mapping from credential type
to its implementation
+ * class. {@link Credential} is a fixed, built-in SPI (all implementations
ship in the {@code api}
+ * and {@code common} modules on the same class loader as this factory;
custom credentials are
+ * added through {@link CredentialProvider}, not by registering new {@link
Credential} services),
+ * so the {@link ServiceLoader} result is stable for the lifetime of the JVM
and is safe to scan
+ * once and cache instead of on every {@link #create} call.
+ */
+ private static class CredentialClassesHolder {
+ private static final Map<String, Class<? extends Credential>>
CREDENTIAL_CLASSES =
+ loadCredentialClasses();
+
+ private static Map<String, Class<? extends Credential>>
loadCredentialClasses() {
+ Map<String, Class<? extends Credential>> classes = new HashMap<>();
+ for (Credential credential : ServiceLoader.load(Credential.class)) {
+ String type = credential.credentialType().toLowerCase(Locale.ROOT);
+ Class<? extends Credential> existing = classes.put(type,
credential.getClass());
+ if (existing != null) {
+ throw new RuntimeException(
+ "Multiple credentials found for: " +
credential.credentialType());
+ }
+ }
+ return Collections.unmodifiableMap(classes);
+ }
+ }
+
/**
* Creates a {@link Credential} instance based on the provided credential
type, information, and
* expiration time.
@@ -52,18 +78,11 @@ public class CredentialFactory {
}
private static Class<? extends Credential> lookupCredential(String
credentialType) {
- ServiceLoader<Credential> serviceLoader =
ServiceLoader.load(Credential.class);
- List<Class<? extends Credential>> credentials =
- Streams.stream(serviceLoader.iterator())
- .filter(credential ->
credentialType.equalsIgnoreCase(credential.credentialType()))
- .map(Credential::getClass)
- .collect(Collectors.toList());
- if (credentials.isEmpty()) {
+ Class<? extends Credential> credentialClz =
+
CredentialClassesHolder.CREDENTIAL_CLASSES.get(credentialType.toLowerCase(Locale.ROOT));
+ if (credentialClz == null) {
throw new RuntimeException("No credential found for: " + credentialType);
- } else if (credentials.size() > 1) {
- throw new RuntimeException("Multiple credential found for: " +
credentialType);
- } else {
- return Iterables.getOnlyElement(credentials);
}
+ return credentialClz;
}
}
diff --git
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
index 259949ae2b..eaa7402c2a 100644
---
a/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
+++
b/common/src/test/java/org/apache/gravitino/credential/TestCredentialFactory.java
@@ -236,4 +236,40 @@ public class TestCredentialFactory {
Assertions.assertEquals(jdbcPassword, jdbcCredential.jdbcPassword());
Assertions.assertEquals(expireTime, jdbcCredential.expireTimeInMs());
}
+
+ @Test
+ void testUnknownCredentialType() {
+ RuntimeException e =
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () -> CredentialFactory.create("no-such-credential",
ImmutableMap.of(), 0));
+ Assertions.assertEquals("No credential found for: no-such-credential",
e.getMessage());
+ }
+
+ @Test
+ void testUnknownCredentialTypeMessagePreservesOriginalCasing() {
+ // Lookup is case-insensitive, but the "not found" message must echo the
type as the caller
+ // supplied it, not the lower-cased lookup key.
+ RuntimeException e =
+ Assertions.assertThrows(
+ RuntimeException.class,
+ () -> CredentialFactory.create("No-Such-Credential",
ImmutableMap.of(), 0));
+ Assertions.assertEquals("No credential found for: No-Such-Credential",
e.getMessage());
+ }
+
+ @Test
+ void testCredentialTypeLookupIsCaseInsensitive() {
+ // The factory looks up the credential class by type case-insensitively; a
differently-cased
+ // type must resolve to the same implementation.
+ Credential credential =
+ CredentialFactory.create(
+
JdbcCredential.JDBC_CREDENTIAL_TYPE.toUpperCase(java.util.Locale.ROOT),
+ ImmutableMap.of(
+ JdbcCredential.GRAVITINO_JDBC_USER,
+ "test-user",
+ JdbcCredential.GRAVITINO_JDBC_PASSWORD,
+ "test-password"),
+ 0);
+ Assertions.assertInstanceOf(JdbcCredential.class, credential);
+ }
}