diff --git a/CHANGELOG.next-release.md b/CHANGELOG.next-release.md
index 74d5d98219..d1a9588546 100644
--- a/CHANGELOG.next-release.md
+++ b/CHANGELOG.next-release.md
@@ -15,7 +15,7 @@ This file contains all changes which are not released yet.
# Features and enhancements
-
+* Support for Spring Framework 7 - [#4345](https://github.com/elastic/apm-agent-java/pull/4398)
# Deprecations
diff --git a/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/util/MethodLookupUtil.java b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/util/MethodLookupUtil.java
new file mode 100644
index 0000000000..abfd195bb4
--- /dev/null
+++ b/apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/util/MethodLookupUtil.java
@@ -0,0 +1,51 @@
+/*
+ * Licensed to Elasticsearch B.V. under one or more contributor
+ * license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright
+ * ownership. Elasticsearch B.V. 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 co.elastic.apm.agent.sdk.internal.util;
+
+import javax.annotation.Nullable;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.util.Arrays;
+
+public class MethodLookupUtil {
+
+ private static final MethodHandles.Lookup lookup = MethodHandles.lookup();
+
+ @Nullable
+ public static MethodHandle find(Class> clazz, String methodName, Class> rtype, Class>... atypes) {
+ final MethodType type = MethodType.methodType(rtype, atypes);
+ try {
+ return lookup.findVirtual(clazz, methodName, type);
+ } catch (NoSuchMethodException | IllegalAccessException ex) {
+ return null;
+ }
+ }
+
+ public static MethodHandle findOneOf(Class> clazz, String[] methodNames, Class> rtype, Class>... atypes) {
+ for (String methodName : methodNames) {
+ MethodHandle handle = find(clazz, methodName, rtype, atypes);
+ if (handle != null) {
+ return handle;
+ }
+ }
+ throw new IllegalStateException("Cannot find one of the methods ['"+ Arrays.asList(methodNames)+"'] for class '"+clazz.getName()+"'!");
+ }
+}
+
diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
index 8482edab22..606eb79c3a 100644
--- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
+++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/pom.xml
@@ -17,19 +17,6 @@
13.0
-
-
-
-
- org.springframework.boot
- spring-boot-dependencies
- 3.5.7
- pom
- import
-
-
-
-
org.springframework
@@ -120,4 +107,44 @@
+
+
+ spring-boot3
+
+ true
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 3.5.7
+ pom
+ import
+
+
+
+
+
+ spring-boot4
+
+ false
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-dependencies
+ 4.0.2
+ pom
+ import
+
+
+
+
+
+
+
diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/ClientHttpResponseAdapter.java b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/ClientHttpResponseAdapter.java
index ddce079000..6c66901d86 100644
--- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/ClientHttpResponseAdapter.java
+++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/ClientHttpResponseAdapter.java
@@ -18,10 +18,17 @@
*/
package co.elastic.apm.agent.resttemplate;
+import co.elastic.apm.agent.sdk.internal.util.MethodLookupUtil;
import org.springframework.http.client.ClientHttpResponse;
+import javax.annotation.Nullable;
+import java.lang.invoke.MethodHandle;
+
public class ClientHttpResponseAdapter {
+ @Nullable
+ private static final MethodHandle STATUS_CODE_METHOD =
+ MethodLookupUtil.find(ClientHttpResponse.class, "getRawStatusCode", int.class);
private static final int UNKNOWN_STATUS = -1;
public static int getStatusCode(ClientHttpResponse response) {
@@ -45,9 +52,11 @@ private static int legacyGetStatusCode(ClientHttpResponse response) {
// getRawStatusCode has been introduced in 3.1.1
// but deprecated in 6.x, will be removed in 7.x (using method handle will be needed).
try {
- return response.getRawStatusCode();
- } catch (Exception|Error e) {
- // using broad exception to handle when method is missing in pre-3.1.1 and post 7.x
+ if (STATUS_CODE_METHOD != null) {
+ return (int) STATUS_CODE_METHOD.invoke(response);
+ }
+ return UNKNOWN_STATUS;
+ } catch (Throwable th) {
return UNKNOWN_STATUS;
}
}
diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/SpringRestRequestHeaderSetter.java b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/SpringRestRequestHeaderSetter.java
index ff04b829bc..760629e116 100644
--- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/SpringRestRequestHeaderSetter.java
+++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/main/java/co/elastic/apm/agent/resttemplate/SpringRestRequestHeaderSetter.java
@@ -18,18 +18,34 @@
*/
package co.elastic.apm.agent.resttemplate;
+import co.elastic.apm.agent.sdk.internal.util.MethodLookupUtil;
import co.elastic.apm.agent.tracer.dispatch.TextHeaderSetter;
+import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
+import java.lang.invoke.MethodHandle;
+
public class SpringRestRequestHeaderSetter implements TextHeaderSetter {
+
+ private static final MethodHandle CONTAINS_METHOD =
+ MethodLookupUtil.findOneOf(HttpHeaders.class, new String[] {"containsKey", "containsHeader"}, boolean.class, Object.class);
+
public static final SpringRestRequestHeaderSetter INSTANCE = new SpringRestRequestHeaderSetter();
@Override
public void setHeader(String headerName, String headerValue, HttpRequest request) {
- if (!request.getHeaders().containsKey(headerName)) {
+ if (!containsHeader(request.getHeaders(), headerName)) {
// the org.springframework.http.HttpRequest has only be introduced in 3.1.0
request.getHeaders().add(headerName, headerValue);
}
}
+
+ private boolean containsHeader(HttpHeaders headers, String headerName) {
+ try {
+ return (boolean) CONTAINS_METHOD.invoke(headers, headerName);
+ } catch (Throwable e) {
+ return false;
+ }
+ }
}
diff --git a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/SpringRestTemplateInstrumentationTest.java b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/SpringRestTemplateInstrumentationTest.java
index c666384e7e..2bb7ee2b5a 100644
--- a/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/SpringRestTemplateInstrumentationTest.java
+++ b/apm-agent-plugins/apm-spring-resttemplate/apm-spring-resttemplate-plugin/src/test/java/co/elastic/apm/agent/resttemplate/SpringRestTemplateInstrumentationTest.java
@@ -22,23 +22,37 @@
import co.elastic.apm.agent.httpclient.AbstractHttpClientInstrumentationTest;
import org.junit.runner.RunWith;
import org.junit.runners.Parameterized;
+import org.springframework.beans.BeanUtils;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
-import org.springframework.http.client.OkHttp3ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
+import org.springframework.util.ClassUtils;
import org.springframework.web.client.RestTemplate;
+import java.util.HashMap;
import java.util.List;
+import java.util.Map;
import java.util.function.Supplier;
import java.util.stream.Collectors;
-import java.util.stream.Stream;
@RunWith(Parameterized.class)
public class SpringRestTemplateInstrumentationTest extends AbstractHttpClientInstrumentationTest {
+ private static final Map, Boolean> factories = new HashMap<>();
+ {
+ factories.put(SimpleClientHttpRequestFactory.class, true);
+ factories.put(HttpComponentsClientHttpRequestFactory.class, true);
+ try {
+ factories.put((Class extends ClientHttpRequestFactory>) ClassUtils.forName("org.springframework.http.client.OkHttp3ClientHttpRequestFactory", getClass().getClassLoader()), false);
+ } catch (ClassNotFoundException cnfe) {
+ // Ignore
+ }
+
+ }
+
// Cannot directly reference RestTemplate here because it is compiled with Java 17
private final Object restTemplate;
@@ -94,21 +108,15 @@ public static void performPost(Object restTemplateObj, String path, byte[] conte
}
public static Iterable> getRestTemplateFactories() {
- return Stream.>of(
- SimpleClientHttpRequestFactory::new,
- OkHttp3ClientHttpRequestFactory::new,
- HttpComponentsClientHttpRequestFactory::new)
- .map(fac -> (Supplier) (() -> new RestTemplate(fac.get())))
- .collect(Collectors.toList());
+ return factories.keySet()
+ .stream()
+ .map((clz) -> BeanUtils.instantiateClass(clz))
+ .map( (fac) -> (Supplier) () -> new RestTemplate(fac)).collect(Collectors.toList());
}
public static boolean isBodyCapturingSupported(Object restTemplateObj) {
RestTemplate restTemplate = (RestTemplate) restTemplateObj;
- if (restTemplate.getRequestFactory() instanceof OkHttp3ClientHttpRequestFactory) {
- // We do not support body capturing for OkHttp yet
- return false;
- }
- return true;
+ return factories.get(restTemplate.getRequestFactory().getClass());
}
public static boolean isTestHttpCallWithUserInfoEnabled(Object restTemplateObj) {
diff --git a/pom.xml b/pom.xml
index 7dd82797be..9609bcb3d4 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,7 +71,6 @@
7
11
-
${maven.compiler.target}
${maven.compiler.testTarget}