diff --git a/core/src/main/java/feign/QueryObject.java b/core/src/main/java/feign/QueryObject.java
new file mode 100644
index 0000000000..6bfa38937c
--- /dev/null
+++ b/core/src/main/java/feign/QueryObject.java
@@ -0,0 +1,24 @@
+package feign;
+
+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;
+
+/**
+ * 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(METHOD)
+ @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..fedbe16d54
--- /dev/null
+++ b/core/src/main/java/feign/codec/QueryObjectEncoder.java
@@ -0,0 +1,74 @@
+package feign.codec;
+
+
+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.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;
+
+ 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) {
+ value = String.valueOf(value);
+ }
+ params.put(key, value);
+ template.query(key, keyToTemplate(key));
+ }
+ }
+ template.resolve(params);
+ } 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 {
+ fallbackEncoder.encode(parametersObject, bodyType, template);
+ }
+ }
+
+ private String keyToTemplate(String key) {
+ return "{" + key + "}";
+ }
+
+}
+
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 {