[KYUUBI #7769] [COMMON][JDBC] Validate class types before reflective instantiation

### Why are the changes needed?

Several reflective loading paths can initialize or construct a class before checking its expected type.

Use a shared type check before initialization or construction for exception reconstruction and provider, plugin, strategy, and JDBC driver loading. Keep the existing constructor selection and exception reconstruction for supported classes.

### How was this patch tested?

- 23 targeted tests passed across common, JDBC client, server, and Data Agent modules, including checks that rejected classes are not initialized or constructed.
- Manual checks confirmed missing-dependency error wrapping for session advisors, group providers, and Data Agent providers.
- Spotless and `git diff --check` passed.

### Was this patch assisted by generative AI tooling?

Assisted-by: Codex (GPT-6)

Closes #7769 from wangzhigang1999/zhigang/jdbc-exception-security.

Closes #7769

52ce110b9 [wangzhigang] [COMMON][JDBC] Validate class types before reflective instantiation

Authored-by: zhigang <iamzhigangwang@gmail.com>
Signed-off-by: Akira Ajisaka <aajisaka@apache.org>
diff --git a/externals/kyuubi-data-agent-engine/src/main/java/org/apache/kyuubi/engine/dataagent/provider/DataAgentProvider.java b/externals/kyuubi-data-agent-engine/src/main/java/org/apache/kyuubi/engine/dataagent/provider/DataAgentProvider.java
index 97a4340..8e35207 100644
--- a/externals/kyuubi-data-agent-engine/src/main/java/org/apache/kyuubi/engine/dataagent/provider/DataAgentProvider.java
+++ b/externals/kyuubi-data-agent-engine/src/main/java/org/apache/kyuubi/engine/dataagent/provider/DataAgentProvider.java
@@ -20,6 +20,7 @@
 import java.util.function.Consumer;
 import org.apache.kyuubi.config.KyuubiConf;
 import org.apache.kyuubi.engine.dataagent.runtime.event.AgentEvent;
+import org.apache.kyuubi.util.reflect.DynClasses;
 import org.apache.kyuubi.util.reflect.DynConstructors;
 
 /**
@@ -69,10 +70,12 @@
   static DataAgentProvider load(KyuubiConf conf) {
     String providerClass = conf.get(KyuubiConf.ENGINE_DATA_AGENT_PROVIDER());
     try {
+      Class<? extends DataAgentProvider> implementation =
+          DynClasses.loadSubclass(providerClass, DataAgentProvider.class);
       return (DataAgentProvider)
           DynConstructors.builder(DataAgentProvider.class)
-              .impl(providerClass, KyuubiConf.class)
-              .impl(providerClass)
+              .impl(implementation, KyuubiConf.class)
+              .impl(implementation)
               .buildChecked()
               .newInstanceChecked(conf);
     } catch (ClassCastException e) {
@@ -83,7 +86,7 @@
               + DataAgentProvider.class.getName()
               + "'.",
           e);
-    } catch (Exception e) {
+    } catch (Exception | NoClassDefFoundError e) {
       throw new IllegalArgumentException("Error while instantiating '" + providerClass + "': ", e);
     }
   }
diff --git a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala
index f8ec72d..ae6eb9c 100644
--- a/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala
+++ b/externals/kyuubi-jdbc-engine/src/main/scala/org/apache/kyuubi/engine/jdbc/connection/ConnectionProvider.scala
@@ -28,9 +28,18 @@
   protected val providers = loadProviders()
 
   def getDriverClass(kyuubiConf: KyuubiConf): String = {
-    val driverClass: Class[_ <: Driver] = Option(
-      DynClasses.builder().impl(kyuubiConf.get(ENGINE_JDBC_DRIVER_CLASS).get)
-        .orNull().build[Driver]()).getOrElse {
+    val driverName = kyuubiConf.get(ENGINE_JDBC_DRIVER_CLASS).get
+    val classLoader = Thread.currentThread().getContextClassLoader
+    val configuredDriver: Option[Class[_ <: Driver]] =
+      try {
+        val driverClass = DynClasses.loadSubclass(driverName, classOf[Driver], classLoader)
+        // JDBC drivers register themselves during initialization, after the type check.
+        Class.forName(driverName, true, classLoader)
+        Some(driverClass)
+      } catch {
+        case _: ClassNotFoundException => None
+      }
+    val driverClass: Class[_ <: Driver] = configuredDriver.getOrElse {
       val url = kyuubiConf.get(ENGINE_JDBC_CONNECTION_URL).get
       DriverManager.getDriver(url).getClass
     }
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala
index 6153ac3..d56bb00 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/KyuubiSQLException.scala
@@ -25,7 +25,7 @@
 
 import org.apache.kyuubi.Utils.stringifyException
 import org.apache.kyuubi.shaded.hive.service.rpc.thrift.{TStatus, TStatusCode}
-import org.apache.kyuubi.util.reflect.DynConstructors
+import org.apache.kyuubi.util.reflect.{DynClasses, DynConstructors}
 
 /**
  * @param reason     a description of the exception
@@ -139,12 +139,14 @@
   }
   private def newInstance(className: String, message: String, cause: Throwable): Throwable = {
     try {
+      val exceptionClass = DynClasses.loadSubclass(className, classOf[Throwable])
       DynConstructors.builder()
-        .impl(className, classOf[String], classOf[Throwable])
+        .impl(exceptionClass, classOf[String], classOf[Throwable])
         .buildChecked[Throwable]()
         .newInstance(message, cause)
     } catch {
-      case _: Exception => new RuntimeException(className + ":" + message, cause)
+      case _: Exception | _: NoClassDefFoundError =>
+        new RuntimeException(className + ":" + message, cause)
     }
   }
 
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/EngineSecuritySecretProvider.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/EngineSecuritySecretProvider.scala
index 5a4cde0..546e93e 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/EngineSecuritySecretProvider.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/service/authentication/EngineSecuritySecretProvider.scala
@@ -19,7 +19,7 @@
 
 import org.apache.kyuubi.config.KyuubiConf
 import org.apache.kyuubi.config.KyuubiConf._
-import org.apache.kyuubi.util.reflect.DynConstructors
+import org.apache.kyuubi.util.reflect.{DynClasses, DynConstructors}
 
 trait EngineSecuritySecretProvider {
 
@@ -51,8 +51,11 @@
 
 object EngineSecuritySecretProvider {
   def create(conf: KyuubiConf): EngineSecuritySecretProvider = {
+    val providerClass = DynClasses.loadSubclass(
+      conf.get(INTERNAL_SECURITY_SECRET_PROVIDER),
+      classOf[EngineSecuritySecretProvider])
     val provider = DynConstructors.builder()
-      .impl(conf.get(INTERNAL_SECURITY_SECRET_PROVIDER))
+      .impl(providerClass)
       .buildChecked[EngineSecuritySecretProvider]()
       .newInstance(conf)
     provider.initialize(conf)
diff --git a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ClassUtils.scala b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ClassUtils.scala
index d8eda34..fc85b24 100644
--- a/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ClassUtils.scala
+++ b/kyuubi-common/src/main/scala/org/apache/kyuubi/util/ClassUtils.scala
@@ -32,15 +32,15 @@
    * @return
    */
   def createInstance[T](className: String, expected: Class[T], conf: KyuubiConf): T = {
-    val classLoader = Thread.currentThread.getContextClassLoader
     try {
-      DynConstructors.builder(expected).loader(classLoader)
-        .impl(className, classOf[KyuubiConf])
-        .impl(className)
+      val implementation = DynClasses.loadSubclass(className, expected)
+      DynConstructors.builder(expected)
+        .impl(implementation, classOf[KyuubiConf])
+        .impl(implementation)
         .buildChecked[T]()
         .newInstance(conf)
     } catch {
-      case e: Exception =>
+      case e @ (_: Exception | _: NoClassDefFoundError) =>
         throw new KyuubiException(s"$className must extend of ${expected.getName}", e)
     }
   }
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala
index 0b1d65c..995943d 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/KyuubiSQLExceptionSuite.scala
@@ -77,4 +77,29 @@
     val cornerCase = new InvocationTargetException(null)
     assert(KyuubiSQLException(cornerCase).getCause === cornerCase)
   }
