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

wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-java.git


The following commit(s) were added to refs/heads/main by this push:
     new 64035d609a Add Jetty 12 and Struts 7 (Jakarta) agent plugins (#813)
64035d609a is described below

commit 64035d609a9f5d3bf0ff74eac190a74c0b5cd26d
Author: 吴晟 Wu Sheng <[email protected]>
AuthorDate: Tue Jul 7 11:21:52 2026 +0800

    Add Jetty 12 and Struts 7 (Jakarta) agent plugins (#813)
    
    Jetty 12 removed the concrete HttpChannel whose handle() the merged 
jetty-server
    plugin enhances, and moved request handling to the async, namespace-neutral 
core
    API Server#handle(Request, Response, Callback). Add a separate 
jetty-server-12.x
    plugin that enhances it and records an async entry span (finished with the
    response status when the wrapped Callback completes).
    
    Struts 7 relocated DefaultActionInvocation from com.opensymphony.xwork2 to
    org.apache.struts2 and switched to jakarta.servlet. Add a struts2-7.x plugin
    enhancing the new class (a distinct package from struts2-2.x avoids 
collision).
    
    Both reuse existing components (JettyServer, Struts2). Add plugin test 
scenarios
    covering the versions (Jetty 12.0.x/12.1.x; Struts 7.0.x/7.1.x/7.2.x) and 
update
    the supported list, plugin list and CHANGES. Each scenario drives the full 
RPC
    validation path in one trace - a server entry span, a client exit span, and 
a
    second entry span on a nested internal call carrying a cross-process ref - 
so
    both the entry and the context-extract sides of the plugin are proven, not 
just
    a lone entry span. The struts scenario uses a lightweight httpheader result
    instead of a JSP view to keep the cold first request under the plugin-test
    framework's 3s entry-service timeout.
    
    Add test/plugin/CLAUDE.md (plugin-test-first approach, the RPC/client 
validation
    paths, the one-version-per-minor rule, how the jvm/tomcat containers and 
GitHub
    Actions lanes pin their JDK/Tomcat base image, and the cold-start timeout 
gotcha)
    and document the supported jvm/tomcat containers in Plugin-test.md.
---
 .github/workflows/plugins-jdk17-test.0.yaml        |   1 +
 .github/workflows/plugins-jdk17-test.1.yaml        |   1 +
 .gitignore                                         |   5 +
 CHANGES.md                                         |   2 +
 apm-sniffer/apm-sdk-plugin/CLAUDE.md               |   2 +
 .../{ => jetty-server-12.x-plugin}/pom.xml         |  36 +++---
 .../plugin/jetty/v12/server/HandleInterceptor.java | 100 +++++++++++++++
 .../apm/plugin/jetty/v12/server/SwCallback.java    |  98 +++++++++++++++
 .../define/Jetty12ServerInstrumentation.java       |  89 ++++++++++++++
 .../src/main/resources/skywalking-plugin.def       |  18 +++
 apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml    |   1 +
 apm-sniffer/apm-sdk-plugin/pom.xml                 |   1 +
 .../{jetty-plugin => struts2-7.x-plugin}/pom.xml   |  40 +++---
 .../apm/plugin/struts2/v7/Struts2Interceptor.java  |  82 +++++++++++++
 .../struts2/v7/define/Struts2Instrumentation.java  |  75 ++++++++++++
 .../src/main/resources/skywalking-plugin.def       |  18 +++
 .../setup/service-agent/java-agent/Plugin-list.md  |   2 +
 .../setup/service-agent/java-agent/Plugin-test.md  |   8 +-
 .../service-agent/java-agent/Supported-list.md     |   6 +-
 test/plugin/CLAUDE.md                              | 134 +++++++++++++++++++++
 .../scenarios/jetty-12.x-scenario/bin/startup.sh   |  21 ++++
 .../jetty-12.x-scenario/config/expectedData.yaml   |  86 +++++++++++++
 .../jetty-12.x-scenario/configuration.yml          |  22 ++++
 test/plugin/scenarios/jetty-12.x-scenario/pom.xml  | 128 ++++++++++++++++++++
 .../src/main/assembly/assembly.xml                 |  41 +++++++
 .../apm/testcase/jetty12xserver/Application.java   |  37 ++++++
 .../jetty12xserver/handler/CaseHandler.java        |  67 +++++++++++
 .../src/main/resources/log4j2.xml                  |  30 +++++
 .../jetty-12.x-scenario/support-version.list       |  18 +++
 .../struts2.7-scenario/config/expectedData.yaml    |  72 +++++++++++
 .../scenarios/struts2.7-scenario/configuration.yml |  21 ++++
 test/plugin/scenarios/struts2.7-scenario/pom.xml   |  96 +++++++++++++++
 .../apm/testcase/struts/Case1Action.java           |  28 +++++
 .../skywalking/apm/testcase/struts/CaseAction.java |  47 ++++++++
 .../apm/testcase/struts/HealthCheckServlet.java    |  41 +++++++
 .../src/main/resources/log4j2.xml                  |  30 +++++
 .../src/main/resources/struts.xml                  |  46 +++++++
 .../src/main/webapp/WEB-INF/web.xml                |  44 +++++++
 .../struts2.7-scenario/support-version.list        |  19 +++
 39 files changed, 1578 insertions(+), 35 deletions(-)

diff --git a/.github/workflows/plugins-jdk17-test.0.yaml 
b/.github/workflows/plugins-jdk17-test.0.yaml
index 49b3675c01..b62166b155 100644
--- a/.github/workflows/plugins-jdk17-test.0.yaml
+++ b/.github/workflows/plugins-jdk17-test.0.yaml
@@ -79,6 +79,7 @@ jobs:
           - grizzly-2.3.x-4.x-workthreadpool-scenario
           - jetty-11.x-scenario
           - jetty-10.x-scenario
+          - jetty-12.x-scenario
           - spring-ai-1.x-scenario
           - spring-rabbitmq-scenario
           - graphql-20plus-scenario
diff --git a/.github/workflows/plugins-jdk17-test.1.yaml 
b/.github/workflows/plugins-jdk17-test.1.yaml
index 6be997dc0e..e4f7144dfd 100644
--- a/.github/workflows/plugins-jdk17-test.1.yaml
+++ b/.github/workflows/plugins-jdk17-test.1.yaml
@@ -72,6 +72,7 @@ jobs:
         case:
           - spring-6.x-scenario
           - spring-boot-3.x-scenario
+          - struts2.7-scenario
           - resteasy-6.x-scenario
           - gateway-4.x-scenario
           - gateway-4.1.2.x-scenario
diff --git a/.gitignore b/.gitignore
index da77f29d42..1efb481266 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,8 @@ packages/
 /test/jacoco/*.exec
 test/jacoco
 .claude/settings.local.json
+
+# Generated by tools/plugin/check-javaagent-plugin-list.sh (not source)
+/javaagent-position.txt
+/tools/plugin/genernate-javaagent-plugin-list.txt
+/tools/plugin/md-javaagent-plugin-list.txt
diff --git a/CHANGES.md b/CHANGES.md
index 9f994787ff..48310efdab 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -7,6 +7,8 @@ Release Notes.
 
 * Fix `plugin.http.include_http_headers` not working on Spring Boot 3.x / 
Jakarta EE (apache/skywalking#13938).
 * Unify the Tomcat plugins into a single `tomcat` plugin (Tomcat 7 - 10) and 
the Jetty server plugins into a single `jetty-server` plugin (Jetty 9 - 11), 
each supporting both `javax.servlet` and `jakarta.servlet`. Breaking change: 
plugin names `tomcat-7.x/8.x` and `tomcat-10.x` are replaced by `tomcat`, and 
`jetty-server-9.x` and `jetty-server-11.x` by `jetty-server`; update 
`plugin.exclude_plugins` if you reference the old names.
+* Add a Jetty 12 server plugin (`jetty-server-12.x`). Jetty 12 removed the 
`HttpChannel` handle target and moved request handling to the async 
`Server#handle(Request, Response, Callback)` core API, so it needs a separate 
plugin from the merged `jetty-server`.
+* Add a Struts 7 plugin (`struts2-7.x`) for Jakarta Struts, whose 
`DefaultActionInvocation` moved to `org.apache.struts2`.
 * Added support for Lettuce reactive Redis commands.
 * Add Spring AI 1.x plugin and GenAI layer.
 * Fix httpclient-5.x plugin injecting sw8 propagation headers into ClickHouse 
HTTP requests (port 8123), causing HTTP 400. Add `PROPAGATION_EXCLUDE_PORTS` 
config to skip tracing (including header injection) for specified ports in the 
classic client interceptor.
diff --git a/apm-sniffer/apm-sdk-plugin/CLAUDE.md 
b/apm-sniffer/apm-sdk-plugin/CLAUDE.md
index b839fbe73b..81de9bc670 100644
--- a/apm-sniffer/apm-sdk-plugin/CLAUDE.md
+++ b/apm-sniffer/apm-sdk-plugin/CLAUDE.md
@@ -251,6 +251,8 @@ histogram.addValue(3);
 
 The plugin test framework verifies plugin functionality using Docker 
containers with real services and a mock OAP backend.
 
+> See `test/plugin/CLAUDE.md` for the testing philosophy: a plugin's scenario 
(with one-version-per-minor coverage) is the **key** test — it proves the 
interceptor's cast/fetch-data logic and version compatibility by running the 
real framework. Do **not** over-engineer with many mock-based unit tests for a 
plugin.
+
 ### Environment Requirements
 - MacOS/Linux
 - JDK 8+
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
similarity index 55%
copy from apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
copy to apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
index 1ac21fb14d..dfc04b5933 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/pom.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one or more
   ~ contributor license agreements.  See the NOTICE file distributed with
@@ -18,27 +17,36 @@
   -->
 
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-
     <parent>
+        <artifactId>jetty-plugins</artifactId>
         <groupId>org.apache.skywalking</groupId>
-        <artifactId>apm-sdk-plugin</artifactId>
         <version>9.7.0-SNAPSHOT</version>
     </parent>
+    <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>jetty-plugins</artifactId>
-    <modules>
-        <module>jetty-client-9.x-plugin</module>
-        <module>jetty-client-9.0-plugin</module>
-        <module>jetty-server-plugin</module>
-        <module>jetty-client-11.x-plugin</module>
-    </modules>
-    <packaging>pom</packaging>
+    <artifactId>apm-jetty-server-12.x-plugin</artifactId>
+    <packaging>jar</packaging>
 
-    <name>jetty-plugin</name>
+    <name>jetty-server-12.x-plugin</name>
     <url>http://maven.apache.org</url>
 
+    <!--
+      Jetty 12 removed the concrete org.eclipse.jetty.server.HttpChannel class 
that the merged
+      jetty-server plugin enhances, and redesigned request handling around the 
namespace-neutral
+      core API org.eclipse.jetty.server.Server#handle(Request, Response, 
Callback), which is async
+      (the response status is only known when the Callback completes). Hence a 
separate plugin that
+      does not use the servlet-commons wrapper. Covers Jetty 12.0.x and 12.1.x 
(same signature).
+    -->
     <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <jetty-server.version>12.0.36</jetty-server.version>
     </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>${jetty-server.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
 </project>
diff --git 
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
new file mode 100644
index 0000000000..74e08614dd
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/HandleInterceptor.java
@@ -0,0 +1,100 @@
+/*
+ * 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.skywalking.apm.plugin.jetty.v12.server;
+
+import java.lang.reflect.Method;
+import org.apache.skywalking.apm.agent.core.context.CarrierItem;
+import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
+import org.apache.skywalking.apm.agent.core.context.ContextManager;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.eclipse.jetty.http.HttpFields;
+import org.eclipse.jetty.http.HttpURI;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Enhances the core, namespace-neutral {@code 
org.eclipse.jetty.server.Server#handle(Request, Response,
+ * Callback)} — the single top-level entry invoked once per request in Jetty 
12. Handling is async, so
+ * the entry span is prepared for async in beforeMethod, its synchronous 
segment ends in afterMethod, and
+ * it is finished (with the response status) when the wrapped {@link 
SwCallback} completes.
+ */
+public class HandleInterceptor implements InstanceMethodsAroundInterceptor {
+
+    private static final ThreadLocal<SwCallback> HOLDER = new 
ThreadLocal<SwCallback>();
+
+    @Override
+    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] 
allArguments, Class<?>[] argumentsTypes,
+                             MethodInterceptResult result) throws Throwable {
+        Request request = (Request) allArguments[0];
+        Response response = (Response) allArguments[1];
+        Callback callback = (Callback) allArguments[2];
+
+        ContextCarrier contextCarrier = new ContextCarrier();
+        HttpFields headers = request.getHeaders();
+        CarrierItem next = contextCarrier.items();
+        while (next.hasNext()) {
+            next = next.next();
+            next.setHeadValue(headers.get(next.getHeadKey()));
+        }
+
+        HttpURI uri = request.getHttpURI();
+        AbstractSpan span = ContextManager.createEntrySpan(uri.getPath(), 
contextCarrier);
+        Tags.URL.set(span, uri.asString());
+        Tags.HTTP.METHOD.set(span, request.getMethod());
+        span.setComponent(ComponentsDefine.JETTY_SERVER);
+        SpanLayer.asHttp(span);
+        span.prepareForAsync();
+
+        SwCallback swCallback = new SwCallback(callback, response, span);
+        allArguments[2] = swCallback;
+        HOLDER.set(swCallback);
+    }
+
+    @Override
+    public Object afterMethod(EnhancedInstance objInst, Method method, 
Object[] allArguments, Class<?>[] argumentsTypes,
+                              Object ret) throws Throwable {
+        SwCallback swCallback = HOLDER.get();
+        HOLDER.remove();
+        ContextManager.stopSpan();
+        // ret == true: handled; the wrapped callback fires on response 
completion and finishes the span.
+        // ret == false: not handled — Jetty writes 404 on the original 
callback, bypassing ours.
+        // ret == null: handle() threw (see handleMethodException) — Jetty 
writes 500.
+        // For the latter two the wrapped callback never fires and the status 
is written only after
+        // handle() returns, so finish here with the status Jetty's contract 
guarantees.
+        if (swCallback != null && !Boolean.TRUE.equals(ret)) {
+            swCallback.finishWithStatus(ret == null ? 500 : 404);
+        }
+        return ret;
+    }
+
+    @Override
+    public void handleMethodException(EnhancedInstance objInst, Method method, 
Object[] allArguments,
+                                      Class<?>[] argumentsTypes, Throwable t) {
+        AbstractSpan span = ContextManager.activeSpan();
+        span.log(t);
+        span.errorOccurred();
+    }
+}
diff --git 
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
new file mode 100644
index 0000000000..f5193d7c05
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/SwCallback.java
@@ -0,0 +1,98 @@
+/*
+ * 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.skywalking.apm.plugin.jetty.v12.server;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+import org.eclipse.jetty.util.thread.Invocable;
+
+/**
+ * Wraps the {@link Callback} passed to {@code Server#handle}. Jetty 12 
request handling is async — the
+ * response status is only known when the callback completes on the (possibly 
different) response thread.
+ * On completion this reads the status, records it on the entry span and 
finishes the async span. It
+ * delegates every method to the original callback so Jetty's threading model 
is preserved.
+ */
+public class SwCallback implements Callback {
+
+    private final Callback delegate;
+    private final Response response;
+    private final AbstractSpan span;
+    private final AtomicBoolean finished = new AtomicBoolean();
+
+    public SwCallback(Callback delegate, Response response, AbstractSpan span) 
{
+        this.delegate = delegate;
+        this.response = response;
+        this.span = span;
+    }
+
+    @Override
+    public void succeeded() {
+        finishOnce();
+        delegate.succeeded();
+    }
+
+    @Override
+    public void failed(Throwable x) {
+        if (finished.compareAndSet(false, true)) {
+            span.log(x);
+            span.errorOccurred();
+            Tags.HTTP_RESPONSE_STATUS_CODE.set(span, response.getStatus());
+            span.asyncFinish();
+        }
+        delegate.failed(x);
+    }
+
+    @Override
+    public Invocable.InvocationType getInvocationType() {
+        return delegate.getInvocationType();
+    }
+
+    /**
+     * Records the response status and finishes the async span exactly once. 
Safe to call from either the
+     * callback completion or the interceptor's afterMethod fallback 
(not-handled / exception paths).
+     */
+    void finishOnce() {
+        if (finished.compareAndSet(false, true)) {
+            int statusCode = response.getStatus();
+            Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+            if (statusCode >= 400) {
+                span.errorOccurred();
+            }
+            span.asyncFinish();
+        }
+    }
+
+    /**
+     * Finishes the async span exactly once with an explicit status. Used for 
the not-handled / exception
+     * fallback paths where Jetty writes the error status on the original 
callback after {@code handle()}
+     * returns — so the wrapped callback never fires and {@link 
Response#getStatus()} is still 0 (uncommitted).
+     */
+    void finishWithStatus(int statusCode) {
+        if (finished.compareAndSet(false, true)) {
+            Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+            if (statusCode >= 400) {
+                span.errorOccurred();
+            }
+            span.asyncFinish();
+        }
+    }
+}
diff --git 
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
new file mode 100644
index 0000000000..8b4d347af9
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/jetty/v12/server/define/Jetty12ServerInstrumentation.java
@@ -0,0 +1,89 @@
+/*
+ * 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.skywalking.apm.plugin.jetty.v12.server.define;
+
+import java.util.Collections;
+import java.util.List;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import org.apache.skywalking.apm.agent.core.plugin.WitnessMethod;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+import static 
org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
+
+/**
+ * Enhances {@code org.eclipse.jetty.server.Server#handle(Request, Response, 
Callback)} by
+ * {@code HandleInterceptor}. Only Jetty 12+ declares this three-argument, 
{@code Callback}-terminated
+ * signature, so the witness method both activates the plugin exclusively on 
Jetty 12 and keeps it from
+ * overlapping the merged jetty-server plugin (which enhances the Jetty 
9/10/11 {@code HttpChannel},
+ * removed in Jetty 12).
+ */
+public class Jetty12ServerInstrumentation extends 
ClassInstanceMethodsEnhancePluginDefine {
+
+    private static final String ENHANCE_CLASS = 
"org.eclipse.jetty.server.Server";
+    private static final String ENHANCE_METHOD = "handle";
+    private static final String CALLBACK_CLASS = 
"org.eclipse.jetty.util.Callback";
+    private static final String INTERCEPT_CLASS = 
"org.apache.skywalking.apm.plugin.jetty.v12.server.HandleInterceptor";
+
+    @Override
+    protected List<WitnessMethod> witnessMethods() {
+        return Collections.singletonList(new WitnessMethod(
+            ENHANCE_CLASS,
+            named(ENHANCE_METHOD).and(takesArgument(2, named(CALLBACK_CLASS)))
+        ));
+    }
+
+    @Override
+    public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+        return new ConstructorInterceptPoint[0];
+    }
+
+    @Override
+    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() 
{
+        return new InstanceMethodsInterceptPoint[] {
+            new InstanceMethodsInterceptPoint() {
+                @Override
+                public ElementMatcher<MethodDescription> getMethodsMatcher() {
+                    return 
named(ENHANCE_METHOD).and(takesArguments(3)).and(takesArgument(2, 
named(CALLBACK_CLASS)));
+                }
+
+                @Override
+                public String getMethodsInterceptor() {
+                    return INTERCEPT_CLASS;
+                }
+
+                @Override
+                public boolean isOverrideArgs() {
+                    return true;
+                }
+            }
+        };
+    }
+
+    @Override
+    protected ClassMatch enhanceClass() {
+        return byName(ENHANCE_CLASS);
+    }
+}
diff --git 
a/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
new file mode 100644
index 0000000000..2c466d5f66
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/jetty-server-12.x-plugin/src/main/resources/skywalking-plugin.def
@@ -0,0 +1,18 @@
+#
+# 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.
+
+jetty-server-12.x=org.apache.skywalking.apm.plugin.jetty.v12.server.define.Jetty12ServerInstrumentation
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml 
b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
index 1ac21fb14d..671cc92f91 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
@@ -31,6 +31,7 @@
         <module>jetty-client-9.x-plugin</module>
         <module>jetty-client-9.0-plugin</module>
         <module>jetty-server-plugin</module>
