-
Notifications
You must be signed in to change notification settings - Fork 102
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: support feign
- Loading branch information
Showing
12 changed files
with
488 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
29 changes: 29 additions & 0 deletions
29
arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<parent> | ||
<groupId>io.arex</groupId> | ||
<artifactId>arex-instrumentation-parent</artifactId> | ||
<version>${revision}</version> | ||
<relativePath>../../pom.xml</relativePath> | ||
</parent> | ||
<modelVersion>4.0.0</modelVersion> | ||
<artifactId>arex-httpclient-feign</artifactId> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>${project.groupId}</groupId> | ||
<artifactId>arex-httpclient-common</artifactId> | ||
<version>${project.version}</version> | ||
<scope>compile</scope> | ||
</dependency> | ||
<dependency> | ||
<groupId>io.github.openfeign</groupId> | ||
<artifactId>feign-core</artifactId> | ||
<version>9.4.0</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
</project> |
109 changes: 109 additions & 0 deletions
109
...arex-httpclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientAdapter.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
package io.arex.inst.httpclient.feign; | ||
|
||
import feign.Request; | ||
import feign.Response; | ||
import feign.Response.Body; | ||
import feign.Util; | ||
import io.arex.agent.bootstrap.util.CollectionUtil; | ||
import io.arex.inst.httpclient.common.HttpClientAdapter; | ||
import io.arex.inst.httpclient.common.HttpResponseWrapper; | ||
import io.arex.inst.httpclient.common.HttpResponseWrapper.StringTuple; | ||
import io.arex.inst.runtime.log.LogManager; | ||
import java.net.URI; | ||
import java.util.ArrayList; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import java.util.List; | ||
import java.util.Map; | ||
|
||
public class FeignClientAdapter implements HttpClientAdapter<Request, Response> { | ||
private static final String CONTENT_TYPE = "Content-Type"; | ||
private final Request request; | ||
private final URI uri; | ||
private byte[] responseBody; | ||
|
||
public FeignClientAdapter(Request request, URI uri) { | ||
this.request = request; | ||
this.uri = uri; | ||
} | ||
|
||
@Override | ||
public String getMethod() { | ||
return request.method(); | ||
} | ||
|
||
@Override | ||
public byte[] getRequestBytes() { | ||
return request.body(); | ||
} | ||
|
||
@Override | ||
public String getRequestContentType() { | ||
return getRequestHeader(CONTENT_TYPE); | ||
} | ||
|
||
@Override | ||
public String getRequestHeader(String name) { | ||
final Collection<String> values = request.headers().get(name); | ||
if (CollectionUtil.isEmpty(values)) { | ||
return null; | ||
} | ||
return values.iterator().next(); | ||
} | ||
|
||
@Override | ||
public URI getUri() { | ||
return uri; | ||
} | ||
|
||
@Override | ||
public HttpResponseWrapper wrap(Response response) { | ||
final String statusLine = String.valueOf(response.status()); | ||
final List<StringTuple> headers = new ArrayList<>(response.headers().size()); | ||
response.headers().forEach((k, v) -> headers.add(new StringTuple(k, v.iterator().next()))); | ||
HttpResponseWrapper responseWrapper = new HttpResponseWrapper(statusLine, responseBody, null, headers); | ||
responseWrapper.setReason(response.reason()); | ||
return responseWrapper; | ||
} | ||
|
||
@Override | ||
public Response unwrap(HttpResponseWrapper wrapped) { | ||
final int status = parseInt(wrapped.getStatusLine()); | ||
byte[] responseContent = wrapped.getContent(); | ||
final List<StringTuple> wrappedHeaders = wrapped.getHeaders(); | ||
Map<String, Collection<String>> headers = new HashMap<>(wrappedHeaders.size()); | ||
for (StringTuple header : wrappedHeaders) { | ||
headers.put(header.name(), Collections.singletonList(header.value())); | ||
} | ||
return Response.builder().body(responseContent).status(status).headers(headers).reason(wrapped.getReason()).request(request).build(); | ||
} | ||
|
||
private int parseInt(String statusLine) { | ||
try { | ||
return Integer.parseInt(statusLine); | ||
} catch (Exception ex) { | ||
LogManager.warn("feign.parseInt", "statusLine: " + statusLine, ex); | ||
return -1; | ||
} | ||
} | ||
|
||
public Response copyResponse(Response response) { | ||
if (response == null) { | ||
return null; | ||
} | ||
final Body body = response.body(); | ||
if (body == null) { | ||
return response; | ||
} | ||
try { | ||
responseBody = Util.toByteArray(body.asInputStream()); | ||
} catch (Exception ex) { | ||
LogManager.warn("feign.copyResponse", "uri: " + getUri(), ex); | ||
} | ||
if (body.isRepeatable()) { | ||
return response; | ||
} | ||
return response.toBuilder().body(responseBody).build(); | ||
} | ||
} |
95 changes: 95 additions & 0 deletions
95
...pclient-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientInstrumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,95 @@ | ||
package io.arex.inst.httpclient.feign; | ||
|
||
import feign.Request; | ||
import feign.Response; | ||
import io.arex.agent.bootstrap.model.MockResult; | ||
import io.arex.inst.extension.MethodInstrumentation; | ||
import io.arex.inst.extension.TypeInstrumentation; | ||
import io.arex.inst.httpclient.common.HttpClientExtractor; | ||
import io.arex.inst.runtime.context.ContextManager; | ||
import io.arex.inst.runtime.context.RepeatedCollectManager; | ||
import io.arex.inst.runtime.util.IgnoreUtils; | ||
import java.net.URI; | ||
import java.net.URL; | ||
import java.util.Collections; | ||
import java.util.List; | ||
import net.bytebuddy.asm.Advice; | ||
import net.bytebuddy.asm.Advice.Argument; | ||
import net.bytebuddy.asm.Advice.Local; | ||
import net.bytebuddy.asm.Advice.OnMethodEnter; | ||
import net.bytebuddy.asm.Advice.OnMethodExit; | ||
import net.bytebuddy.asm.Advice.OnNonDefaultValue; | ||
import net.bytebuddy.asm.Advice.Return; | ||
import net.bytebuddy.description.type.TypeDescription; | ||
import net.bytebuddy.implementation.bytecode.assign.Assigner.Typing; | ||
import net.bytebuddy.matcher.ElementMatcher; | ||
|
||
import static net.bytebuddy.matcher.ElementMatchers.*; | ||
|
||
public class FeignClientInstrumentation extends TypeInstrumentation { | ||
|
||
@Override | ||
public ElementMatcher<TypeDescription> typeMatcher() { | ||
return hasSuperType(named("feign.Client")).and(not(isInterface())); | ||
} | ||
|
||
@Override | ||
public List<MethodInstrumentation> methodAdvices() { | ||
return Collections.singletonList(new MethodInstrumentation( | ||
named("execute").and(takesArguments(2)) | ||
.and(takesArgument(0, named("feign.Request"))), | ||
ExecuteAdvice.class.getName())); | ||
} | ||
|
||
public static class ExecuteAdvice{ | ||
@OnMethodEnter(skipOn = OnNonDefaultValue.class, suppress = Throwable.class) | ||
public static boolean onEnter(@Argument(0)Request request, | ||
@Local("adapter") FeignClientAdapter adapter, | ||
@Local("extractor") HttpClientExtractor extractor, | ||
@Local("mockResult") MockResult mockResult) { | ||
if (ContextManager.needRecordOrReplay()) { | ||
final URI uri = URI.create(request.url()); | ||
if (IgnoreUtils.excludeOperation(uri.getPath())) { | ||
return false; | ||
} | ||
RepeatedCollectManager.enter(); | ||
adapter = new FeignClientAdapter(request, uri); | ||
extractor = new HttpClientExtractor(adapter); | ||
if (ContextManager.needReplay()) { | ||
mockResult = extractor.replay(); | ||
return mockResult != null && mockResult.notIgnoreMockResult(); | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
@OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) | ||
public static void onExit(@Local("adapter") FeignClientAdapter adapter, | ||
@Local("extractor") HttpClientExtractor extractor, | ||
@Local("mockResult") MockResult mockResult, | ||
@Return(readOnly = false, typing = Typing.DYNAMIC) Response response, | ||
@Advice.Thrown(readOnly = false) Throwable throwable){ | ||
if (extractor == null) { | ||
return; | ||
} | ||
|
||
if (mockResult != null && mockResult.notIgnoreMockResult()) { | ||
if (mockResult.getThrowable() != null) { | ||
throwable = mockResult.getThrowable(); | ||
} else { | ||
response = (Response) mockResult.getResult(); | ||
} | ||
return; | ||
} | ||
|
||
if (ContextManager.needRecord() && RepeatedCollectManager.exitAndValidate()) { | ||
response = adapter.copyResponse(response); | ||
if (throwable != null) { | ||
extractor.record(throwable); | ||
} else { | ||
extractor.record(response); | ||
} | ||
} | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
...t-feign/src/main/java/io/arex/inst/httpclient/feign/FeignClientModuleInstrumentation.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package io.arex.inst.httpclient.feign; | ||
|
||
import com.google.auto.service.AutoService; | ||
import io.arex.inst.extension.ModuleInstrumentation; | ||
import io.arex.inst.extension.TypeInstrumentation; | ||
import java.util.Collections; | ||
import java.util.List; | ||
|
||
@AutoService(ModuleInstrumentation.class) | ||
public class FeignClientModuleInstrumentation extends ModuleInstrumentation { | ||
|
||
public FeignClientModuleInstrumentation() { | ||
super("feign-client"); | ||
} | ||
|
||
@Override | ||
public List<TypeInstrumentation> instrumentationTypes() { | ||
return Collections.singletonList(new FeignClientInstrumentation()); | ||
} | ||
} |
96 changes: 96 additions & 0 deletions
96
...-httpclient-feign/src/test/java/io/arex/inst/httpclient/feign/FeignClientAdapterTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,96 @@ | ||
package io.arex.inst.httpclient.feign; | ||
|
||
import static org.junit.jupiter.api.Assertions.*; | ||
|
||
import feign.Request; | ||
import feign.Response; | ||
import feign.Util; | ||
import io.arex.inst.httpclient.common.HttpResponseWrapper; | ||
import java.io.ByteArrayInputStream; | ||
import java.io.IOException; | ||
import java.io.InputStream; | ||
import java.net.URI; | ||
import java.util.Collection; | ||
import java.util.Collections; | ||
import java.util.HashMap; | ||
import org.junit.jupiter.api.BeforeAll; | ||
import org.junit.jupiter.api.MethodOrderer; | ||
import org.junit.jupiter.api.Order; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.api.TestMethodOrder; | ||
|
||
@TestMethodOrder(MethodOrderer.OrderAnnotation.class) | ||
class FeignClientAdapterTest { | ||
private static FeignClientAdapter feignClientAdapter; | ||
|
||
@BeforeAll | ||
static void setUp() { | ||
final HashMap<String, Collection<String>> headers = new HashMap<>(); | ||
headers.put("testKey", Collections.singletonList("testValue")); | ||
Request request = Request.create("post", "http://localhost:8080/test", headers, null, null); | ||
feignClientAdapter = new FeignClientAdapter(request, URI.create("http://localhost:8080/test")); | ||
} | ||
|
||
@Test | ||
void getMethod() { | ||
assertEquals("post", feignClientAdapter.getMethod()); | ||
} | ||
|
||
@Test | ||
void getRequestBytes() { | ||
assertNull(feignClientAdapter.getRequestBytes()); | ||
} | ||
|
||
@Test | ||
void getRequestContentType() { | ||
assertNull(feignClientAdapter.getRequestContentType()); | ||
} | ||
|
||
@Test | ||
void getRequestHeader() { | ||
assertNull(feignClientAdapter.getRequestHeader("test")); | ||
assertEquals("testValue", feignClientAdapter.getRequestHeader("testKey")); | ||
} | ||
|
||
@Test | ||
void getUri() { | ||
assertEquals("http://localhost:8080/test", feignClientAdapter.getUri().toString()); | ||
} | ||
|
||
@Test | ||
void wrapAndunwrap() throws IOException { | ||
final HashMap<String, Collection<String>> responseHeaders = new HashMap<>(); | ||
responseHeaders.put("testKey", Collections.singletonList("testValue")); | ||
byte[] body = "testResponse".getBytes(); | ||
final Response response = Response.builder().body(body).reason("test").status(200).headers(responseHeaders).build(); | ||
final HttpResponseWrapper wrap = feignClientAdapter.wrap(response); | ||
assertEquals("testResponse", new String(wrap.getContent())); | ||
|
||
final Response unwrap = feignClientAdapter.unwrap(wrap); | ||
final InputStream bodyStream = unwrap.body().asInputStream(); | ||
final byte[] bytes = Util.toByteArray(bodyStream); | ||
assertEquals("testResponse", new String(bytes)); | ||
} | ||
|
||
@Test | ||
@Order(1) | ||
void copyResponse() { | ||
// null response | ||
assertNull(feignClientAdapter.copyResponse(null)); | ||
|
||
byte[] body = "testResponse".getBytes(); | ||
// repeatable response | ||
final Response repeatResponse = Response.builder().body(body).reason("test").status(200).headers(new HashMap<>()).build(); | ||
final Response copyRepeatResponse = feignClientAdapter.copyResponse(repeatResponse); | ||
assertTrue(copyRepeatResponse.body().isRepeatable()); | ||
assertEquals(repeatResponse.hashCode(), copyRepeatResponse.hashCode()); | ||
|
||
// not repeatable response | ||
final ByteArrayInputStream inputStream = new ByteArrayInputStream(body); | ||
final Response unRepeatResponse = Response.builder().body(inputStream, 1024).reason("test").status(200).headers(new HashMap<>()).build(); | ||
assertFalse(unRepeatResponse.body().isRepeatable()); | ||
final Response copyUnRepeatResponse = feignClientAdapter.copyResponse(unRepeatResponse); | ||
assertTrue(copyUnRepeatResponse.body().isRepeatable()); | ||
assertNotEquals(unRepeatResponse.hashCode(), copyUnRepeatResponse.hashCode()); | ||
} | ||
} |
Oops, something went wrong.