Skip to content

Commit 73d91bc

Browse files
王灿velo
authored andcommitted
add BeanQueryMapEncoder (#802)
* changed default query encoder result from POJO field to getter property * changed default query encoder result from POJO field to getter property * reset mistakenly deleted file * Create PropertyQueryMapEncoder and extract QueryMapEncoder.Default to FieldQueryMapEncoder * rename PropertyQueryMapEncoder to BeanQueryMapEncoder and add README * fix README * add comments to QueryMapEncoder and remove deprecation on Default * rename test name * rename package name queryMap to querymap * format code
1 parent 907b80c commit 73d91bc

7 files changed

Lines changed: 427 additions & 74 deletions

File tree

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,18 @@ public class Example {
690690
}
691691
```
692692

693+
When annotating objects with @QueryMap, the default encoder uses reflection to inspect provided objects Fields to expand the objects values into a query string. If you prefer that the query string be built using getter and setter methods, as defined in the Java Beans API, please use the BeanQueryMapEncoder
694+
695+
```java
696+
public class Example {
697+
public static void main(String[] args) {
698+
MyApi myApi = Feign.builder()
699+
.queryMapEncoder(new BeanQueryMapEncoder())
700+
.target(MyApi.class, "https://api.hostname.com");
701+
}
702+
}
703+
```
704+
693705
### Error Handling
694706
If you need more control over handling unexpected responses, Feign instances can
695707
register a custom `ErrorDecoder` via the builder.

core/src/main/java/feign/QueryMapEncoder.java

Lines changed: 15 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,16 @@
1313
*/
1414
package feign;
1515

16-
import feign.codec.EncodeException;
17-
import java.lang.reflect.Field;
18-
import java.util.ArrayList;
19-
import java.util.Collections;
20-
import java.util.HashMap;
21-
import java.util.List;
16+
import feign.querymap.BeanQueryMapEncoder;
17+
import feign.querymap.FieldQueryMapEncoder;
2218
import java.util.Map;
2319

24-
/** A QueryMapEncoder encodes Objects into maps of query parameter names to values. */
20+
/**
21+
* A QueryMapEncoder encodes Objects into maps of query parameter names to values.
22+
*
23+
* @see FieldQueryMapEncoder
24+
* @see BeanQueryMapEncoder
25+
*/
2526
public interface QueryMapEncoder {
2627

2728
/**
@@ -32,55 +33,11 @@ public interface QueryMapEncoder {
3233
*/
3334
Map<String, Object> encode(Object object);
3435

35-
class Default implements QueryMapEncoder {
36-
37-
private final Map<Class<?>, ObjectParamMetadata> classToMetadata =
38-
new HashMap<Class<?>, ObjectParamMetadata>();
39-
40-
@Override
41-
public Map<String, Object> encode(Object object) throws EncodeException {
42-
try {
43-
ObjectParamMetadata metadata = getMetadata(object.getClass());
44-
Map<String, Object> fieldNameToValue = new HashMap<String, Object>();
45-
for (Field field : metadata.objectFields) {
46-
Object value = field.get(object);
47-
if (value != null && value != object) {
48-
fieldNameToValue.put(field.getName(), value);
49-
}
50-
}
51-
return fieldNameToValue;
52-
} catch (IllegalAccessException e) {
53-
throw new EncodeException("Failure encoding object into query map", e);
54-
}
55-
}
56-
57-
private ObjectParamMetadata getMetadata(Class<?> objectType) {
58-
ObjectParamMetadata metadata = classToMetadata.get(objectType);
59-
if (metadata == null) {
60-
metadata = ObjectParamMetadata.parseObjectType(objectType);
61-
classToMetadata.put(objectType, metadata);
62-
}
63-
return metadata;
64-
}
65-
66-
private static class ObjectParamMetadata {
67-
68-
private final List<Field> objectFields;
69-
70-
private ObjectParamMetadata(List<Field> objectFields) {
71-
this.objectFields = Collections.unmodifiableList(objectFields);
72-
}
73-
74-
private static ObjectParamMetadata parseObjectType(Class<?> type) {
75-
List<Field> fields = new ArrayList<Field>();
76-
for (Field field : type.getDeclaredFields()) {
77-
if (!field.isAccessible()) {
78-
field.setAccessible(true);
79-
}
80-
fields.add(field);
81-
}
82-
return new ObjectParamMetadata(fields);
83-
}
84-
}
85-
}
36+
/**
37+
* @deprecated use {@link BeanQueryMapEncoder} instead. default encoder uses reflection to inspect
38+
* provided objects Fields to expand the objects values into a query string. If you prefer
39+
* that the query string be built using getter and setter methods, as defined in the Java
40+
* Beans API, please use the {@link BeanQueryMapEncoder}
41+
*/
42+
class Default extends FieldQueryMapEncoder {}
8643
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* <p>http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package feign.querymap;
15+
16+
import feign.QueryMapEncoder;
17+
import feign.codec.EncodeException;
18+
import java.beans.IntrospectionException;
19+
import java.beans.Introspector;
20+
import java.beans.PropertyDescriptor;
21+
import java.lang.reflect.InvocationTargetException;
22+
import java.util.*;
23+
24+
/**
25+
* the query map will be generated using java beans accessible getter property as query parameter
26+
* names.
27+
*
28+
* <p>eg: "/uri?name={name}&number={number}"
29+
*
30+
* <p>order of included query parameters not guaranteed, and as usual, if any value is null, it will
31+
* be left out
32+
*/
33+
public class BeanQueryMapEncoder implements QueryMapEncoder {
34+
private final Map<Class<?>, ObjectParamMetadata> classToMetadata =
35+
new HashMap<Class<?>, ObjectParamMetadata>();
36+
37+
@Override
38+
public Map<String, Object> encode(Object object) throws EncodeException {
39+
try {
40+
ObjectParamMetadata metadata = getMetadata(object.getClass());
41+
Map<String, Object> propertyNameToValue = new HashMap<String, Object>();
42+
for (PropertyDescriptor pd : metadata.objectProperties) {
43+
Object value = pd.getReadMethod().invoke(object);
44+
if (value != null && value != object) {
45+
propertyNameToValue.put(pd.getName(), value);
46+
}
47+
}
48+
return propertyNameToValue;
49+
} catch (IllegalAccessException | IntrospectionException | InvocationTargetException e) {
50+
throw new EncodeException("Failure encoding object into query map", e);
51+
}
52+
}
53+
54+
private ObjectParamMetadata getMetadata(Class<?> objectType) throws IntrospectionException {
55+
ObjectParamMetadata metadata = classToMetadata.get(objectType);
56+
if (metadata == null) {
57+
metadata = ObjectParamMetadata.parseObjectType(objectType);
58+
classToMetadata.put(objectType, metadata);
59+
}
60+
return metadata;
61+
}
62+
63+
private static class ObjectParamMetadata {
64+
65+
private final List<PropertyDescriptor> objectProperties;
66+
67+
private ObjectParamMetadata(List<PropertyDescriptor> objectProperties) {
68+
this.objectProperties = Collections.unmodifiableList(objectProperties);
69+
}
70+
71+
private static ObjectParamMetadata parseObjectType(Class<?> type)
72+
throws IntrospectionException {
73+
List<PropertyDescriptor> properties = new ArrayList<PropertyDescriptor>();
74+
75+
for (PropertyDescriptor pd : Introspector.getBeanInfo(type).getPropertyDescriptors()) {
76+
boolean isGetterMethod = pd.getReadMethod() != null && !"class".equals(pd.getName());
77+
if (isGetterMethod) {
78+
properties.add(pd);
79+
}
80+
}
81+
82+
return new ObjectParamMetadata(properties);
83+
}
84+
}
85+
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* Copyright 2012-2018 The Feign Authors
3+
*
4+
* <p>Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file
5+
* except in compliance with the License. You may obtain a copy of the License at
6+
*
7+
* <p>http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* <p>Unless required by applicable law or agreed to in writing, software distributed under the
10+
* License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
11+
* express or implied. See the License for the specific language governing permissions and
12+
* limitations under the License.
13+
*/
14+
package feign.querymap;
15+
16+
import feign.QueryMapEncoder;
17+
import feign.codec.EncodeException;
18+
import java.lang.reflect.Field;
19+
import java.util.*;
20+
21+
/**
22+
* the query map will be generated using member variable names as query parameter names.
23+
*
24+
* <p>eg: "/uri?name={name}&number={number}"
25+
*
26+
* <p>order of included query parameters not guaranteed, and as usual, if any value is null, it will
27+
* be left out
28+
*/
29+
public class FieldQueryMapEncoder implements QueryMapEncoder {
30+
31+
private final Map<Class<?>, ObjectParamMetadata> classToMetadata =
32+
new HashMap<Class<?>, ObjectParamMetadata>();
33+
34+
@Override
35+
public Map<String, Object> encode(Object object) throws EncodeException {
36+
try {
37+
ObjectParamMetadata metadata = getMetadata(object.getClass());
38+
Map<String, Object> fieldNameToValue = new HashMap<String, Object>();
39+
for (Field field : metadata.objectFields) {
40+
Object value = field.get(object);
41+
if (value != null && value != object) {
42+
fieldNameToValue.put(field.getName(), value);
43+
}
44+
}
45+
return fieldNameToValue;
46+
} catch (IllegalAccessException e) {
47+
throw new EncodeException("Failure encoding object into query map", e);
48+
}
49+
}
50+
51+
private ObjectParamMetadata getMetadata(Class<?> objectType) {
52+
ObjectParamMetadata metadata = classToMetadata.get(objectType);
53+
if (metadata == null) {
54+
metadata = ObjectParamMetadata.parseObjectType(objectType);
55+
classToMetadata.put(objectType, metadata);
56+
}
57+
return metadata;
58+
}
59+
60+
private static class ObjectParamMetadata {
61+
62+
private final List<Field> objectFields;
63+
64+
private ObjectParamMetadata(List<Field> objectFields) {
65+
this.objectFields = Collections.unmodifiableList(objectFields);
66+
}
67+
68+
private static ObjectParamMetadata parseObjectType(Class<?> type) {
69+
List<Field> fields = new ArrayList<Field>();
70+
for (Field field : type.getDeclaredFields()) {
71+
if (!field.isAccessible()) {
72+
field.setAccessible(true);
73+
}
74+
fields.add(field);
75+
}
76+
return new ObjectParamMetadata(fields);
77+
}
78+
}
79+
}

core/src/test/java/feign/FeignTest.java

Lines changed: 58 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -25,25 +25,12 @@
2525
import feign.Feign.ResponseMappingDecoder;
2626
import feign.Request.HttpMethod;
2727
import feign.Target.HardCodedTarget;
28-
import feign.codec.DecodeException;
29-
import feign.codec.Decoder;
30-
import feign.codec.EncodeException;
31-
import feign.codec.Encoder;
32-
import feign.codec.ErrorDecoder;
33-
import feign.codec.StringDecoder;
28+
import feign.codec.*;
29+
import feign.querymap.BeanQueryMapEncoder;
3430
import java.io.IOException;
3531
import java.lang.reflect.Type;
3632
import java.net.URI;
37-
import java.util.ArrayList;
38-
import java.util.Arrays;
39-
import java.util.Collection;
40-
import java.util.Collections;
41-
import java.util.Date;
42-
import java.util.HashMap;
43-
import java.util.LinkedHashMap;
44-
import java.util.List;
45-
import java.util.Map;
46-
import java.util.NoSuchElementException;
33+
import java.util.*;
4734
import java.util.concurrent.atomic.AtomicReference;
4835
import okhttp3.mockwebserver.MockResponse;
4936
import okhttp3.mockwebserver.MockWebServer;
@@ -788,6 +775,53 @@ public void mapAndDecodeExecutesMapFunction() {
788775
assertEquals(api.post(), "RESPONSE!");
789776
}
790777

778+
@Test
779+
public void beanQueryMapEncoderWithPrivateGetterIgnored() throws Exception {
780+
TestInterface api =
781+
new TestInterfaceBuilder()
782+
.queryMapEndcoder(new BeanQueryMapEncoder())
783+
.target("http://localhost:" + server.getPort());
784+
785+
PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass();
786+
propertyPojo.setPrivateGetterProperty("privateGetterProperty");
787+
propertyPojo.setName("Name");
788+
propertyPojo.setNumber(1);
789+
790+
server.enqueue(new MockResponse());
791+
api.queryMapPropertyPojo(propertyPojo);
792+
assertThat(server.takeRequest()).hasQueryParams(Arrays.asList("name=Name", "number=1"));
793+
}
794+
795+
@Test
796+
public void beanQueryMapEncoderWithNullValueIgnored() throws Exception {
797+
TestInterface api =
798+
new TestInterfaceBuilder()
799+
.queryMapEndcoder(new BeanQueryMapEncoder())
800+
.target("http://localhost:" + server.getPort());
801+
802+
PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass();
803+
propertyPojo.setName(null);
804+
propertyPojo.setNumber(1);
805+
806+
server.enqueue(new MockResponse());
807+
api.queryMapPropertyPojo(propertyPojo);
808+
assertThat(server.takeRequest()).hasQueryParams("number=1");
809+
}
810+
811+
@Test
812+
public void beanQueryMapEncoderWithEmptyParams() throws Exception {
813+
TestInterface api =
814+
new TestInterfaceBuilder()
815+
.queryMapEndcoder(new BeanQueryMapEncoder())
816+
.target("http://localhost:" + server.getPort());
817+
818+
PropertyPojo.ChildPojoClass propertyPojo = new PropertyPojo.ChildPojoClass();
819+
820+
server.enqueue(new MockResponse());
821+
api.queryMapPropertyPojo(propertyPojo);
822+
assertThat(server.takeRequest()).hasQueryParams("/");
823+
}
824+
791825
interface TestInterface {
792826

793827
@RequestLine("POST /")
@@ -863,6 +897,9 @@ void queryMapWithQueryParams(
863897
@RequestLine("GET /")
864898
void queryMapPojo(@QueryMap CustomPojo object);
865899

900+
@RequestLine("GET /")
901+
void queryMapPropertyPojo(@QueryMap PropertyPojo object);
902+
866903
class DateToMillis implements Param.Expander {
867904

868905
@Override
@@ -964,6 +1001,11 @@ TestInterfaceBuilder decode404() {
9641001
return this;
9651002
}
9661003

1004+
TestInterfaceBuilder queryMapEndcoder(QueryMapEncoder queryMapEncoder) {
1005+
delegate.queryMapEncoder(queryMapEncoder);
1006+
return this;
1007+
}
1008+
9671009
TestInterface target(String url) {
9681010
return delegate.target(TestInterface.class, url);
9691011
}

0 commit comments

Comments
 (0)