+        <module>jetty-server-12.x-plugin</module>
         <module>jetty-client-11.x-plugin</module>
     </modules>
     <packaging>pom</packaging>
diff --git a/apm-sniffer/apm-sdk-plugin/pom.xml 
b/apm-sniffer/apm-sdk-plugin/pom.xml
index 829d63dd84..8bc2e1faaa 100644
--- a/apm-sniffer/apm-sdk-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/pom.xml
@@ -44,6 +44,7 @@
         <module>okhttp-common</module>
         <module>spring-plugins</module>
         <module>struts2-2.x-plugin</module>
+        <module>struts2-7.x-plugin</module>
         <module>nutz-plugins</module>
         <module>jetty-plugin</module>
         <module>spymemcached-2.x-plugin</module>
diff --git a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
similarity index 55%
copy from apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
copy to apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
index 1ac21fb14d..f314ff5fba 100644
--- a/apm-sniffer/apm-sdk-plugin/jetty-plugin/pom.xml
+++ b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/pom.xml
@@ -1,4 +1,3 @@
-<?xml version="1.0" encoding="UTF-8"?>
 <!--
   ~ Licensed to the Apache Software Foundation (ASF) under one or more
   ~ contributor license agreements.  See the NOTICE file distributed with
@@ -18,27 +17,40 @@
   -->
 
 <project xmlns="http://maven.apache.org/POM/4.0.0"; 
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"; 
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
-    <modelVersion>4.0.0</modelVersion>
-
     <parent>
         <groupId>org.apache.skywalking</groupId>
         <artifactId>apm-sdk-plugin</artifactId>
         <version>9.7.0-SNAPSHOT</version>
     </parent>