+
+  test("non-Throwable classes are not initialized or constructed") {
+    // Refer to the module by name to avoid initializing it in the test itself.
+    val classNames = Seq(
+      "org.apache.kyuubi.RemoteInitializationProbe$",
+      classOf[RemoteConstructorProbe].getName)
+    classNames.foreach { className =>
+      val cause = KyuubiSQLException.toCause(List(s"*$className:remote failure:0:-1"))
+      assert(!KyuubiSQLExceptionSuite.remoteCodeExecuted)
+      assert(cause.getClass === classOf[RuntimeException])
+      assert(cause.getMessage === s"$className:remote failure")
+    }
+  }
+}
+
+object KyuubiSQLExceptionSuite {
+  var remoteCodeExecuted = false
+}
+
+private[kyuubi] object RemoteInitializationProbe {
+  KyuubiSQLExceptionSuite.remoteCodeExecuted = true
+}
+
+private[kyuubi] class RemoteConstructorProbe(message: String, cause: Throwable) {
+  KyuubiSQLExceptionSuite.remoteCodeExecuted = true
 }
diff --git a/kyuubi-common/src/test/scala/org/apache/kyuubi/util/ClassUtilsSuite.scala b/kyuubi-common/src/test/scala/org/apache/kyuubi/util/ClassUtilsSuite.scala
index cda638b..72f7eee 100644
--- a/kyuubi-common/src/test/scala/org/apache/kyuubi/util/ClassUtilsSuite.scala
+++ b/kyuubi-common/src/test/scala/org/apache/kyuubi/util/ClassUtilsSuite.scala
@@ -17,13 +17,20 @@
 
 package org.apache.kyuubi.util
 
