Skip to content

Commit

Permalink
feat: support feign (#323)
Browse files Browse the repository at this point in the history
feat: support feign
  • Loading branch information
YongwuHe authored Nov 16, 2023
1 parent 4747464 commit 66735f4
Show file tree
Hide file tree
Showing 12 changed files with 488 additions and 1 deletion.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ AREX utilizes the advanced Java technique, Instrument API, and is capable of ins
- OkHttp [3.0, 4.11]
- Spring WebClient [5.0,)
- Spring Template
- Feign [9.0,)
#### Redis Client
- Jedis [2.10+, 4+]
- Redisson [3.0,)
Expand Down
5 changes: 5 additions & 0 deletions arex-agent/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@
<artifactId>arex-httpclient-okhttp-v3</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>arex-httpclient-feign</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>arex-netty-v3</artifactId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ public class HttpResponseWrapper {
private byte[] content;
private StringTuple locale;
private List<StringTuple> headers;
private String reason;

public String getReason() {
return reason;
}

public void setReason(String reason) {
this.reason = reason;
}

public void setHeaders(List<StringTuple> headers) {
this.headers = headers;
}
Expand Down
29 changes: 29 additions & 0 deletions arex-instrumentation/httpclient/arex-httpclient-feign/pom.xml
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>
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();
}
}
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);
}
}
}
}
}
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());
}
}
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());
}
}
Loading

0 comments on commit 66735f4

Please sign in to comment.