+    <modelVersion>4.0.0</modelVersion>
 
-    <artifactId>jetty-plugins</artifactId>
-    <modules>
-        <module>jetty-client-9.x-plugin</module>
-        <module>jetty-client-9.0-plugin</module>
-        <module>jetty-server-plugin</module>
-        <module>jetty-client-11.x-plugin</module>
-    </modules>
-    <packaging>pom</packaging>
+    <artifactId>apm-struts2-7.x-plugin</artifactId>
+    <packaging>jar</packaging>
 
-    <name>jetty-plugin</name>
-    <url>http://maven.apache.org</url>
+    <name>struts2-7.x-plugin</name>
 
+    <!--
+      Struts 7.x moved DefaultActionInvocation from com.opensymphony.xwork2 to 
org.apache.struts2
+      and switched to the jakarta.servlet namespace (Jakarta Servlet 6.0, JDK 
17), so it needs a
+      separate plugin from struts2-2.x-plugin (which stays on the old class + 
javax).
+    -->
     <properties>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <struts.version>7.2.1</struts.version>
+        <jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
     </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-core</artifactId>
+            <version>${struts.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <version>${jakarta-servlet-api.version}</version>
+            <scope>provided</scope>
+        </dependency>
+    </dependencies>
 </project>
diff --git 
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
new file mode 100644
index 0000000000..80a42b2eaa
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/Struts2Interceptor.java
@@ -0,0 +1,82 @@
+/*
+ * 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.skywalking.apm.plugin.struts2.v7;
+
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import org.apache.skywalking.apm.agent.core.context.CarrierItem;
+import org.apache.skywalking.apm.agent.core.context.ContextCarrier;
+import org.apache.skywalking.apm.agent.core.context.ContextManager;
+import org.apache.skywalking.apm.agent.core.context.tag.Tags;
+import org.apache.skywalking.apm.agent.core.context.trace.AbstractSpan;
+import org.apache.skywalking.apm.agent.core.context.trace.SpanLayer;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.EnhancedInstance;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.InstanceMethodsAroundInterceptor;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.MethodInterceptResult;
+import org.apache.skywalking.apm.network.trace.component.ComponentsDefine;
+import org.apache.struts2.ServletActionContext;
+
+/**
+ * Enhances {@code org.apache.struts2.DefaultActionInvocation#invokeAction} in 
Struts 7.x. Struts 7 is
+ * Jakarta-only (Jakarta Servlet 6.0), so the servlet request/response are 
referenced through the
+ * jakarta.servlet namespace directly.
+ */
+public class Struts2Interceptor implements InstanceMethodsAroundInterceptor {
+
+    @Override
+    public void beforeMethod(EnhancedInstance objInst, Method method, Object[] 
allArguments, Class<?>[] argumentsTypes,
+        MethodInterceptResult result) throws Throwable {
+        HttpServletRequest request = ServletActionContext.getRequest();
+        ContextCarrier contextCarrier = new ContextCarrier();
+
+        CarrierItem next = contextCarrier.items();
+        while (next.hasNext()) {
+            next = next.next();
+            next.setHeadValue(request.getHeader(next.getHeadKey()));
+        }
+
+        AbstractSpan span = 
ContextManager.createEntrySpan(request.getRequestURI(), contextCarrier);
+        Tags.URL.set(span, request.getRequestURL().toString());
+        Tags.HTTP.METHOD.set(span, request.getMethod());
+        span.setComponent(ComponentsDefine.STRUTS2);
+        SpanLayer.asHttp(span);
+    }
+
+    @Override
+    public Object afterMethod(EnhancedInstance objInst, Method method, 
Object[] allArguments, Class<?>[] argumentsTypes,
+        Object ret) throws Throwable {
+        HttpServletResponse response = ServletActionContext.getResponse();
+
+        AbstractSpan span = ContextManager.activeSpan();
+        int statusCode = response.getStatus();
+        Tags.HTTP_RESPONSE_STATUS_CODE.set(span, statusCode);
+        if (statusCode >= 400) {
+            span.errorOccurred();
+        }
+        ContextManager.stopSpan();
+        return ret;
+    }
+
+    @Override
+    public void handleMethodException(EnhancedInstance objInst, Method method, 
Object[] allArguments,
+        Class<?>[] argumentsTypes, Throwable t) {
+        ContextManager.activeSpan().log(t);
+    }
+}
diff --git 
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
new file mode 100644
index 0000000000..68302e8c5a
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/java/org/apache/skywalking/apm/plugin/struts2/v7/define/Struts2Instrumentation.java
@@ -0,0 +1,75 @@
+/*
+ * 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.skywalking.apm.plugin.struts2.v7.define;
+
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.ConstructorInterceptPoint;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.InstanceMethodsInterceptPoint;
+import 
org.apache.skywalking.apm.agent.core.plugin.interceptor.enhance.ClassInstanceMethodsEnhancePluginDefine;
+import org.apache.skywalking.apm.agent.core.plugin.match.ClassMatch;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static 
org.apache.skywalking.apm.agent.core.plugin.match.NameMatch.byName;
+
+/**
+ * {@link Struts2Instrumentation} enhances the <code>invokeAction</code> 
method of Struts 7's
+ * <code>org.apache.struts2.DefaultActionInvocation</code> (relocated from
+ * <code>com.opensymphony.xwork2.DefaultActionInvocation</code> in Struts 
2.x). The class name is
+ * unique to Struts 7, so it self-witnesses — no witness class is needed, and 
there is no overlap
+ * with the struts2-2.x plugin, which enhances the old XWork class.
+ */
+public class Struts2Instrumentation extends 
ClassInstanceMethodsEnhancePluginDefine {
+
+    private static final String ENHANCE_CLASS = 
"org.apache.struts2.DefaultActionInvocation";
+    private static final String ENHANCE_METHOD = "invokeAction";
+    private static final String INTERCEPT_CLASS = 
"org.apache.skywalking.apm.plugin.struts2.v7.Struts2Interceptor";
+
+    @Override
+    public ConstructorInterceptPoint[] getConstructorsInterceptPoints() {
+        return new ConstructorInterceptPoint[0];
+    }
+
+    @Override
+    public InstanceMethodsInterceptPoint[] getInstanceMethodsInterceptPoints() 
{
+        return new InstanceMethodsInterceptPoint[] {
+            new InstanceMethodsInterceptPoint() {
+                @Override
+                public ElementMatcher<MethodDescription> getMethodsMatcher() {
+                    return named(ENHANCE_METHOD);
+                }
+
+                @Override
+                public String getMethodsInterceptor() {
+                    return INTERCEPT_CLASS;
+                }
+
+                @Override
+                public boolean isOverrideArgs() {
+                    return false;
+                }
+            }
+        };
+    }
+
+    @Override
+    protected ClassMatch enhanceClass() {
+        return byName(ENHANCE_CLASS);
+    }
+}
diff --git 
a/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
new file mode 100644
index 0000000000..a9a45c666b
--- /dev/null
+++ 
b/apm-sniffer/apm-sdk-plugin/struts2-7.x-plugin/src/main/resources/skywalking-plugin.def
@@ -0,0 +1,18 @@
+#
+# 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.
+
+struts2-7.x=org.apache.skywalking.apm.plugin.struts2.v7.define.Struts2Instrumentation
diff --git a/docs/en/setup/service-agent/java-agent/Plugin-list.md 
b/docs/en/setup/service-agent/java-agent/Plugin-list.md
index 2e5ad78336..ac9ea19eeb 100644
--- a/docs/en/setup/service-agent/java-agent/Plugin-list.md
+++ b/docs/en/setup/service-agent/java-agent/Plugin-list.md
@@ -56,6 +56,7 @@
 - jetty-client-9.0
 - jetty-client-9.x
 - jetty-server
+- jetty-server-12.x
 - kafka-0.11.x/1.x/2.x
 - kafka-3.7.x
 - kafka-3.9.x
@@ -132,6 +133,7 @@
 - spring-webflux-5.x-webclient
 - spymemcached-2.x
 - struts2-2.x
+- struts2-7.x
 - thrift
 - tomcat
 - toolkit-counter
diff --git a/docs/en/setup/service-agent/java-agent/Plugin-test.md 
b/docs/en/setup/service-agent/java-agent/Plugin-test.md
index a40eb82e35..ca1d6ef2c2 100644
--- a/docs/en/setup/service-agent/java-agent/Plugin-test.md
+++ b/docs/en/setup/service-agent/java-agent/Plugin-test.md
@@ -16,11 +16,11 @@ Every plugin maintained in the main repo requires 
corresponding test cases as we
 
 ## Case Base Image Introduction
 
-The test framework provides `JVM-container` and `Tomcat-container` base images 
including JDK8 and JDK17. You can choose the best one for your test case. If 
both are suitable for your case, **`JVM-container` is preferred**.
+The test framework provides `JVM-container` and `Tomcat-container` base 
images. You can choose the best one for your test case. If both are suitable 
for your case, **`JVM-container` is preferred**.
 
 ### JVM-container Image Introduction
 
-[JVM-container](../../../../../test/plugin/containers/jvm-container) uses 
`eclipse-temurin:8-jdk` as the base image. `JVM-container` supports JDK8 and 
JDK17 as well in CI, which inherits `eclipse-temurin:8-jdk` and 
`eclipse-temurin:17-jdk`.
+[JVM-container](../../../../../test/plugin/containers/jvm-container) uses 
`eclipse-temurin:8-jdk` as the default base image. In CI it is exercised on 
**JDK 8, 11, 17, 21 and 25** (via the `eclipse-temurin:{8,11,17,21,25}-jdk` 
images).
 It is supported to custom the base Java docker image by specify 
`base_image_java`.
 The test case project must be packaged as `project-name.zip`, including 
`startup.sh` and uber jar, by using `mvn clean package`.
 
@@ -31,8 +31,8 @@ Take the following test projects as examples:
 
 ### Tomcat-container Image Introduction
 
-[Tomcat-container](../../../../../test/plugin/containers/tomcat-container) 
uses `tomcat:8.5-jdk8-openjdk`, `tomcat:8.5-jdk17-openjdk` as the base image.
-It is supported to custom the base Tomcat docker image by specify 
`base_image_tomcat`.
+[Tomcat-container](../../../../../test/plugin/containers/tomcat-container) 
uses `tomcat:8.5-jdk8-openjdk` as the default base image. In CI it is exercised 
on **Tomcat 8.5, 9.0, 10.0 and 10.1** (e.g. `tomcat:8.5-jdk8-openjdk`, 
`tomcat:9.0.71-jdk8`, `tomcat:10.0.22-jdk8`, `tomcat:8.5-jdk17-openjdk`, 
`tomcat:10.1-jdk17-temurin`, `tomcat:10.1-jdk21-temurin`). Tomcat 8.5/9.0 are 
`javax.servlet`; Tomcat 10.0/10.1 are `jakarta.servlet`.
+It is supported to custom the base Tomcat docker image by specify 
`base_image_tomcat`. The base image must provide `curl` (used by the 
container's health check); if an official image lacks it, build a small custom 
image that adds `curl` first — as the JDK 25 lane does with 
`tomcat-curl:10.1.50-jdk25-temurin`.
 The test case project must be packaged as `project-name.war` by using `mvn 
package`.
 
 Take the following test project as an example
diff --git a/docs/en/setup/service-agent/java-agent/Supported-list.md 
b/docs/en/setup/service-agent/java-agent/Supported-list.md
index ca6f481f06..1416ec3471 100644
--- a/docs/en/setup/service-agent/java-agent/Supported-list.md
+++ b/docs/en/setup/service-agent/java-agent/Supported-list.md
@@ -11,10 +11,10 @@ metrics based on the tracing data.
   * Spring MVC 3.x, 4.x 5.x with servlet 3.x
   * Spring MVC 6.x (Optional²)
   * [Nutz Web Framework](https://github.com/nutzam/nutz)  1.x
-  * [Struts2 MVC](http://struts.apache.org/)  2.3.x -> 2.5.x
+  * [Struts2 MVC](http://struts.apache.org/)  2.3.x -> 2.5.x, 7.x (Jakarta, 
JDK17)
   * Resin 3 (Optional¹), See [SkySPM Plugin 
Repository](https://github.com/SkyAPM/java-plugin-extensions)
   * Resin 4 (Optional¹), See [SkySPM Plugin 
Repository](https://github.com/SkyAPM/java-plugin-extensions)
-  * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 11.x
+  * [Jetty Server](http://www.eclipse.org/jetty/) 9.x -> 12.x
   * [Spring 
WebFlux](https://docs.spring.io/spring/docs/current/spring-framework-reference/web-reactive.html)
 5.x (Optional²) -> 6.x (Optional²)
   * [Undertow](http://undertow.io/)  1.3.0.Final -> 2.3.18.Final
   * [RESTEasy](https://resteasy.dev/)  3.1.0.Final -> 6.2.4.Final
@@ -172,7 +172,7 @@ The meter plugin provides the advanced metrics collections, 
which are not a part
   * [Undertow](https://github.com/undertow-io/undertow) 2.1.x -> 2.6.x
   * [Tomcat](https://github.com/apache/tomcat) 7.0.x -> 10.0.x
   * [Dubbo](https://github.com/apache/dubbo) 2.5.x -> 2.7.x
-  * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 11.x
+  * [Jetty](https://github.com/eclipse/jetty.project) 9.1.x -> 12.x
   * [Grizzly](https://github.com/eclipse-ee4j/grizzly) 2.3.x -> 4.x
 * Connection Pool
   * Supported JDBC drviers
diff --git a/test/plugin/CLAUDE.md b/test/plugin/CLAUDE.md
new file mode 100644
index 0000000000..6b73f93540
--- /dev/null
+++ b/test/plugin/CLAUDE.md
@@ -0,0 +1,134 @@
+# CLAUDE.md - Plugin Test Guide
+
+This file guides AI assistants writing and changing **plugin tests** under 
`test/plugin/scenarios/`.
+For the full test-framework mechanics (scenario layout, `configuration.yml`, 
`expectedData.yaml`,
+containers, running locally) see the **"Plugin Test Framework"** section of
+`apm-sniffer/apm-sdk-plugin/CLAUDE.md`, and `docs/.../Plugin-test.md`.
+
+## Testing philosophy: plugin scenarios are the key test — not unit tests
+
+For an agent plugin, the **plugin test scenario is the primary and sufficient 
test**. Do **not**
+over-engineer by adding many unit tests for a plugin.
+
+A plugin's real risk is two things, and a scenario proves both at once by 
running the **real
+framework**:
+
+1. **Cast / fetch-data logic** — the interceptor casting the intercepted 
arguments to the framework
+   types and reading request/response fields off them (headers, method, URI, 
status, params). Mocks
+   can't validate that the real framework class actually exposes those methods 
the way you assumed.
+2. **Version compatibility** — the same interceptor must work across the 
supported version range.
+
+So: **when adding or changing a plugin, add or extend its scenario**, not a 
pile of mock-based unit
+tests. Reserve unit tests for genuinely logic/namespace-critical **shared** 
code where a deterministic
+in-JVM assertion earns its keep (e.g. `servlet-commons` `wrap()` resolving 
javax vs jakarta with both
+APIs on the test classpath) — never for per-plugin interceptor mocking.
+
+## Validation path: assert the full span shape your plugin produces
+
+A scenario proves a plugin by asserting the **exact spans** the real framework 
makes the agent
+produce. Pick the shape by what the framework actually does — and don't settle 
for a single entry
+span when the plugin also does propagation, or you silently skip the 
inject/extract path.
+
+- **RPC / web frameworks that have both a server and a client side** (Struts, 
Spring MVC, gRPC,
+  Dubbo, HTTP servers …): drive the full round-trip —
+  `curl → server → its own client call → another service's endpoint`. The 
trace then contains
+  **2 entry spans + 1 exit span**:
+  1. the **server entry** span for the request `curl` hit (proves the server 
side: extract + entry),
+  2. the **client exit** span for the outbound call (proves inject + exit),
+  3. a **second entry** span on the other endpoint carrying a **cross-process 
ref** back to the
+     first (proves propagation across the wire — the receiving/extract side).
+
+  **The ref itself must be asserted** — for RPC the cross-process `refs:` 
block is the whole point,
+  so `expectedData.yaml` must include it on that second entry span with 
`refType: CrossProcess`,
+  `parentEndpoint`, `parentSpanId` (pointing at the exit span), 
`parentTraceSegmentId: not null`,
+  `parentService`, and `traceId: not null`. A scenario that asserts spans but 
not the ref does not
+  prove propagation.
+
+  The struts2.7 scenario is exactly this: `case.action` (entry) → HttpClient 
(exit) → `case1.action`
+  (entry + ref); jetty-12 uses the same shape via a JDK-HttpURLConnection 
self-call. Keep that nested
+  self-call — it is the only thing that proves inject / extract / propagation; 
a lone entry span does not.
+
+- **Client-only plugins** (JDBC/PostgreSQL, Redis, MongoDB, an HTTP client, a 
message producer): the
+  peer is a database/broker, not another instrumented SkyWalking service, so 
there is **no second
+  entry span**. Assert just the **client exit** span (component, `peer`, and 
the `db.*` / `http.*`
+  tags). You do NOT need to stand up another service endpoint.
+
+## Version coverage: one version per minor (latest patch)
+
+`support-version.list` must cover the framework's supported range, but keep 
**one version per minor
+version — the latest patch**, not every patch. E.g. Jetty `12.0.36` + 
`12.1.10`; Struts `7.0.3`,
+`7.1.1`, `7.2.1`; Spring `6.0.4`, `6.1.1`, `6.2.19`. Verify each version 
resolves on Maven Central.
+This is the compatibility proof; CI runs the scenario against each listed 
version.
+
+## How the test containers work (and where the JDK/Tomcat version really comes 
from)
+
+A scenario runs inside one of two prebuilt container images, selected by 
`type:` in
+`configuration.yml`:
+
+- **`type: jvm`** → the **`agent-test-jvm`** container. Your app is a fat-jar 
started by
+  `bin/startup.sh` with `${agent_opts}` (the `-javaagent:` line — you MUST 
include it in
+  `startup.sh`). Built `FROM ${base_image_java}`.
+- **`type: tomcat`** → the **`agent-test-tomcat`** container. Your app is a 
`*.war` dropped into
+  `/usr/local/tomcat/webapps/`; the agent is wired in via the container's 
patched `catalina.sh`.
+  Built `FROM ${base_image_tomcat}`.
+
+**The version pin is in the CI lane, not `configuration.yml`.** 
`configuration.yml` only says
+`type: tomcat` — it does **not** choose a Tomcat version. The concrete JDK and 
Tomcat versions come
+from the `base_image_java` / `base_image_tomcat` inputs passed to 
`./.github/actions/build` in the
+lane's `Build` job, and **every scenario in that lane shares that one image**. 
So:
+
+- `type: tomcat` on the `plugins-jdk8-*` lane → `tomcat:8.5-jdk8` (javax);
+  on `plugins-jdk11-*` → `tomcat:9.0-jdk11` (javax);
+  on `plugins-jdk17-*` → `tomcat:10.1-jdk17` (**jakarta**).
+- That is why a jakarta/Servlet-6 framework (Struts 7, Jetty 12, Spring 6) 
MUST be registered on a
+  JDK-17 lane — it needs the Tomcat-10.1 base — and a javax one on an older 
lane. Putting a jakarta
+  WAR on a Tomcat-8.5 lane just won't deploy.
+
+To run locally you pass the same two knobs explicitly (the defaults are the 
javax/JDK-8 pair):
+`bash test/plugin/run.sh --base_image_java eclipse-temurin:17-jdk 
--base_image_tomcat tomcat:10.1-jdk17-temurin <scenario>`.
+Some newer bases lack `curl` (the container health check needs it) — the 
JDK-25 lane bakes it in
+via `.github/workflows/Dockerfile-tomcat-jdk25-withCurl`.
+
+## How the GitHub Actions lanes work
+
+Scenarios are load-balanced across 
`.github/workflows/plugins-<jdk>-test.<group>.yaml` files. Each
+file is one lane: its `Build` job pins `base_image_java` + 
`base_image_tomcat`, and its `test` job's
+`matrix.case:` list names the scenarios that run on that image. Lanes exist 
per JDK
+(`jdk8`/`jdk11`/`jdk17`/`jdk21`/`jdk25`); the trailing `.0`/`.1`/… is just a 
bucket to spread load.
+Register a new scenario by adding its directory name to the `matrix.case:` 
list of the lane whose
+base image matches the framework's needs — use `python3 tools/select-group.py` 
to pick the
+least-loaded bucket in that JDK lane.
+
+## Cold-start and the 3s entry-service timeout (a real gotcha)
+
+The container's `run.sh` runs under `set -e` and hits the entry service with
+`curl -s --max-time 3 ${SCENARIO_ENTRY_SERVICE}` — **no warm-up first**. So 
the very first (cold)
+entry request must finish within **3 seconds** or `curl` exits 28 and the 
whole scenario fails. That
+first request pays a lot of one-time cost at once: the agent's ByteBuddy 
enhancement of every class
+it touches on that path, JIT, and — for WARs — a cold Jasper **JSP compile**, 
all under the
+container's tight `-Xmx256m` heap and whatever CPU the runner gives, with 
**jacoco** simultaneously
+instrumenting the `org.apache.skywalking.*` classes for coverage.
+
+Keep the entry path's cold cost low:
+
+- Don't render a **JSP** from the entry action if a lightweight result works 
(e.g. Struts
+  `httpheader`, a plain-text/stream result). The view is irrelevant to what a 
plugin scenario
+  verifies (entry/exit spans, tags, propagation), and a cold Jasper compile is 
expensive.
+- Be extra careful when the entry action makes a **nested internal call** (for 
cross-process
+  propagation coverage): if that nested request also does cold work (a JSP 
compile) *while* the
+  outer request is still cold-enhancing Struts/HttpClient, the two cold-start 
storms contend for the
+  limited CPU and heap and blow up **super-linearly** (observed ~0.5s each in 
isolation but ~3–20s
+  nested under throttle). This is a slow first request, not a plugin bug — 
verify with a
+  CPU-throttled container (`docker run --cpus=0.5 …`) before assuming a hang.
+
+## Practical notes
+
+- One scenario per plugin (or per javax/jakarta era). Mirror an existing 
sibling scenario as a
+  template and change only what differs (framework version, namespace, 
deployment model).
+- Match `type:` to the deployment: `jvm` (fat-jar started by `bin/startup.sh`, 
`${agent_opts}`
+  required) or `tomcat` (WAR on a container). Jakarta/newer frameworks go in a 
JDK-17 CI group.
+- Assert the entry span shape that exercises the plugin's data logic: 
`componentId`, `spanType: Entry`,
+  `spanLayer: Http`, and the `url` / `http.method` / `http.status_code` (and 
`http.params` /
+  `http.headers` where the plugin collects them) tags, plus cross-process refs.
+- Register the scenario in the right `.github/workflows/plugins-*.yaml` matrix 
(use
+  `python3 tools/select-group.py` to pick the least-loaded group for its JDK 
lane).
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh 
b/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh
new file mode 100644
index 0000000000..ea5bec3c39
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/bin/startup.sh
@@ -0,0 +1,21 @@
+#!/bin/bash
+#
+# 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.
+
+home="$(cd "$(dirname $0)"; pwd)"
+
+java -jar ${agent_opts} ${home}/../libs/jetty-12.x-scenario.jar &
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml 
b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml
new file mode 100644
index 0000000000..b3e44c9df3
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/config/expectedData.yaml
@@ -0,0 +1,86 @@
+# 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.
+segmentItems:
+  - serviceName: jetty-12.x-scenario
+    segmentSize: gt 2
+    segments:
+      - segmentId: not null
+        spans:
+          - operationName: /jetty-12.x-scenario/case/case1
+            parentSpanId: '-1'
+            spanId: '0'
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: '19'
+            isError: 'false'
+            spanType: Entry
+            peer: ''
+            skipAnalysis: 'false'
+            tags:
+              - key: url
+                value: http://localhost:18080/jetty-12.x-scenario/case/case1
+              - key: http.method
+                value: GET
+              - key: http.status_code
+                value: '200'
+            refs:
+              - parentEndpoint: /jetty-12.x-scenario/case/jetty-12.x-scenario
+                networkAddress: localhost:18080
+                refType: CrossProcess
+                parentSpanId: '1'
+                parentTraceSegmentId: not null
+                parentServiceInstance: not null
+                parentService: jetty-12.x-scenario
+                traceId: not null
+      - segmentId: not null
+        spans:
+          - operationName: /jetty-12.x-scenario/case/case1
+            parentSpanId: '0'
+            spanId: '1'
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: '2'
+            isError: 'false'
+            spanType: Exit
+            peer: localhost:18080
+            skipAnalysis: 'false'
+            tags:
+              - key: url
+                value: http://localhost:18080/jetty-12.x-scenario/case/case1
+              - key: http.method
+                value: GET
+              - key: http.status_code
+                value: '200'
+          - operationName: /jetty-12.x-scenario/case/jetty-12.x-scenario
+            parentSpanId: '-1'
+            spanId: '0'
+            spanLayer: Http
+            startTime: gt 0
+            endTime: gt 0
+            componentId: '19'
+            isError: 'false'
+            spanType: Entry
+            peer: ''
+            skipAnalysis: 'false'
+            tags:
+              - key: url
+                value: 
http://localhost:18080/jetty-12.x-scenario/case/jetty-12.x-scenario
+              - key: http.method
+                value: GET
+              - key: http.status_code
+                value: '200'
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml 
b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml
new file mode 100644
index 0000000000..2db90d0c99
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/configuration.yml
@@ -0,0 +1,22 @@
+# 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.
+
+type: jvm
+entryService: 
http://localhost:18080/jetty-12.x-scenario/case/jetty-12.x-scenario
+healthCheck: http://localhost:18080/jetty-12.x-scenario/case/healthCheck
+startScript: ./bin/startup.sh
+environment:
+dependencies:
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/pom.xml 
b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml
new file mode 100644
index 0000000000..6a28e254bf
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/pom.xml
@@ -0,0 +1,128 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+    <groupId>org.apache.skywalking.apm.testcase</groupId>
+    <artifactId>jetty-12.x-scenario</artifactId>
+    <version>1.0.0</version>
+    <packaging>jar</packaging>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <compiler.version>17</compiler.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+
+        <test.framework.version>12.0.36</test.framework.version>
+
+        <log4j.version>2.6.2</log4j.version>
+    </properties>
+
+    <name>skywalking-jetty-12.x-scenario</name>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.eclipse.jetty</groupId>
+            <artifactId>jetty-server</artifactId>
+            <version>${test.framework.version}</version>
+        </dependency>
+        <!--
+          Used only by the entry handler to make the nested internal call that 
proves cross-process
+          propagation. Apache HttpClient 4.x is instrumented by a normal 
(non-bootstrap) plugin, so
+          the exit span + sw8 injection are reliable across JDKs (the JDK 
HttpURLConnection bootstrap
+          plugin does not enhance sun.net.www.protocol.http on JDK 17 in this 
container).
+        -->
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.3</version>
+        </dependency>
+    </dependencies>
+
+
+    <build>
+        <finalName>jetty-12.x-scenario</finalName>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-shade-plugin</artifactId>
+                <version>3.1.0</version>
+                <executions>
+                    <execution>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>shade</goal>
+                        </goals>
+                        <configuration>
+                            <filters>
+                                <filter>
+                                    <artifact>*:*</artifact>
+                                    <excludes>
+                                        <exclude>META-INF/*.SF</exclude>
+                                        <exclude>META-INF/*.DSA</exclude>
+                                        <exclude>META-INF/*.RSA</exclude>
+                                    </excludes>
+                                </filter>
+                            </filters>
+                            <transformers>
+                                <transformer
+                                        
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
+                                    <mainClass>
+                                        
org.apache.skywalking.apm.testcase.jetty12xserver.Application
+                                    </mainClass>
+                                </transformer>
+                            </transformers>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>${maven-compiler-plugin.version}</version>
+                <configuration>
+                    <source>${compiler.version}</source>
+                    <target>${compiler.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-assembly-plugin</artifactId>
+                <executions>
+                    <execution>
+                        <id>assemble</id>
+                        <phase>package</phase>
+                        <goals>
+                            <goal>single</goal>
+                        </goals>
+                        <configuration>
+                            <descriptors>
+                                
<descriptor>src/main/assembly/assembly.xml</descriptor>
+                            </descriptors>
+                            <outputDirectory>./target/</outputDirectory>
+                        </configuration>
+                    </execution>
+                </executions>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml
new file mode 100644
index 0000000000..4be83c4cb7
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/assembly/assembly.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<assembly
+    
xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2";
+    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+    
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2
 http://maven.apache.org/xsd/assembly-1.1.2.xsd";>
+    <formats>
+        <format>zip</format>
+    </formats>
+
+    <fileSets>
+        <fileSet>
+            <directory>./bin</directory>
+            <fileMode>0775</fileMode>
+        </fileSet>
+    </fileSets>
+
+    <files>
+        <file>
+            <source>${project.build.directory}/jetty-12.x-scenario.jar</source>
+            <outputDirectory>./libs</outputDirectory>
+            <fileMode>0775</fileMode>
+        </file>
+    </files>
+</assembly>
diff --git 
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
new file mode 100644
index 0000000000..aecb666125
--- /dev/null
+++ 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/Application.java
@@ -0,0 +1,37 @@
+/*
+ * 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.skywalking.apm.testcase.jetty12xserver;
+
+import org.apache.skywalking.apm.testcase.jetty12xserver.handler.CaseHandler;
+import org.eclipse.jetty.server.Server;
+
+/**
+ * Embedded Jetty 12 server. Jetty 12 handling routes every request through
+ * {@code org.eclipse.jetty.server.Server#handle(Request, Response, Callback)} 
— the method the
+ * jetty-server-12.x plugin enhances — before delegating to the top-level 
{@link CaseHandler}.
+ */
+public class Application {
+
+    public static void main(String[] args) throws Exception {
+        Server server = new Server(18080);
+        server.setHandler(new CaseHandler());
+        server.start();
+        server.join();
+    }
+}
diff --git 
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
new file mode 100644
index 0000000000..201ab06792
--- /dev/null
+++ 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/java/org/apache/skywalking/apm/testcase/jetty12xserver/handler/CaseHandler.java
@@ -0,0 +1,67 @@
+/*
+ * 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.skywalking.apm.testcase.jetty12xserver.handler;
+
+import java.nio.ByteBuffer;
+import java.nio.charset.StandardCharsets;
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+import org.eclipse.jetty.server.Handler;
+import org.eclipse.jetty.server.Request;
+import org.eclipse.jetty.server.Response;
+import org.eclipse.jetty.util.Callback;
+
+/**
+ * Top-level Jetty 12 core handler.
+ *
+ * The entry service {@code /jetty-12.x-scenario/case/jetty-12.x-scenario} 
makes a nested internal
+ * call to {@code /jetty-12.x-scenario/case/case1} on the same server. This 
exercises both sides of
+ * the jetty-server-12.x plugin in one trace: the entry span for the inbound 
request, the HttpClient
+ * exit span for the outbound call (inject), and a second entry span on {@code 
case1} that extracts
+ * the propagated context into a cross-process ref (the receiving/extract 
side). The health check
+ * {@code /case/healthCheck} and the nested {@code /case/case1} endpoint 
simply return 200.
+ */
+public class CaseHandler extends Handler.Abstract {
+
+    @Override
+    public boolean handle(Request request, Response response, Callback 
callback) throws Exception {
+        String path = request.getHttpURI().getPath();
+        if (path != null && path.endsWith("/case/jetty-12.x-scenario")) {
+            visit("http://localhost:18080/jetty-12.x-scenario/case/case1";);
+        }
+        response.setStatus(200);
+        response.write(true, 
ByteBuffer.wrap("hello".getBytes(StandardCharsets.UTF_8)), callback);
+        return true;
+    }
+
+    private static void visit(String url) throws Exception {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            HttpGet httpget = new HttpGet(url);
+            ResponseHandler<String> responseHandler = response -> {
+                HttpEntity entity = response.getEntity();
+                return entity != null ? EntityUtils.toString(entity) : null;
+            };
+            httpClient.execute(httpget, responseHandler);
+        }
+    }
+}
diff --git 
a/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml 
b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..b5cda5ae8a
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/src/main/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<Configuration status="WARN">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_ERR">
+            <PatternLayout charset="UTF-8" pattern="[%d{yyyy-MM-dd 
HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="WARN">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git a/test/plugin/scenarios/jetty-12.x-scenario/support-version.list 
b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list
new file mode 100644
index 0000000000..6c62f269a7
--- /dev/null
+++ b/test/plugin/scenarios/jetty-12.x-scenario/support-version.list
@@ -0,0 +1,18 @@
+# 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.
+
+12.0.36
+12.1.10
diff --git a/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml 
b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml
new file mode 100644
index 0000000000..02d7206ab2
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/config/expectedData.yaml
@@ -0,0 +1,72 @@
+# 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.
+segmentItems:
+- serviceName: struts2.7-scenario
+  segmentSize: gt 2
+  segments:
+  - segmentId: not null
+    spans:
+    - operationName: /struts2.7-scenario/case/case1.action
+      parentSpanId: -1
+      spanId: 0
+      spanLayer: Http
+      startTime: gt 0
+      endTime: gt 0
+      componentId: 15
+      isError: false
+      spanType: Entry
+      peer: ''
+      tags:
+      - {key: url, value: 
'http://localhost:8080/struts2.7-scenario/case/case1.action'}
+      - {key: http.method, value: GET}
+      - {key: http.status_code, value: '200'}
+      refs:
+      - {parentEndpoint: /struts2.7-scenario/case/case.action, networkAddress: 
'localhost:8080',
+        refType: CrossProcess, parentSpanId: 1, parentTraceSegmentId: not 
null, parentServiceInstance: not
+          null, parentService: struts2.7-scenario, traceId: not null}
+      skipAnalysis: 'false'
+  - segmentId: not null
+    spans:
+    - operationName: /struts2.7-scenario/case/case1.action
+      parentSpanId: 0
+      spanId: 1
+      spanLayer: Http
+      startTime: gt 0
+      endTime: gt 0
+      componentId: 2
+      isError: false
+      spanType: Exit
+      peer: localhost:8080
+      tags:
+      - {key: url, value: 
'http://localhost:8080/struts2.7-scenario/case/case1.action'}
+      - {key: http.method, value: GET}
+      - {key: http.status_code, value: '200'}
+      skipAnalysis: 'false'
+    - operationName: /struts2.7-scenario/case/case.action
+      parentSpanId: -1
+      spanId: 0
+      spanLayer: Http
+      startTime: gt 0
+      endTime: gt 0
+      componentId: 15
+      isError: false
+      spanType: Entry
+      peer: ''
+      tags:
+      - {key: url, value: 
'http://localhost:8080/struts2.7-scenario/case/case.action'}
+      - {key: http.method, value: GET}
+      - {key: http.status_code, value: '200'}
+      skipAnalysis: 'false'
diff --git a/test/plugin/scenarios/struts2.7-scenario/configuration.yml 
b/test/plugin/scenarios/struts2.7-scenario/configuration.yml
new file mode 100644
index 0000000000..b16e56768e
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/configuration.yml
@@ -0,0 +1,21 @@
+# 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.
+
+type: tomcat
+entryService: http://localhost:8080/struts2.7-scenario/case/case.action
+healthCheck: http://localhost:8080/struts2.7-scenario/healthCheck
+environment:
+dependencies:
diff --git a/test/plugin/scenarios/struts2.7-scenario/pom.xml 
b/test/plugin/scenarios/struts2.7-scenario/pom.xml
new file mode 100644
index 0000000000..beede09526
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/pom.xml
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 
http://maven.apache.org/xsd/maven-4.0.0.xsd";>
+
+    <groupId>org.apache.skywalking.apm.testcase</groupId>
+    <artifactId>struts2.7-scenario</artifactId>
+    <version>1.0.0</version>
+    <packaging>war</packaging>
+
+    <modelVersion>4.0.0</modelVersion>
+
+    <name>skywalking-struts2.7-scenario</name>
+
+    <properties>
+        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+        <!-- Struts 7 is Jakarta-only (Jakarta Servlet 6.0) and requires JDK 
17+. -->
+        <compiler.version>17</compiler.version>
+        <maven-compiler-plugin.version>3.8.1</maven-compiler-plugin.version>
+        <test.framework.version>7.2.1</test.framework.version>
+        <jakarta-servlet-api.version>6.0.0</jakarta-servlet-api.version>
+        <lombok.version>1.18.42</lombok.version>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>jakarta.servlet</groupId>
+            <artifactId>jakarta.servlet-api</artifactId>
+            <version>${jakarta-servlet-api.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-api</artifactId>
+            <version>2.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.logging.log4j</groupId>
+            <artifactId>log4j-core</artifactId>
+            <version>2.8.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <version>${lombok.version}</version>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.struts</groupId>
+            <artifactId>struts2-core</artifactId>
+            <version>${test.framework.version}</version>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.httpcomponents</groupId>
+            <artifactId>httpclient</artifactId>
+            <version>4.3</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <finalName>struts2.7-scenario</finalName>
+        <plugins>
+            <plugin>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <version>${maven-compiler-plugin.version}</version>
+                <configuration>
+                    <source>${compiler.version}</source>
+                    <target>${compiler.version}</target>
+                    <encoding>${project.build.sourceEncoding}</encoding>
+                </configuration>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.3.1</version>
+            </plugin>
+        </plugins>
+    </build>
+</project>
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
new file mode 100644
index 0000000000..9f40d9c99f
--- /dev/null
+++ 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/Case1Action.java
@@ -0,0 +1,28 @@
+/*
+ * 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.skywalking.apm.testcase.struts;
+
+import java.io.IOException;
+
+public class Case1Action {
+
+    public String execute() throws IOException {
+        return "SUCCESS";
+    }
+}
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
new file mode 100644
index 0000000000..89a4c70e36
--- /dev/null
+++ 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/CaseAction.java
@@ -0,0 +1,47 @@
+/*
+ * 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.skywalking.apm.testcase.struts;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.client.ResponseHandler;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.impl.client.CloseableHttpClient;
+import org.apache.http.impl.client.HttpClients;
+import org.apache.http.util.EntityUtils;
+
+import java.io.IOException;
+
+public class CaseAction {
+
+    public String execute() throws IOException {
+        visit();
+        return "SUCCESS";
+    }
+
+    private static void visit() throws IOException {
+        try (CloseableHttpClient httpClient = HttpClients.createDefault()) {
+            HttpGet httpget = new 
HttpGet("http://localhost:8080/struts2.7-scenario/case/case1.action";);
+            ResponseHandler<String> responseHandler = response -> {
+                HttpEntity entity = response.getEntity();
+                return entity != null ? EntityUtils.toString(entity) : null;
+            };
+            httpClient.execute(httpget, responseHandler);
+        }
+    }
+}
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
new file mode 100644
index 0000000000..31458cea43
--- /dev/null
+++ 
b/test/plugin/scenarios/struts2.7-scenario/src/main/java/org/apache/skywalking/apm/testcase/struts/HealthCheckServlet.java
@@ -0,0 +1,41 @@
+/*
+ * 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.skywalking.apm.testcase.struts;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServlet;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+public class HealthCheckServlet extends HttpServlet {
+
+    @Override
+    protected void doGet(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        // your codes
+        PrintWriter writer = resp.getWriter();
+        writer.write("Success");
+        writer.flush();
+    }
+
+    @Override
+    protected void doPost(HttpServletRequest req, HttpServletResponse resp) 
throws ServletException, IOException {
+        doGet(req, resp);
+    }
+}
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml 
b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml
new file mode 100644
index 0000000000..b5cda5ae8a
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/log4j2.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ 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.
+  ~
+  -->
+<Configuration status="WARN">
+    <Appenders>
+        <Console name="Console" target="SYSTEM_ERR">
+            <PatternLayout charset="UTF-8" pattern="[%d{yyyy-MM-dd 
HH:mm:ss:SSS}] [%p] - %l - %m%n"/>
+        </Console>
+    </Appenders>
+    <Loggers>
+        <Root level="WARN">
+            <AppenderRef ref="Console"/>
+        </Root>
+    </Loggers>
+</Configuration>
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml 
b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml
new file mode 100644
index 0000000000..62faf006d4
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/resources/struts.xml
@@ -0,0 +1,46 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!--
+  ~ 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.
+  ~
+  -->
+<!DOCTYPE struts PUBLIC
+    "-//Apache Software Foundation//DTD Struts Configuration 6.5//EN"
+    "https://struts.apache.org/dtds/struts-6.5.dtd";>
+
+<!--
+  The actions intentionally use the lightweight `httpheader` result rather 
than a JSP view.
+  The entry action (case.action) makes a nested internal call (case1.action) 
within the same
+  cold request; a JSP result would trigger a cold Jasper compile on that 
nested thread at the
+  same time the agent enhances the Struts/HttpClient classes, and the two 
cold-start storms
+  contend badly under the plugin-test container's limited CPU/heap, pushing 
the first request
+  past the framework's 3s entry-service timeout. The result view is irrelevant 
to what this
+  scenario verifies (the Struts entry span, the HttpClient exit span, and 
cross-process
+  propagation), so a no-view result keeps the cold path fast without losing 
any coverage.
+-->
+<struts>
+    <package name="case" namespace="/case" extends="struts-default">
+        <action name="case" 
class="org.apache.skywalking.apm.testcase.struts.CaseAction">
+            <result name="SUCCESS" type="httpheader">
+                <param name="status">200</param>
+            </result>
+        </action>
+        <action name="case1" 
class="org.apache.skywalking.apm.testcase.struts.Case1Action">
+            <result name="SUCCESS" type="httpheader">
+                <param name="status">200</param>
+            </result>
+        </action>
+    </package>
+</struts>
diff --git 
a/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml 
b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml
new file mode 100644
index 0000000000..a43b33cc55
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/src/main/webapp/WEB-INF/web.xml
@@ -0,0 +1,44 @@
+<!--
+  ~ 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.
+  ~
+  -->
+<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee
+                      https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd";
+         version="6.0">
+    <display-name>skywalking-struts2.7-scenario</display-name>
+
+    <filter>
+        <filter-name>struts2</filter-name>
+        
<filter-class>org.apache.struts2.dispatcher.filter.StrutsPrepareAndExecuteFilter</filter-class>
+    </filter>
+
+    <filter-mapping>
+        <filter-name>struts2</filter-name>
+        <url-pattern>/case/*</url-pattern>
+    </filter-mapping>
+
+    <servlet>
+        <servlet-name>healthCheck</servlet-name>
+        
<servlet-class>org.apache.skywalking.apm.testcase.struts.HealthCheckServlet</servlet-class>
+    </servlet>
+
+    <servlet-mapping>
+        <servlet-name>healthCheck</servlet-name>
+        <url-pattern>/healthCheck</url-pattern>
+    </servlet-mapping>
+</web-app>
diff --git a/test/plugin/scenarios/struts2.7-scenario/support-version.list 
b/test/plugin/scenarios/struts2.7-scenario/support-version.list
new file mode 100644
index 0000000000..5fcb5f103f
--- /dev/null
+++ b/test/plugin/scenarios/struts2.7-scenario/support-version.list
@@ -0,0 +1,19 @@
+# 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.
+
+7.0.3
+7.1.1
+7.2.1

Reply via email to