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

chengpan pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/kyuubi.git


The following commit(s) were added to refs/heads/master by this push:
     new 586f6008b [KYUUBI #6399] Spark Kyuubi UI supports both javax and 
jakarta servlet namespaces
586f6008b is described below

commit 586f6008bda6aabec4e1de7c60609ab2caadbc59
Author: Cheng Pan <[email protected]>
AuthorDate: Wed May 22 13:03:06 2024 +0800

    [KYUUBI #6399] Spark Kyuubi UI supports both javax and jakarta servlet 
namespaces
    
    # :mag: Description
    
    Spark 4.0 migrated from `javax.servlet` to `jakarta.servlet` in 
SPARK-47118, which breaks the binary compatibility of `SparkUITab` and 
`WebUIPage` that Kyuubi used, thus breaking the previous assumption of Kyuubi 
Spark SQL engine: single jar built with default Spark version, compatible with 
all supported versions of Spark runtime.
    
    ## Describe Your Solution ๐Ÿ”ง
    
    This PR uses bytebuddy to dynamically generate classes and Java reflection 
find and dispatch method invocation in runtime, to recover the existing 
compatibility of Kyuubi Spark SQL engine.
    
    ## Types of changes :bookmark:
    
    - [ ] Bugfix (non-breaking change which fixes an issue)
    - [x] New feature (non-breaking change which adds functionality)
    - [ ] Breaking change (fix or feature that would cause existing 
functionality to change)
    
    ## Test Plan ๐Ÿงช
    
    Build with Spark 3.5
    ```
    build/dist --tgz --web-ui --spark-provided --flink-provided --hive-provided 
-Pspark-3.5
    ```
    
    It produces both Scala 2.12 and 2.13 Spark SQL engine jars
    - `kyuubi-spark-sql-engine_2.12-1.10.0-SNAPSHOT.jar`
    - `kyuubi-spark-sql-engine_2.13-1.10.0-SNAPSHOT.jar`
    
    Run with Spark 3.4 Scala 2.12
    
    <img width="1639" alt="image" 
src="https://github.com/apache/kyuubi/assets/26535726/caeef30d-7467-4942-a56a-88a7c93ef7cc";>
    
    Run with Spark 3.5 Scala 2.13
    
    <img width="1639" alt="image" 
src="https://github.com/apache/kyuubi/assets/26535726/c339c1e9-c07f-4952-9a57-098b832c889f";>
    
    Run with Spark 4.0.0-preview1 Scala 2.13
    
    <img width="1639" alt="image" 
src="https://github.com/apache/kyuubi/assets/26535726/a3fb6e77-b27e-4634-8acf-245a26b39d2b";>
    
    ---
    
    # Checklist ๐Ÿ“
    
    - [x] This patch was not authored or co-authored using [Generative 
Tooling](https://www.apache.org/legal/generative-tooling.html)
    
    **Be nice. Be informative.**
    
    Closes #6399 from pan3793/ui-4.0.
    
    Closes #6399
    
    e0104f6df [Cheng Pan] nit
    a2f9df4fa [Cheng Pan] nit
    c369ab2e3 [Cheng Pan] nit
    ec1c45f66 [Cheng Pan] nit
    3e05744d6 [Cheng Pan] fix
    a7e38cc1e [Cheng Pan] nit
    fa14a0d98 [Cheng Pan] refactor
    9d0ce6111 [Cheng Pan] A work version
    fc78b58e4 [Cheng Pan] fix startup
    d74c1c0fe [Cheng Pan] fix
    50066f563 [Cheng Pan] nit
    f5ad4c760 [Cheng Pan] Kyuubi UI supports Spark 4.0
    
    Authored-by: Cheng Pan <[email protected]>
    Signed-off-by: Cheng Pan <[email protected]>
---
 externals/kyuubi-spark-sql-engine/pom.xml          |  10 ++
 .../engine/spark/operation/SparkOperation.scala    |   2 +-
 .../engine/spark/session/SparkSessionImpl.scala    |   2 +-
 .../scala/org/apache/spark/ui/EnginePage.scala     |  48 +++---
 .../org/apache/spark/ui/EngineSessionPage.scala    |  26 +++-
 .../main/scala/org/apache/spark/ui/EngineTab.scala | 113 ++++++++++----
 ...lsHelper.scala => HttpServletRequestLike.scala} |  22 ++-
 .../spark/ui/JakartaHttpServletRequest.scala       | 171 +++++++++++++++++++++
 .../apache/spark/ui/JavaxHttpServletRequest.scala  | 170 ++++++++++++++++++++
 .../scala/org/apache/spark/ui/SparkUIUtils.scala   | 103 +++++++++++++
 pom.xml                                            |   7 +
 11 files changed, 609 insertions(+), 65 deletions(-)

diff --git a/externals/kyuubi-spark-sql-engine/pom.xml 
b/externals/kyuubi-spark-sql-engine/pom.xml
index 3155462a4..2d1dd433b 100644
--- a/externals/kyuubi-spark-sql-engine/pom.xml
+++ b/externals/kyuubi-spark-sql-engine/pom.xml
@@ -64,6 +64,11 @@
             <artifactId>grpc-util</artifactId>
         </dependency>
 
+        <dependency>
+            <groupId>net.bytebuddy</groupId>
+            <artifactId>byte-buddy</artifactId>
+        </dependency>
+
         <dependency>
             <groupId>javax.servlet</groupId>
             <artifactId>javax.servlet-api</artifactId>
@@ -233,6 +238,7 @@
                             
<include>com.google.j2objc:j2objc-annotations</include>
                             <include>com.google.protobuf:*</include>
                             <include>dev.failsafe:failsafe</include>
+                            <include>net.bytebuddy:byte-buddy</include>
                             <include>io.etcd:*</include>
                             <include>io.grpc:*</include>
                             <include>io.netty:*</include>
@@ -377,6 +383,10 @@
                             <pattern>com.google.type</pattern>
                             
<shadedPattern>${kyuubi.shade.packageName}.com.google.type</shadedPattern>
                         </relocation>
+                        <relocation>
+                            <pattern>net.bytebuddy</pattern>
+                            
<shadedPattern>${kyuubi.shade.packageName}.net.bytebuddy</shadedPattern>
+                        </relocation>
                     </relocations>
                     <transformers>
                         <transformer 
implementation="org.apache.maven.plugins.shade.resource.ServicesResourceTransformer"></transformer>
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala
index d6d68da0c..b72447ae5 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/operation/SparkOperation.scala
@@ -25,7 +25,7 @@ import org.apache.spark.kyuubi.SparkUtilsHelper.redact
 import org.apache.spark.sql.{DataFrame, Row, SparkSession}
 import org.apache.spark.sql.execution.SQLExecution
 import org.apache.spark.sql.types.{BinaryType, StructField, StructType}
-import org.apache.spark.ui.SparkUIUtilsHelper.formatDuration
+import org.apache.spark.ui.SparkUIUtils.formatDuration
 
 import org.apache.kyuubi.{KyuubiSQLException, Utils}
 import org.apache.kyuubi.config.KyuubiConf
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala
index b0e2d91c5..85c918eb6 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/kyuubi/engine/spark/session/SparkSessionImpl.scala
@@ -21,7 +21,7 @@ import java.util.concurrent.atomic.AtomicLong
 
 import org.apache.commons.lang3.StringUtils
 import org.apache.spark.sql.{AnalysisException, SparkSession}
-import org.apache.spark.ui.SparkUIUtilsHelper.formatDuration
+import org.apache.spark.ui.SparkUIUtils.formatDuration
 
 import org.apache.kyuubi.KyuubiSQLException
 import org.apache.kyuubi.config.KyuubiReservedKeys.KYUUBI_SESSION_HANDLE_KEY
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EnginePage.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EnginePage.scala
index d59b64dd9..36f01e316 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EnginePage.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EnginePage.scala
@@ -20,7 +20,6 @@ package org.apache.spark.ui
 import java.net.URLEncoder
 import java.nio.charset.StandardCharsets.UTF_8
 import java.util.Date
-import javax.servlet.http.HttpServletRequest
 
 import scala.collection.JavaConverters.mapAsScalaMapConverter
 import scala.collection.mutable
@@ -33,10 +32,21 @@ import org.apache.spark.ui.UIUtils._
 import org.apache.kyuubi._
 import org.apache.kyuubi.engine.spark.events.{SessionEvent, 
SparkOperationEvent}
 
-case class EnginePage(parent: EngineTab) extends WebUIPage("") {
+abstract class EnginePage(parent: EngineTab) extends WebUIPage("") {
   private val store = parent.store
 
-  override def render(request: HttpServletRequest): Seq[Node] = {
+  def dispatchRender(req: AnyRef): Seq[Node] = req match {
+    case reqLike: HttpServletRequestLike =>
+      this.render0(reqLike)
+    case javaxReq: javax.servlet.http.HttpServletRequest =>
+      this.render0(HttpServletRequestLike.fromJavax(javaxReq))
+    case jakartaReq: jakarta.servlet.http.HttpServletRequest =>
+      this.render0(HttpServletRequestLike.fromJakarta(jakartaReq))
+    case unsupported =>
+      throw new IllegalArgumentException(s"Unsupported class 
${unsupported.getClass.getName}")
+  }
+
+  def render0(request: HttpServletRequestLike): Seq[Node] = {
     val onlineSession = new mutable.ArrayBuffer[SessionEvent]()
     val closedSession = new mutable.ArrayBuffer[SessionEvent]()
 
@@ -77,7 +87,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
           runningSqlStat.toSeq,
           completedSqlStat.toSeq,
           failedSqlStat.toSeq)
-    UIUtils.headerSparkPage(request, parent.name, content, parent)
+    SparkUIUtils.headerSparkPage(request, parent.name, content, parent)
   }
 
   private def generateBasicStats(): Seq[Node] = {
@@ -131,8 +141,8 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
     </ul>
   }
 
-  private def stop(request: HttpServletRequest): Seq[Node] = {
-    val basePath = UIUtils.prependBaseUri(request, parent.basePath)
+  private def stop(request: HttpServletRequestLike): Seq[Node] = {
+    val basePath = SparkUIUtils.prependBaseUri(request, parent.basePath)
     if (parent.killEnabled) {
       val confirmForceStop =
         s"if (window.confirm('Are you sure you want to stop kyuubi engine 
immediately ?')) " +
@@ -160,7 +170,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
 
   /** Generate stats of running statements for the engine */
   private def generateStatementStatsTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       running: Seq[SparkOperationEvent],
       completed: Seq[SparkOperationEvent],
       failed: Seq[SparkOperationEvent]): Seq[Node] = {
@@ -241,7 +251,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
   }
 
   private def statementStatsTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       sqlTableTag: String,
       parent: EngineTab,
       data: Seq[SparkOperationEvent]): Seq[Node] = {
@@ -255,7 +265,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
         parent,
         data,
         "kyuubi",
-        UIUtils.prependBaseUri(request, parent.basePath),
+        SparkUIUtils.prependBaseUri(request, parent.basePath),
         s"${sqlTableTag}-table").table(sqlTablePage)
     } catch {
       case e @ (_: IllegalArgumentException | _: IndexOutOfBoundsException) =>
@@ -270,7 +280,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
 
   /** Generate stats of online sessions for the engine */
   private def generateSessionStatsTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       online: Seq[SessionEvent],
       closed: Seq[SessionEvent]): Seq[Node] = {
     val content = mutable.ListBuffer[Node]()
@@ -326,7 +336,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
   }
 
   private def sessionTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       sessionTage: String,
       parent: EngineTab,
       data: Seq[SessionEvent]): Seq[Node] = {
@@ -338,7 +348,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
         parent,
         data,
         "kyuubi",
-        UIUtils.prependBaseUri(request, parent.basePath),
+        SparkUIUtils.prependBaseUri(request, parent.basePath),
         s"${sessionTage}-table").table(sessionPage)
     } catch {
       case e @ (_: IllegalArgumentException | _: IndexOutOfBoundsException) =>
@@ -352,7 +362,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
   }
 
   private class SessionStatsPagedTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       parent: EngineTab,
       data: Seq[SessionEvent],
       subPath: String,
@@ -418,7 +428,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
 
     override def row(session: SessionEvent): Seq[Node] = {
       val sessionLink = "%s/%s/session/?id=%s".format(
-        UIUtils.prependBaseUri(request, parent.basePath),
+        SparkUIUtils.prependBaseUri(request, parent.basePath),
         parent.prefix,
         session.sessionId)
       <tr>
@@ -440,7 +450,7 @@ case class EnginePage(parent: EngineTab) extends 
WebUIPage("") {
 }
 
 private class StatementStatsPagedTable(
-    request: HttpServletRequest,
+    request: HttpServletRequestLike,
     parent: EngineTab,
     data: Seq[SparkOperationEvent],
     subPath: String,
@@ -507,7 +517,7 @@ private class StatementStatsPagedTable(
 
   override def row(event: SparkOperationEvent): Seq[Node] = {
     val sessionLink = "%s/%s/session/?id=%s".format(
-      UIUtils.prependBaseUri(request, parent.basePath),
+      SparkUIUtils.prependBaseUri(request, parent.basePath),
       parent.prefix,
       event.sessionId)
     <tr>
@@ -544,7 +554,7 @@ private class StatementStatsPagedTable(
       if (event.executionId.isDefined) {
         <a href={
           "%s/SQL/execution/?id=%s".format(
-            UIUtils.prependBaseUri(request, parent.basePath),
+            SparkUIUtils.prependBaseUri(request, parent.basePath),
             event.executionId.get)
         }>
           {event.executionId.get}
@@ -659,7 +669,7 @@ private object TableSourceUtil {
    * Returns parameter of this table.
    */
   def getRequestTableParameters(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       tableTag: String,
       defaultSortColumn: String): (String, Boolean, Int) = {
     val parameterSortColumn = request.getParameter(s"$tableTag.sort")
@@ -678,7 +688,7 @@ private object TableSourceUtil {
   /**
    * Returns parameters of other tables in the page.
    */
-  def getRequestParameterOtherTable(request: HttpServletRequest, tableTag: 
String): String = {
+  def getRequestParameterOtherTable(request: HttpServletRequestLike, tableTag: 
String): String = {
     request.getParameterMap.asScala
       .filterNot(_._1.startsWith(tableTag))
       .map(parameter => parameter._1 + "=" + parameter._2(0))
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineSessionPage.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineSessionPage.scala
index 46011ceae..a6aae725a 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineSessionPage.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineSessionPage.scala
@@ -18,7 +18,6 @@
 package org.apache.spark.ui
 
 import java.util.Date
-import javax.servlet.http.HttpServletRequest
 
 import scala.collection.mutable
 import scala.xml.Node
@@ -31,7 +30,7 @@ import org.apache.spark.util.Utils
 import org.apache.kyuubi.engine.spark.events.SparkOperationEvent
 
 /** Page for Spark Web UI that shows statistics of jobs running in the engine 
server */
-case class EngineSessionPage(parent: EngineTab)
+abstract class EngineSessionPage(parent: EngineTab)
   extends WebUIPage("session") with Logging {
   val store = parent.store
 
@@ -39,8 +38,19 @@ case class EngineSessionPage(parent: EngineTab)
   private def headerClasses = Seq("sorttable_alpha", "sorttable_alpha")
   private def propertyRow(kv: (String, String)) = 
<tr><td>{kv._1}</td><td>{kv._2}</td></tr>
 
+  def dispatchRender(req: AnyRef): Seq[Node] = req match {
+    case reqLike: HttpServletRequestLike =>
+      this.render0(reqLike)
+    case javaxReq: javax.servlet.http.HttpServletRequest =>
+      this.render0(HttpServletRequestLike.fromJavax(javaxReq))
+    case jakartaReq: jakarta.servlet.http.HttpServletRequest =>
+      this.render0(HttpServletRequestLike.fromJakarta(jakartaReq))
+    case unsupported =>
+      throw new IllegalArgumentException(s"Unsupported class 
${unsupported.getClass.getName}")
+  }
+
   /** Render the page */
-  def render(request: HttpServletRequest): Seq[Node] = {
+  def render0(request: HttpServletRequestLike): Seq[Node] = {
     val parameterId = request.getParameter("id")
     require(parameterId != null && parameterId.nonEmpty, "Missing id 
parameter")
 
@@ -98,7 +108,7 @@ case class EngineSessionPage(parent: EngineTab)
         sessionPropertiesTable ++
         generateSQLStatsTable(request, sessionStat.sessionId)
     }
-    UIUtils.headerSparkPage(request, parent.name + " Session", content, parent)
+    SparkUIUtils.headerSparkPage(request, parent.name + " Session", content, 
parent)
   }
 
   /** Generate basic stats of the engine server */
@@ -128,7 +138,9 @@ case class EngineSessionPage(parent: EngineTab)
     }
 
   /** Generate stats of batch statements of the engine server */
-  private def generateSQLStatsTable(request: HttpServletRequest, sessionID: 
String): Seq[Node] = {
+  private def generateSQLStatsTable(
+      request: HttpServletRequestLike,
+      sessionID: String): Seq[Node] = {
     val running = new mutable.ArrayBuffer[SparkOperationEvent]()
     val completed = new mutable.ArrayBuffer[SparkOperationEvent]()
     val failed = new mutable.ArrayBuffer[SparkOperationEvent]()
@@ -218,7 +230,7 @@ case class EngineSessionPage(parent: EngineTab)
   }
 
   private def statementStatsTable(
-      request: HttpServletRequest,
+      request: HttpServletRequestLike,
       sqlTableTag: String,
       parent: EngineTab,
       data: Seq[SparkOperationEvent]): Seq[Node] = {
@@ -231,7 +243,7 @@ case class EngineSessionPage(parent: EngineTab)
         parent,
         data,
         "kyuubi/session",
-        UIUtils.prependBaseUri(request, parent.basePath),
+        SparkUIUtils.prependBaseUri(request, parent.basePath),
         s"${sqlTableTag}").table(sqlTablePage)
     } catch {
       case e @ (_: IllegalArgumentException | _: IndexOutOfBoundsException) =>
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineTab.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineTab.scala
index 52edcf220..5c031f943 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineTab.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/EngineTab.scala
@@ -17,12 +17,18 @@
 
 package org.apache.spark.ui
 
-import javax.servlet.http.HttpServletRequest
-
 import scala.util.control.NonFatal
 
+import net.bytebuddy.ByteBuddy
+import net.bytebuddy.dynamic.loading.ClassLoadingStrategy
+import net.bytebuddy.dynamic.scaffold.subclass.ConstructorStrategy
+import net.bytebuddy.implementation.MethodCall
+import net.bytebuddy.matcher.ElementMatchers.{isConstructor, named}
+import org.apache.spark.util.{Utils => SparkUtils}
+
 import org.apache.kyuubi.{Logging, Utils}
 import org.apache.kyuubi.config.KyuubiConf
+import 
org.apache.kyuubi.engine.spark.KyuubiSparkUtil.SPARK_ENGINE_RUNTIME_VERSION
 import org.apache.kyuubi.engine.spark.SparkSQLEngine
 import org.apache.kyuubi.engine.spark.events.EngineEventsStore
 import org.apache.kyuubi.service.ServiceState
@@ -53,49 +59,96 @@ case class EngineTab(
       .getOrElse(0L)
   }
 
+  private val enginePage = {
+    val dispatchMethod = classOf[EnginePage].getMethod("dispatchRender", 
classOf[AnyRef])
+    new ByteBuddy()
+      .subclass(classOf[EnginePage], 
ConstructorStrategy.Default.IMITATE_SUPER_CLASS_PUBLIC)
+      .method(isConstructor()).intercept(MethodCall.invokeSuper())
+      
.method(named("render")).intercept(MethodCall.invoke(dispatchMethod).withAllArguments())
+      .make()
+      .load(SparkUtils.getContextOrSparkClassLoader, 
ClassLoadingStrategy.Default.INJECTION)
+      .getLoaded
+      .getDeclaredConstructor(classOf[EngineTab])
+      .newInstance(this)
+  }
+
+  private val engineSessionPage = {
+    val dispatchMethod = 
classOf[EngineSessionPage].getMethod("dispatchRender", classOf[AnyRef])
+    new ByteBuddy()
+      .subclass(classOf[EngineSessionPage], 
ConstructorStrategy.Default.IMITATE_SUPER_CLASS_PUBLIC)
+      .method(isConstructor()).intercept(MethodCall.invokeSuper())
+      
.method(named("render")).intercept(MethodCall.invoke(dispatchMethod).withAllArguments())
+      .make()
+      .load(SparkUtils.getContextOrSparkClassLoader, 
ClassLoadingStrategy.Default.INJECTION)
+      .getLoaded
+      .getDeclaredConstructor(classOf[EngineTab])
+      .newInstance(this)
+  }
+
   sparkUI.foreach { ui =>
-    this.attachPage(EnginePage(this))
-    this.attachPage(EngineSessionPage(this))
+    this.attachPage(enginePage)
+    this.attachPage(engineSessionPage)
     ui.attachTab(this)
     Utils.addShutdownHook(() => ui.detachTab(this))
   }
 
   sparkUI.foreach { ui =>
     try {
-      // [KYUUBI #3627]: the official spark release uses the shaded and 
relocated jetty classes,
-      // but if we use sbt to build for testing, e.g. docker image, it still 
uses the vanilla
-      // jetty classes.
       val sparkServletContextHandlerClz = DynClasses.builder()
+        // for official Spark releases and distributions built via Maven
         .impl("org.sparkproject.jetty.servlet.ServletContextHandler")
+        // for distributions built via SBT
         .impl("org.eclipse.jetty.servlet.ServletContextHandler")
         .buildChecked()
       val attachHandlerMethod = DynMethods.builder("attachHandler")
         .impl("org.apache.spark.ui.SparkUI", sparkServletContextHandlerClz)
         .buildChecked(ui)
-      val createRedirectHandlerMethod = 
DynMethods.builder("createRedirectHandler")
-        .impl(
-          "org.apache.spark.ui.JettyUtils",
-          classOf[String],
-          classOf[String],
-          classOf[HttpServletRequest => Unit],
-          classOf[String],
-          classOf[Set[String]])
-        .buildStaticChecked()
-
-      attachHandlerMethod
-        .invoke(
+
+      if (SPARK_ENGINE_RUNTIME_VERSION >= "4.0") {
+        attachHandlerMethod.invoke {
+          val createRedirectHandlerMethod = 
DynMethods.builder("createRedirectHandler")
+            .impl(
+              JettyUtils.getClass,
+              classOf[String],
+              classOf[String],
+              classOf[jakarta.servlet.http.HttpServletRequest => Unit],
+              classOf[String],
+              classOf[Set[String]])
+            .buildChecked(JettyUtils)
+
+          val killHandler =
+            (_: jakarta.servlet.http.HttpServletRequest) => handleKill()
+          val gracefulKillHandler =
+            (_: jakarta.servlet.http.HttpServletRequest) => 
handleGracefulKill()
+
           createRedirectHandlerMethod
-            .invoke("/kyuubi/stop", "/kyuubi", handleKillRequest _, "", 
Set("GET", "POST")))
+            .invoke("/kyuubi/stop", "/kyuubi", killHandler, "", Set("GET", 
"POST"))
+          createRedirectHandlerMethod
+            .invoke("/kyuubi/gracefulstop", "/kyuubi", gracefulKillHandler, 
"", Set("GET", "POST"))
+        }
+      } else {
+        val createRedirectHandlerMethod = 
DynMethods.builder("createRedirectHandler")
+          .impl(
+            JettyUtils.getClass,
+            classOf[String],
+            classOf[String],
+            classOf[javax.servlet.http.HttpServletRequest => Unit],
+            classOf[String],
+            classOf[Set[String]])
+          .buildChecked(JettyUtils)
+
+        attachHandlerMethod.invoke {
+          val killHandler =
+            (_: javax.servlet.http.HttpServletRequest) => handleKill()
+          val gracefulKillHandler =
+            (_: javax.servlet.http.HttpServletRequest) => handleGracefulKill()
 
-      attachHandlerMethod
-        .invoke(
           createRedirectHandlerMethod
-            .invoke(
-              "/kyuubi/gracefulstop",
-              "/kyuubi",
-              handleGracefulKillRequest _,
-              "",
-              Set("GET", "POST")))
+            .invoke("/kyuubi/stop", "/kyuubi", killHandler, "", Set("GET", 
"POST"))
+          createRedirectHandlerMethod
+            .invoke("/kyuubi/gracefulstop", "/kyuubi", gracefulKillHandler, 
"", Set("GET", "POST"))
+        }
+      }
     } catch {
       case NonFatal(cause) => reportInstallError(cause)
       case cause: NoClassDefFoundError => reportInstallError(cause)
@@ -109,13 +162,13 @@ case class EngineTab(
       cause)
   }
 
-  def handleKillRequest(request: HttpServletRequest): Unit = {
+  def handleKill(): Unit = {
     if (killEnabled && engine.isDefined && engine.get.getServiceState != 
ServiceState.STOPPED) {
       engine.get.stop()
     }
   }
 
-  def handleGracefulKillRequest(request: HttpServletRequest): Unit = {
+  def handleGracefulKill(): Unit = {
     if (killEnabled && engine.isDefined && engine.get.getServiceState != 
ServiceState.STOPPED) {
       engine.get.gracefulStop()
     }
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtilsHelper.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/HttpServletRequestLike.scala
similarity index 65%
rename from 
externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtilsHelper.scala
rename to 
externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/HttpServletRequestLike.scala
index c60fe4466..7b66cb87b 100644
--- 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtilsHelper.scala
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/HttpServletRequestLike.scala
@@ -17,13 +17,21 @@
 
 package org.apache.spark.ui
 
-/**
- * A place to invoke non-public APIs of [[UIUtils]], anything to be added here 
need to
- * think twice
- */
-object SparkUIUtilsHelper {
+import java.util
+
+trait HttpServletRequestLike {
+
+  def getParameter(name: String): String
+
+  def getParameterMap: util.Map[String, Array[String]]
+}
+
+object HttpServletRequestLike {
+  def fromJavax(req: javax.servlet.http.HttpServletRequest): 
JavaxHttpServletRequest = {
+    new JavaxHttpServletRequest(req)
+  }
 
-  def formatDuration(ms: Long): String = {
-    UIUtils.formatDuration(ms)
+  def fromJakarta(req: jakarta.servlet.http.HttpServletRequest): 
JakartaHttpServletRequest = {
+    new JakartaHttpServletRequest(req)
   }
 }
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JakartaHttpServletRequest.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JakartaHttpServletRequest.scala
new file mode 100644
index 000000000..9219f294a
--- /dev/null
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JakartaHttpServletRequest.scala
@@ -0,0 +1,171 @@
+/*
+ * 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.spark.ui
+
+import java.io.BufferedReader
+import java.security.Principal
+import java.util
+import java.util.Locale
+
+import jakarta.servlet._
+import jakarta.servlet.http._
+
+class JakartaHttpServletRequest(req: HttpServletRequest)
+  extends HttpServletRequest with HttpServletRequestLike {
+  override def getAuthType: String = req.getAuthType
+
+  override def getCookies: Array[Cookie] = req.getCookies
+
+  override def getDateHeader(s: String): Long = req.getDateHeader(s)
+
+  override def getHeader(s: String): String = req.getHeader(s)
+
+  override def getHeaders(s: String): util.Enumeration[String] = 
req.getHeaders(s)
+
+  override def getHeaderNames: util.Enumeration[String] = req.getHeaderNames
+
+  override def getIntHeader(s: String): Int = req.getIntHeader(s)
+
+  override def getMethod: String = req.getMethod
+
+  override def getPathInfo: String = req.getPathInfo
+
+  override def getPathTranslated: String = req.getPathTranslated
+
+  override def getContextPath: String = req.getContextPath
+
+  override def getQueryString: String = req.getQueryString
+
+  override def getRemoteUser: String = req.getRemoteUser
+
+  override def isUserInRole(s: String): Boolean = req.isUserInRole(s)
+
+  override def getUserPrincipal: Principal = req.getUserPrincipal
+
+  override def getRequestedSessionId: String = req.getRequestedSessionId
+
+  override def getRequestURI: String = req.getRequestURI
+
+  override def getRequestURL: StringBuffer = req.getRequestURL
+
+  override def getServletPath: String = req.getServletPath
+
+  override def getSession(b: Boolean): HttpSession = req.getSession(b)
+
+  override def getSession: HttpSession = req.getSession
+
+  override def changeSessionId(): String = req.changeSessionId()
+
+  override def isRequestedSessionIdValid: Boolean = 
req.isRequestedSessionIdValid
+
+  override def isRequestedSessionIdFromCookie: Boolean = 
req.isRequestedSessionIdFromCookie
+
+  override def isRequestedSessionIdFromURL: Boolean = 
req.isRequestedSessionIdFromURL
+
+  override def isRequestedSessionIdFromUrl: Boolean = 
req.isRequestedSessionIdFromUrl
+
+  override def authenticate(httpServletResponse: HttpServletResponse): Boolean 
=
+    req.authenticate(httpServletResponse)
+
+  override def login(s: String, s1: String): Unit = req.login(s, s1)
+
+  override def logout(): Unit = req.logout()
+
+  override def getParts: util.Collection[Part] = req.getParts
+
+  override def getPart(s: String): Part = req.getPart(s)
+
+  override def upgrade[T <: HttpUpgradeHandler](aClass: Class[T]): T = 
req.upgrade(aClass)
+
+  override def getAttribute(s: String): AnyRef = req.getAttribute(s)
+
+  override def getAttributeNames: util.Enumeration[String] = 
req.getAttributeNames
+
+  override def getCharacterEncoding: String = req.getCharacterEncoding
+
+  override def setCharacterEncoding(s: String): Unit = 
req.setCharacterEncoding(s)
+
+  override def getContentLength: Int = req.getContentLength
+
+  override def getContentLengthLong: Long = req.getContentLengthLong
+
+  override def getContentType: String = req.getContentType
+
+  override def getInputStream: ServletInputStream = req.getInputStream
+
+  override def getParameter(s: String): String = req.getParameter(s)
+
+  override def getParameterNames: util.Enumeration[String] = 
req.getParameterNames
+
+  override def getParameterValues(s: String): Array[String] = 
req.getParameterValues(s)
+
+  override def getParameterMap: util.Map[String, Array[String]] = 
req.getParameterMap
+
+  override def getProtocol: String = req.getProtocol
+
+  override def getScheme: String = req.getScheme
+
+  override def getServerName: String = req.getServerName
+
+  override def getServerPort: Int = req.getServerPort
+
+  override def getReader: BufferedReader = req.getReader
+
+  override def getRemoteAddr: String = req.getRemoteAddr
+
+  override def getRemoteHost: String = req.getRemoteHost
+
+  override def setAttribute(s: String, o: Any): Unit = req.setAttribute(s, o)
+
+  override def removeAttribute(s: String): Unit = req.removeAttribute(s)
+
+  override def getLocale: Locale = req.getLocale
+
+  override def getLocales: util.Enumeration[Locale] = req.getLocales
+
+  override def isSecure: Boolean = req.isSecure
+
+  override def getRequestDispatcher(s: String): RequestDispatcher = 
req.getRequestDispatcher(s)
+
+  override def getRealPath(s: String): String = req.getRealPath(s)
+
+  override def getRemotePort: Int = req.getRemotePort
+
+  override def getLocalName: String = req.getLocalName
+
+  override def getLocalAddr: String = req.getLocalAddr
+
+  override def getLocalPort: Int = req.getLocalPort
+
+  override def getServletContext: ServletContext = req.getServletContext
+
+  override def startAsync(): AsyncContext = req.startAsync()
+
+  override def startAsync(
+      servletRequest: ServletRequest,
+      servletResponse: ServletResponse): AsyncContext =
+    req.startAsync(servletRequest, servletResponse)
+
+  override def isAsyncStarted: Boolean = req.isAsyncStarted
+
+  override def isAsyncSupported: Boolean = req.isAsyncSupported
+
+  override def getAsyncContext: AsyncContext = req.getAsyncContext
+
+  override def getDispatcherType: DispatcherType = req.getDispatcherType
+}
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JavaxHttpServletRequest.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JavaxHttpServletRequest.scala
new file mode 100644
index 000000000..d77948001
--- /dev/null
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/JavaxHttpServletRequest.scala
@@ -0,0 +1,170 @@
+/*
+ * 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.spark.ui
+
+import java.io.BufferedReader
+import java.security.Principal
+import java.util
+import java.util.Locale
+import javax.servlet._
+import javax.servlet.http._
+
+class JavaxHttpServletRequest(req: HttpServletRequest)
+  extends HttpServletRequest with HttpServletRequestLike {
+  override def getAuthType: String = req.getAuthType
+
+  override def getCookies: Array[Cookie] = req.getCookies
+
+  override def getDateHeader(s: String): Long = req.getDateHeader(s)
+
+  override def getHeader(s: String): String = req.getHeader(s)
+
+  override def getHeaders(s: String): util.Enumeration[String] = 
req.getHeaders(s)
+
+  override def getHeaderNames: util.Enumeration[String] = req.getHeaderNames
+
+  override def getIntHeader(s: String): Int = req.getIntHeader(s)
+
+  override def getMethod: String = req.getMethod
+
+  override def getPathInfo: String = req.getPathInfo
+
+  override def getPathTranslated: String = req.getPathTranslated
+
+  override def getContextPath: String = req.getContextPath
+
+  override def getQueryString: String = req.getQueryString
+
+  override def getRemoteUser: String = req.getRemoteUser
+
+  override def isUserInRole(s: String): Boolean = req.isUserInRole(s)
+
+  override def getUserPrincipal: Principal = req.getUserPrincipal
+
+  override def getRequestedSessionId: String = req.getRequestedSessionId
+
+  override def getRequestURI: String = req.getRequestURI
+
+  override def getRequestURL: StringBuffer = req.getRequestURL
+
+  override def getServletPath: String = req.getServletPath
+
+  override def getSession(b: Boolean): HttpSession = req.getSession(b)
+
+  override def getSession: HttpSession = req.getSession
+
+  override def changeSessionId(): String = req.changeSessionId()
+
+  override def isRequestedSessionIdValid: Boolean = 
req.isRequestedSessionIdValid
+
+  override def isRequestedSessionIdFromCookie: Boolean = 
req.isRequestedSessionIdFromCookie
+
+  override def isRequestedSessionIdFromURL: Boolean = 
req.isRequestedSessionIdFromURL
+
+  override def isRequestedSessionIdFromUrl: Boolean = 
req.isRequestedSessionIdFromUrl
+
+  override def authenticate(httpServletResponse: HttpServletResponse): Boolean 
=
+    req.authenticate(httpServletResponse)
+
+  override def login(s: String, s1: String): Unit = req.login(s, s1)
+
+  override def logout(): Unit = req.logout()
+
+  override def getParts: util.Collection[Part] = req.getParts
+
+  override def getPart(s: String): Part = req.getPart(s)
+
+  override def upgrade[T <: HttpUpgradeHandler](aClass: Class[T]): T = 
req.upgrade(aClass)
+
+  override def getAttribute(s: String): AnyRef = req.getAttribute(s)
+
+  override def getAttributeNames: util.Enumeration[String] = 
req.getAttributeNames
+
+  override def getCharacterEncoding: String = req.getCharacterEncoding
+
+  override def setCharacterEncoding(s: String): Unit = 
req.setCharacterEncoding(s)
+
+  override def getContentLength: Int = req.getContentLength
+
+  override def getContentLengthLong: Long = req.getContentLengthLong
+
+  override def getContentType: String = req.getContentType
+
+  override def getInputStream: ServletInputStream = req.getInputStream
+
+  override def getParameter(s: String): String = req.getParameter(s)
+
+  override def getParameterNames: util.Enumeration[String] = 
req.getParameterNames
+
+  override def getParameterValues(s: String): Array[String] = 
req.getParameterValues(s)
+
+  override def getParameterMap: util.Map[String, Array[String]] = 
req.getParameterMap
+
+  override def getProtocol: String = req.getProtocol
+
+  override def getScheme: String = req.getScheme
+
+  override def getServerName: String = req.getServerName
+
+  override def getServerPort: Int = req.getServerPort
+
+  override def getReader: BufferedReader = req.getReader
+
+  override def getRemoteAddr: String = req.getRemoteAddr
+
+  override def getRemoteHost: String = req.getRemoteHost
+
+  override def setAttribute(s: String, o: Any): Unit = req.setAttribute(s, o)
+
+  override def removeAttribute(s: String): Unit = req.removeAttribute(s)
+
+  override def getLocale: Locale = req.getLocale
+
+  override def getLocales: util.Enumeration[Locale] = req.getLocales
+
+  override def isSecure: Boolean = req.isSecure
+
+  override def getRequestDispatcher(s: String): RequestDispatcher = 
req.getRequestDispatcher(s)
+
+  override def getRealPath(s: String): String = req.getRealPath(s)
+
+  override def getRemotePort: Int = req.getRemotePort
+
+  override def getLocalName: String = req.getLocalName
+
+  override def getLocalAddr: String = req.getLocalAddr
+
+  override def getLocalPort: Int = req.getLocalPort
+
+  override def getServletContext: ServletContext = req.getServletContext
+
+  override def startAsync(): AsyncContext = req.startAsync()
+
+  override def startAsync(
+      servletRequest: ServletRequest,
+      servletResponse: ServletResponse): AsyncContext =
+    req.startAsync(servletRequest, servletResponse)
+
+  override def isAsyncStarted: Boolean = req.isAsyncStarted
+
+  override def isAsyncSupported: Boolean = req.isAsyncSupported
+
+  override def getAsyncContext: AsyncContext = req.getAsyncContext
+
+  override def getDispatcherType: DispatcherType = req.getDispatcherType
+}
diff --git 
a/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtils.scala
 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtils.scala
new file mode 100644
index 000000000..4862372a1
--- /dev/null
+++ 
b/externals/kyuubi-spark-sql-engine/src/main/scala/org/apache/spark/ui/SparkUIUtils.scala
@@ -0,0 +1,103 @@
+/*
+ * 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.spark.ui
+
+import java.lang.{Boolean => JBoolean}
+
+import scala.xml.Node
+
+import 
org.apache.kyuubi.engine.spark.KyuubiSparkUtil.SPARK_ENGINE_RUNTIME_VERSION
+import org.apache.kyuubi.util.reflect.DynMethods
+
+/**
+ * A place to invoke non-public APIs of [[UIUtils]], anything to be added here 
need to
+ * think twice
+ */
+object SparkUIUtils {
+
+  def formatDuration(ms: Long): String = {
+    UIUtils.formatDuration(ms)
+  }
+
+  def headerSparkPage(
+      request: HttpServletRequestLike,
+      title: String,
+      content: Seq[Node],
+      activeTab: SparkUITab,
+      helpText: Option[String] = None,
+      showVisualization: JBoolean = false,
+      useDataTables: JBoolean = false): Seq[Node] = {
+    val headerSparkPageMethod = if (SPARK_ENGINE_RUNTIME_VERSION >= "4.0") {
+      DynMethods.builder("headerSparkPage")
+        .impl(
+          UIUtils.getClass,
+          classOf[jakarta.servlet.http.HttpServletRequest],
+          classOf[String],
+          classOf[() => Seq[Node]],
+          classOf[SparkUITab],
+          classOf[Option[String]],
+          classOf[Boolean],
+          classOf[Boolean])
+        .buildChecked(UIUtils)
+    } else {
+      DynMethods.builder("headerSparkPage")
+        .impl(
+          UIUtils.getClass,
+          classOf[javax.servlet.http.HttpServletRequest],
+          classOf[String],
+          classOf[() => Seq[Node]],
+          classOf[SparkUITab],
+          classOf[Option[String]],
+          classOf[Boolean],
+          classOf[Boolean])
+        .buildChecked(UIUtils)
+    }
+    headerSparkPageMethod.invoke[Seq[Node]](
+      request,
+      title,
+      () => content,
+      activeTab,
+      helpText,
+      showVisualization,
+      useDataTables)
+  }
+
+  def prependBaseUri(
+      request: HttpServletRequestLike,
+      basePath: String = "",
+      resource: String = ""): String = {
+    val prependBaseUriMethod = if (SPARK_ENGINE_RUNTIME_VERSION >= "4.0") {
+      DynMethods.builder("prependBaseUri")
+        .impl(
+          UIUtils.getClass,
+          classOf[jakarta.servlet.http.HttpServletRequest],
+          classOf[String],
+          classOf[String])
+        .buildChecked(UIUtils)
+    } else {
+      DynMethods.builder("prependBaseUri")
+        .impl(
+          UIUtils.getClass,
+          classOf[javax.servlet.http.HttpServletRequest],
+          classOf[String],
+          classOf[String])
+        .buildChecked(UIUtils)
+    }
+    prependBaseUriMethod.invoke[String](request, basePath, resource)
+  }
+}
diff --git a/pom.xml b/pom.xml
index d83187d65..513271d0d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -125,6 +125,7 @@
         <antlr.st4.version>4.3.4</antlr.st4.version>
         
<apache.archive.dist>https://archive.apache.org/dist</apache.archive.dist>
         <atlas.version>2.3.0</atlas.version>
+        <byte-buddy.version>1.14.15</byte-buddy.version>
         <bouncycastle.version>1.78</bouncycastle.version>
         <codahale.metrics.version>4.2.23</codahale.metrics.version>
         <commons-cli.version>1.5.0</commons-cli.version>
@@ -838,6 +839,12 @@
                 <scope>test</scope>
             </dependency>
 
+            <dependency>
+                <groupId>net.bytebuddy</groupId>
+                <artifactId>byte-buddy</artifactId>
+                <version>${byte-buddy.version}</version>
+            </dependency>
+
             <dependency>
                 <groupId>junit</groupId>
                 <artifactId>junit</artifactId>


Reply via email to