-import org.apache.kyuubi.KyuubiFunSuite
+import org.apache.kyuubi.{KyuubiException, KyuubiFunSuite}
 import org.apache.kyuubi.config.KyuubiConf
 
 class ClassUtilsSuite extends KyuubiFunSuite {
 
   private val _conf = KyuubiConf()
 
+  test("reject unrelated classes") {
+    val error = intercept[KyuubiException] {
+      ClassUtils.createInstance(classOf[KyuubiConf].getName, classOf[SomeProvider], _conf)
+    }
+    assert(error.getCause.isInstanceOf[ClassCastException])
+  }
+
   test("create instance with zero-arg arg") {
     val instance = ClassUtils.createInstance[SomeProvider](
       "org.apache.kyuubi.util.ProviderA",
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java
index b5e6579..6accf7a 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLException.java
@@ -21,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import org.apache.kyuubi.shaded.hive.service.rpc.thrift.TStatus;
+import org.apache.kyuubi.util.reflect.DynClasses;
 import org.apache.kyuubi.util.reflect.DynConstructors;
 
 public class KyuubiSQLException extends SQLException {
@@ -187,11 +188,13 @@
 
   private static Throwable newInstance(String className, String message) {
     try {
+      Class<? extends Throwable> exceptionClass =
+          DynClasses.loadSubclass(className, Throwable.class);
       return DynConstructors.builder()
-          .impl(className, String.class)
+          .impl(exceptionClass, String.class)
           .<Throwable>buildChecked()
           .newInstance(message);
-    } catch (Exception e) {
+    } catch (Exception | NoClassDefFoundError e) {
       return new RuntimeException(className + ":" + message);
     }
   }
diff --git a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/strategy/ServerSelectStrategyFactory.java b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/strategy/ServerSelectStrategyFactory.java
index 9950097..5792732 100644
--- a/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/strategy/ServerSelectStrategyFactory.java
+++ b/kyuubi-hive-jdbc/src/main/java/org/apache/kyuubi/jdbc/hive/strategy/ServerSelectStrategyFactory.java
@@ -20,6 +20,7 @@
 import java.lang.reflect.Constructor;
 import org.apache.kyuubi.jdbc.hive.strategy.zk.PollingSelectStrategy;
 import org.apache.kyuubi.jdbc.hive.strategy.zk.RandomSelectStrategy;
+import org.apache.kyuubi.util.reflect.DynClasses;
 
 public class ServerSelectStrategyFactory {
   public static ServerSelectStrategy createStrategy(String strategyName) {
@@ -30,15 +31,19 @@
         case RandomSelectStrategy.strategyName:
           return new RandomSelectStrategy();
         default:
-          Class<?> clazz = Class.forName(strategyName);
-          if (ServerSelectStrategy.class.isAssignableFrom(clazz)) {
-            Constructor<? extends ServerSelectStrategy> constructor =
-                clazz.asSubclass(ServerSelectStrategy.class).getConstructor();
-            return constructor.newInstance();
-          } else {
+          Class<? extends ServerSelectStrategy> clazz;
+          try {
+            clazz =
+                DynClasses.loadSubclass(
+                    strategyName,
+                    ServerSelectStrategy.class,
+                    ServerSelectStrategyFactory.class.getClassLoader());
+          } catch (ClassCastException e) {
             throw new ClassNotFoundException(
                 "The loaded class does not implement ServerSelectStrategy");
           }
+          Constructor<? extends ServerSelectStrategy> constructor = clazz.getConstructor();
+          return constructor.newInstance();
       }
     } catch (Exception e) {
       throw new RuntimeException("Failed to init server select strategy", e);
diff --git a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLExceptionTest.java b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLExceptionTest.java
new file mode 100644
index 0000000..35d873a
--- /dev/null
+++ b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/KyuubiSQLExceptionTest.java
@@ -0,0 +1,54 @@
+/*
+ * 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.kyuubi.jdbc.hive;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import java.util.Collections;
+import org.junit.jupiter.api.Test;
+
+public class KyuubiSQLExceptionTest {
+
+  private static boolean remoteClassInitialized;
+
+  public static class StaticInitializerProbe {
+    static {
+      remoteClassInitialized = true;
+    }
+
+    public StaticInitializerProbe(String message) {}
+  }
+
+  @Test
+  public void decodeOnlyThrowableClasses() {
+    String className = StaticInitializerProbe.class.getName();
+    Throwable cause =
+        KyuubiSQLException.toCause(
+            Collections.singletonList("*" + className + ":remote failure:0:-1"));
+
+    assertFalse(remoteClassInitialized);
+    assertEquals(RuntimeException.class, cause.getClass());
+    assertEquals(className + ":remote failure", cause.getMessage());
+
+    IllegalArgumentException original = new IllegalArgumentException("original message");
+    Throwable restored = KyuubiSQLException.toCause(KyuubiSQLException.toString(original));
+    assertEquals(original.getClass(), restored.getClass());
+    assertEquals(original.getMessage(), restored.getMessage());
+  }
+}
diff --git a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/ZooKeeperHiveClientHelperTest.java b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/ZooKeeperHiveClientHelperTest.java
index d70f483..0885929 100644
--- a/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/ZooKeeperHiveClientHelperTest.java
+++ b/kyuubi-hive-jdbc/src/test/java/org/apache/kyuubi/jdbc/hive/ZooKeeperHiveClientHelperTest.java
@@ -20,13 +20,38 @@
 
 import static org.apache.kyuubi.jdbc.hive.Utils.extractURLComponents;
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 
 import java.util.Properties;
+import org.apache.kyuubi.jdbc.hive.strategy.ServerSelectStrategyFactory;
+import org.apache.kyuubi.jdbc.hive.strategy.zk.PollingSelectStrategy;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.ValueSource;
 
 public class ZooKeeperHiveClientHelperTest {
 
+  private static boolean rejectedClassInitialized;
+
+  public static class RejectedStrategy {
+    static {
+      rejectedClassInitialized = true;
+    }
+  }
+
+  @Test
+  public void validateStrategyBeforeInitialization() {
+    assertThrows(
+        RuntimeException.class,
+        () -> ServerSelectStrategyFactory.createStrategy(RejectedStrategy.class.getName()));
+    assertFalse(rejectedClassInitialized);
+    assertEquals(
+        PollingSelectStrategy.class,
+        ServerSelectStrategyFactory.createStrategy(PollingSelectStrategy.class.getName())
+            .getClass());
+  }
+
   @ParameterizedTest
   @ValueSource(
       strings = {
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala
index 1bc80dc..15d76d3 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/plugin/PluginLoader.scala
@@ -21,7 +21,7 @@
 
 import org.apache.kyuubi.KyuubiException
 import org.apache.kyuubi.config.KyuubiConf
-import org.apache.kyuubi.util.reflect.DynConstructors
+import org.apache.kyuubi.util.reflect.{DynClasses, DynConstructors}
 
 private[kyuubi] object PluginLoader {
 
@@ -32,13 +32,14 @@
     }
     advisorClass.get.map { advisorClassName =>
       try {
-        DynConstructors.builder.impl(advisorClassName)
+        val implementation = DynClasses.loadSubclass(advisorClassName, classOf[SessionConfAdvisor])
+        DynConstructors.builder.impl(implementation)
           .buildChecked[SessionConfAdvisor].newInstance()
       } catch {
         case _: ClassCastException =>
           throw new KyuubiException(
             s"Class $advisorClassName is not a child of '${classOf[SessionConfAdvisor].getName}'.")
-        case NonFatal(e) =>
+        case e @ (NonFatal(_) | _: NoClassDefFoundError) =>
           throw new IllegalArgumentException(s"Error while instantiating '$advisorClassName': ", e)
       }
     }
@@ -47,12 +48,13 @@
   def loadGroupProvider(conf: KyuubiConf): GroupProvider = {
     val groupProviderClass = conf.get(KyuubiConf.GROUP_PROVIDER)
     try {
-      DynConstructors.builder().impl(groupProviderClass).buildChecked[GroupProvider]().newInstance()
+      val providerClass = DynClasses.loadSubclass(groupProviderClass, classOf[GroupProvider])
+      DynConstructors.builder().impl(providerClass).buildChecked[GroupProvider]().newInstance()
     } catch {
       case _: ClassCastException =>
         throw new KyuubiException(
           s"Class $groupProviderClass is not a child of '${classOf[GroupProvider].getName}'.")
-      case NonFatal(e) =>
+      case e @ (NonFatal(_) | _: NoClassDefFoundError) =>
         throw new IllegalArgumentException(s"Error while instantiating '$groupProviderClass': ", e)
     }
   }
diff --git a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BearerAuthenticationHandler.scala b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BearerAuthenticationHandler.scala
index 7d22df8..7aa9cbf 100644
--- a/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BearerAuthenticationHandler.scala
+++ b/kyuubi-server/src/main/scala/org/apache/kyuubi/server/http/authentication/BearerAuthenticationHandler.scala
@@ -27,6 +27,7 @@
 import org.apache.kyuubi.server.http.util.HttpAuthUtils
 import org.apache.kyuubi.server.http.util.HttpAuthUtils.{AUTHORIZATION_HEADER, WWW_AUTHENTICATE_HEADER}
 import org.apache.kyuubi.service.authentication.{AnonymousAuthenticationProviderImpl, AuthenticationProviderFactory, DefaultTokenCredential, TokenAuthenticationProvider}
+import org.apache.kyuubi.util.reflect.DynClasses
 
 class BearerAuthenticationHandler(providerClass: String)
   extends AuthenticationHandler with Logging {
@@ -42,7 +43,10 @@
   override def authenticationSupported: Boolean = {
     Option(providerClass).exists { _ =>
       try {
-        Class.forName(providerClass).isAssignableFrom(classOf[TokenAuthenticationProvider])
+        DynClasses.loadSubclass(
+          providerClass,
+          classOf[TokenAuthenticationProvider],
+          classOf[BearerAuthenticationHandler].getClassLoader)
         true
       } catch {
         case _: Throwable => false
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala
index fa4505c..2e032e6 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/plugin/PluginLoaderSuite.scala
@@ -131,8 +131,12 @@
   }
 }
 
-class InvalidSessionConfAdvisor
-class InvalidGroupProvider
+class InvalidSessionConfAdvisor {
+  throw new AssertionError("Invalid advisor must not be constructed")
+}
+class InvalidGroupProvider {
+  throw new AssertionError("Invalid group provider must not be constructed")
+}
 
 class TestSessionConfAdvisor extends SessionConfAdvisor {
   override def getConfOverlay(
diff --git a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala
index de4b056..33a076e 100644
--- a/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala
+++ b/kyuubi-server/src/test/scala/org/apache/kyuubi/server/http/authentication/AuthenticationFilterSuite.scala
@@ -19,7 +19,7 @@
 
 import org.apache.kyuubi.KyuubiFunSuite
 import org.apache.kyuubi.config.KyuubiConf
-import org.apache.kyuubi.service.authentication.AuthTypes
+import org.apache.kyuubi.service.authentication.{AnonymousAuthenticationProviderImpl, AuthTypes}
 
 class AuthenticationFilterSuite extends KyuubiFunSuite {
   test("add auth handler and destroy") {
@@ -32,6 +32,11 @@
     assert(filter.authSchemeHandlers.size == 1)
     filter.addAuthHandler(new KerberosAuthenticationHandler())
     assert(filter.authSchemeHandlers.size == 1)
+    filter.addAuthHandler(new BearerAuthenticationHandler(classOf[KyuubiConf].getName))
+    assert(filter.authSchemeHandlers.size == 1)
+    filter.addAuthHandler(new BearerAuthenticationHandler(
+      classOf[AnonymousAuthenticationProviderImpl].getName))
+    assert(filter.authSchemeHandlers.size == 2)
     filter.destroy()
     assert(filter.authSchemeHandlers.isEmpty)
   }
diff --git a/kyuubi-util/src/main/java/org/apache/kyuubi/util/reflect/DynClasses.java b/kyuubi-util/src/main/java/org/apache/kyuubi/util/reflect/DynClasses.java
index 78bdd54..f01c524 100644
--- a/kyuubi-util/src/main/java/org/apache/kyuubi/util/reflect/DynClasses.java
+++ b/kyuubi-util/src/main/java/org/apache/kyuubi/util/reflect/DynClasses.java
@@ -27,6 +27,21 @@
 
   private DynClasses() {}
 
+  /** Loads a subclass without initializing it, using the thread context class loader. */
+  public static <T> Class<? extends T> loadSubclass(String className, Class<T> expected)
+      throws ClassNotFoundException {
+    return loadSubclass(className, expected, Thread.currentThread().getContextClassLoader());
+  }
+
+  /**
+   * Loads and checks a class without running its static initializer. Initialization must wait until
+   * after the type check, because the class name may come from an untrusted source.
+   */
+  public static <T> Class<? extends T> loadSubclass(
+      String className, Class<T> expected, ClassLoader loader) throws ClassNotFoundException {
+    return Class.forName(className, false, loader).asSubclass(expected);
+  }
+
   public static Builder builder() {
     return new Builder();
   }