From d98cfad6208ed6310a583eaa64710db75b2778db Mon Sep 17 00:00:00 2001 From: Marten Deinum Date: Thu, 12 Feb 2026 07:51:38 +0100 Subject: [PATCH 1/2] Dynamically adapt between Spring Framework 6 (and potentially lower) and Spring Framework 7. --- .../sdk/internal/util/MethodLookupUtil.java | 51 ++++++++++++++++++ .../apm-spring-resttemplate-plugin/pom.xml | 53 ++++++++++++++----- .../ClientHttpResponseAdapter.java | 15 ++++-- .../SpringRestRequestHeaderSetter.java | 18 ++++++- ...SpringRestTemplateInstrumentationTest.java | 34 +++++++----- pom.xml | 1 - 6 files changed, 141 insertions(+), 31 deletions(-) create mode 100644 apm-agent-plugin-sdk/src/main/java/co/elastic/apm/agent/sdk/internal/util/MethodLookupUtil.java 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) 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} From c270bc968113d836c194605c1689b98affa79b52 Mon Sep 17 00:00:00 2001 From: Marten Deinum Date: Thu, 12 Feb 2026 07:59:14 +0100 Subject: [PATCH 2/2] Dynamically adapt between Spring Framework 6 (and potentially lower) and Spring Framework 7. Changelog update --- CHANGELOG.next-release.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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