Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.next-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This file contains all changes which are not released yet.
<!--FIXES-END-->
# Features and enhancements
<!--ENHANCEMENTS-START-->

* Support for Spring Framework 7 - [#4345](https://github.com/elastic/apm-agent-java/pull/4398)
<!--ENHANCEMENTS-END-->
# Deprecations
<!--DEPRECATIONS-START-->
Expand Down
Original file line number Diff line number Diff line change
@@ -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()+"'!");
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,6 @@
<feign.version>13.0</feign.version>
</properties>

<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.7</version> <!-- update IT in case of major upgrade -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.springframework</groupId>
Expand Down Expand Up @@ -120,4 +107,44 @@
</plugins>
</build>

<profiles>
<profile>
<id>spring-boot3</id>
<activation>
<activeByDefault>true</activeByDefault>
</activation>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.5.7</version> <!-- update IT in case of major upgrade -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>
<profile>
<id>spring-boot4</id>
<activation>
<activeByDefault>false</activeByDefault>
</activation>
<dependencyManagement>
<dependencies>
<dependency>
<!-- Import dependency management from Spring Boot -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>4.0.2</version> <!-- update IT in case of major upgrade -->
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</profile>

</profiles>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<HttpRequest> {


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;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Class<? extends ClientHttpRequestFactory>, 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;

Expand Down Expand Up @@ -94,21 +108,15 @@ public static void performPost(Object restTemplateObj, String path, byte[] conte
}

public static Iterable<Supplier<RestTemplate>> getRestTemplateFactories() {
return Stream.<Supplier<ClientHttpRequestFactory>>of(
SimpleClientHttpRequestFactory::new,
OkHttp3ClientHttpRequestFactory::new,
HttpComponentsClientHttpRequestFactory::new)
.map(fac -> (Supplier<RestTemplate>) (() -> new RestTemplate(fac.get())))
.collect(Collectors.toList());
return factories.keySet()
.stream()
.map((clz) -> BeanUtils.instantiateClass(clz))
.map( (fac) -> (Supplier<RestTemplate>) () -> 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) {
Expand Down
1 change: 0 additions & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,6 @@

<maven.compiler.target>7</maven.compiler.target>
<maven.compiler.testTarget>11</maven.compiler.testTarget>

<maven.compiler.source>${maven.compiler.target}</maven.compiler.source>
<maven.compiler.testSource>${maven.compiler.testTarget}</maven.compiler.testSource>

Expand Down
Loading