From b3d7229ec4d5bb60f8d524211849e86ec8ac290b Mon Sep 17 00:00:00 2001 From: Daniel Wu Date: Fri, 29 Dec 2017 17:34:04 +0800 Subject: [PATCH 1/4] add query object encoder for GET request --- core/src/main/java/feign/QueryObject.java | 23 ++++++++ .../java/feign/codec/QueryObjectEncoder.java | 59 +++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 core/src/main/java/feign/QueryObject.java create mode 100644 core/src/main/java/feign/codec/QueryObjectEncoder.java diff --git a/core/src/main/java/feign/QueryObject.java b/core/src/main/java/feign/QueryObject.java new file mode 100644 index 0000000000..f0bc4a39d1 --- /dev/null +++ b/core/src/main/java/feign/QueryObject.java @@ -0,0 +1,23 @@ +package feign; + +import java.lang.annotation.Retention; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +/** + * This is a tag annotation to support Object Query. By default, Feign does not support object query with a GET request, + * You can use use QueryObjectEncoder to support this. + */ +@Retention(RUNTIME) +@java.lang.annotation.Target(TYPE) +public @interface QueryObject { + + @Retention(RUNTIME) + @java.lang.annotation.Target(TYPE) + @interface Param { + String value(); + } + +} + diff --git a/core/src/main/java/feign/codec/QueryObjectEncoder.java b/core/src/main/java/feign/codec/QueryObjectEncoder.java new file mode 100644 index 0000000000..3db0085f63 --- /dev/null +++ b/core/src/main/java/feign/codec/QueryObjectEncoder.java @@ -0,0 +1,59 @@ +package feign.codec; + + +import feign.Param; +import feign.QueryObject; +import feign.RequestTemplate; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.lang.reflect.Type; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +public class QueryObjectEncoder implements Encoder { + + private Encoder fallbackEncoder; + + public QueryObjectEncoder(Encoder fallbackEncoder) { + this.fallbackEncoder = fallbackEncoder; + } + + @Override + public void encode(Object parametersObject, Type bodyType, RequestTemplate template) throws EncodeException { + if (parametersObject.getClass().getAnnotation(QueryObject.class) != null) { + Map params = new HashMap<>(); + try { + for (Method method : parametersObject.getClass().getMethods()) { + QueryObject.Param param = method.getAnnotation(QueryObject.Param.class); + if (param != null && method.getName().startsWith("get")) { + String key = param.value(); + Object value = method.invoke(parametersObject); + if (value != null) { + if (List.class.isAssignableFrom(method.getReturnType())) { + value = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); + } else { + value = String.valueOf(value); + } + params.put(key, value); + template.query(key, keyToTemplate(key)); + } + } + } + template.resolve(params); + } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + throw new EncodeException("Could not encode object query correctly", e); + } + } else { + fallbackEncoder.encode(parametersObject, bodyType, template); + } + } + + private String keyToTemplate(String key) { + return "{" + key + "}"; + } + +} + From d04748b5671752548d8947098af7442ff2a8e592 Mon Sep 17 00:00:00 2001 From: Daniel Wu Date: Fri, 29 Dec 2017 17:44:37 +0800 Subject: [PATCH 2/4] downgrade to jdk 1.6 --- .../java/feign/codec/QueryObjectEncoder.java | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/feign/codec/QueryObjectEncoder.java b/core/src/main/java/feign/codec/QueryObjectEncoder.java index 3db0085f63..37150cfacd 100644 --- a/core/src/main/java/feign/codec/QueryObjectEncoder.java +++ b/core/src/main/java/feign/codec/QueryObjectEncoder.java @@ -1,7 +1,6 @@ package feign.codec; -import feign.Param; import feign.QueryObject; import feign.RequestTemplate; @@ -9,9 +8,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; import java.util.HashMap; -import java.util.List; import java.util.Map; -import java.util.stream.Collectors; public class QueryObjectEncoder implements Encoder { @@ -24,7 +21,7 @@ public QueryObjectEncoder(Encoder fallbackEncoder) { @Override public void encode(Object parametersObject, Type bodyType, RequestTemplate template) throws EncodeException { if (parametersObject.getClass().getAnnotation(QueryObject.class) != null) { - Map params = new HashMap<>(); + Map params = new HashMap(); try { for (Method method : parametersObject.getClass().getMethods()) { QueryObject.Param param = method.getAnnotation(QueryObject.Param.class); @@ -32,18 +29,16 @@ public void encode(Object parametersObject, Type bodyType, RequestTemplate templ String key = param.value(); Object value = method.invoke(parametersObject); if (value != null) { - if (List.class.isAssignableFrom(method.getReturnType())) { - value = ((List) value).stream().map(String::valueOf).collect(Collectors.toList()); - } else { - value = String.valueOf(value); - } - params.put(key, value); - template.query(key, keyToTemplate(key)); + value = String.valueOf(value); } + params.put(key, value); + template.query(key, keyToTemplate(key)); } } template.resolve(params); - } catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { + } catch (IllegalAccessException e) { + throw new EncodeException("Could not encode object query correctly", e); + } catch (InvocationTargetException e) { throw new EncodeException("Could not encode object query correctly", e); } } else { From b6601adc1110c9106125b7281deb2681035016d7 Mon Sep 17 00:00:00 2001 From: Daniel Wu Date: Fri, 29 Dec 2017 17:49:43 +0800 Subject: [PATCH 3/4] add more doc --- .../java/feign/codec/QueryObjectEncoder.java | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/core/src/main/java/feign/codec/QueryObjectEncoder.java b/core/src/main/java/feign/codec/QueryObjectEncoder.java index 37150cfacd..fedbe16d54 100644 --- a/core/src/main/java/feign/codec/QueryObjectEncoder.java +++ b/core/src/main/java/feign/codec/QueryObjectEncoder.java @@ -10,6 +10,26 @@ import java.util.HashMap; import java.util.Map; +/** + * You can create an QueryObject like this + *
+ * {@code
+ * @QueryObject
+ * class FindUsersQuery {
+ *    .....
+ *    @QueryObject.Param("userId")
+ *    long getUserId() {...}
+ *    @QueryObject.Param("userName")
+ *    long getUserName() {...}
+ * }
+ * 
+ * Then you can declare the client like this: + *
+ * {@code
+ * @RequestLine("GET /users?userId={userId}&userName={userName}")
+ * List getUsers(FindUsersQuery query);
+ * 
+ */ public class QueryObjectEncoder implements Encoder { private Encoder fallbackEncoder; From d749976331192e60978f52b63a7e8dddfeda0d03 Mon Sep 17 00:00:00 2001 From: Daniel Wu Date: Mon, 12 Feb 2018 18:28:56 +0800 Subject: [PATCH 4/4] add unit test and fix annotation --- core/src/main/java/feign/QueryObject.java | 3 +- core/src/test/java/feign/FeignTest.java | 77 ++++++++++++++++++++--- 2 files changed, 72 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/feign/QueryObject.java b/core/src/main/java/feign/QueryObject.java index f0bc4a39d1..6bfa38937c 100644 --- a/core/src/main/java/feign/QueryObject.java +++ b/core/src/main/java/feign/QueryObject.java @@ -2,6 +2,7 @@ import java.lang.annotation.Retention; +import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -14,7 +15,7 @@ public @interface QueryObject { @Retention(RUNTIME) - @java.lang.annotation.Target(TYPE) + @java.lang.annotation.Target(METHOD) @interface Param { String value(); } diff --git a/core/src/test/java/feign/FeignTest.java b/core/src/test/java/feign/FeignTest.java index 13a95efacf..190916d427 100644 --- a/core/src/test/java/feign/FeignTest.java +++ b/core/src/test/java/feign/FeignTest.java @@ -18,7 +18,9 @@ import com.google.gson.Gson; import com.google.gson.reflect.TypeToken; +import feign.codec.*; import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.RecordedRequest; import okhttp3.mockwebserver.SocketPolicy; import okhttp3.mockwebserver.MockWebServer; @@ -27,7 +29,6 @@ import java.util.HashMap; import java.util.LinkedHashMap; import okio.Buffer; -import org.assertj.core.api.Fail; import org.assertj.core.data.MapEntry; import org.junit.Rule; import org.junit.Test; @@ -43,14 +44,9 @@ import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; import feign.Target.HardCodedTarget; -import feign.codec.DecodeException; -import feign.codec.Decoder; -import feign.codec.EncodeException; -import feign.codec.Encoder; -import feign.codec.ErrorDecoder; -import feign.codec.StringDecoder; import feign.Feign.ResponseMappingDecoder; import static feign.Util.UTF_8; @@ -699,6 +695,39 @@ public void responseMapperIsAppliedBeforeDelegate() throws IOException { assertThat(output).isEqualTo("RESPONSE"); } + @Test + public void okWithQueryObject() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + TestInterface api = new TestInterfaceBuilder().encoder(new QueryObjectEncoder(new Encoder.Default())). + target("http://localhost:" + server.getPort()); + TestInterface.QueryObjectTestParam testParam = new TestInterface.QueryObjectTestParam("aa", 888); + testParam.setK3("123"); + api.queryObjectWithQueryParams(testParam); + RecordedRequest req = server.takeRequest(); + assertThat(req.getMethod()).isEqualToIgnoringCase("GET"); + Map map = Arrays.stream(req.getPath().substring(2).split("&")).map(kv -> kv.split("=")). + collect(Collectors.toMap(kv -> kv[0], kv -> kv[1])); + assertThat(map.get("k1")).isEqualTo("aa"); + assertThat(map.get("k2")).isEqualTo("888"); + assertThat(map.get("k3")).isEqualTo("123"); + } + + @Test + public void okWithQueryObjectOptional() throws Exception { + server.enqueue(new MockResponse().setBody("foo")); + TestInterface api = new TestInterfaceBuilder().encoder(new QueryObjectEncoder(new Encoder.Default())). + target("http://localhost:" + server.getPort()); + api.queryObjectWithQueryParams(new TestInterface.QueryObjectTestParam("xx", 55)); + RecordedRequest req = server.takeRequest(); + assertThat(req.getMethod()).isEqualToIgnoringCase("GET"); + Map map = Arrays.stream(req.getPath().substring(2).split("&")).map(kv -> kv.split("=")). + collect(Collectors.toMap(kv -> kv[0], kv -> kv[1])); + assertThat(map.get("k1")).isEqualTo("xx"); + assertThat(map.get("k2")).isEqualTo("55"); + assertThat(map.get("k3")).isNull(); + } + + private ResponseMapper upperCaseResponseMapper() { return new ResponseMapper() { @Override @@ -798,6 +827,9 @@ void form( @RequestLine("GET /?trim={trim}") void encodedQueryParam(@Param(value = "trim", encoded = true) String trim); + @RequestLine("GET /?k1={k1}&k2={k2}&k3={k3}") + void queryObjectWithQueryParams(QueryObjectTestParam test); + class DateToMillis implements Param.Expander { @Override @@ -805,6 +837,37 @@ public String expand(Object value) { return String.valueOf(((Date) value).getTime()); } } + + @QueryObject + class QueryObjectTestParam { + private final String k1; + private final int k2; + private String k3; //optional + + public QueryObjectTestParam(String k1, int k2) { + this.k1 = k1; + this.k2 = k2; + } + + @QueryObject.Param("k1") + public String getK1() { + return k1; + } + + @QueryObject.Param("k2") + public int getK2() { + return k2; + } + + @QueryObject.Param("k3") + public String getK3() { + return k3; + } + + public void setK3(String k3) { + this.k3 = k3; + } + } } interface OtherTestInterface {