spring-cloud-openfeign
spring-cloud-openfeign copied to clipboard
@SpringQueryMap nested object support
FeignClient
interface MyFeignClient{
@GetMapping(params = {"projection=wishSummary"})
Object get(
@SpringQueryMap MyObject myObject);
}
MyObject
class MyObject{
Range range;
}
class Range{
Integer from;
Integer to;
}
Feign will build query param like "Range(from=1,to=2)" when I call MyFeignClient.get
I need a query param like "range.from=1&range.to=2" at the example
@zxy-c You can define a custom feign.QueryMapEncoder
and wire it through the FeignClient's configuration
.
For nested object, Collection nested objects. Rewrite FieldQueryMapEncoder .java
public class CustomNestedObjectQueryMapEncoder implements QueryMapEncoder {
private final Map<Class<?>, CustomNestedObjectQueryMapEncoder.ObjectParamMetadata> classToMetadata =
new HashMap<Class<?>, CustomNestedObjectQueryMapEncoder.ObjectParamMetadata>();
@Override
public Map<String, Object> encode(Object object) throws EncodeException {
return encodeInternal(null, object, null);
}
private Map<String, Object> encodeInternal(String prefixName, Object object, Map<String, Object> fieldNameToValue) {
if (null == fieldNameToValue) {
fieldNameToValue = Maps.newHashMap();
}
try {
CustomNestedObjectQueryMapEncoder.ObjectParamMetadata metadata = getMetadata(object.getClass());
for (Field field : metadata.objectFields) {
Object value = field.get(object);
if (value != null && value != object) {
Param alias = field.getAnnotation(Param.class);
String name = alias != null ? alias.value() : field.getName();
if (StringUtils.isNotBlank(prefixName)) {
name = prefixName + "." + name;
}
ClassLoader classLoader = value.getClass().getClassLoader();
if (classLoader == null) {
processNameAndValue(name, value, fieldNameToValue);
} else {
// Recursive call
encodeInternal(name, value, fieldNameToValue);
}
}
}
return fieldNameToValue;
} catch (IllegalAccessException e) {
throw new EncodeException("Failure encoding object into query map", e);
}
}
private void processNameAndValue(String name, Object value, Map<String, Object> fieldNameToValue) throws IllegalAccessException {
// Determines whether it is a custom object collection
if (isCustomObjectCollection(value)) {
Collection collection = (Collection) value;
for (int i = 0; i < collection.size(); i++) {
Object element = ((ArrayList) collection).get(i);
ObjectParamMetadata metadata = getMetadata(element.getClass());
for (Field field : metadata.objectFields) {
Object elementValue = field.get(element);
if (elementValue != null && elementValue != element) {
Param alias1 = field.getAnnotation(Param.class);
String elementName = alias1 != null ? alias1.value() : field.getName();
elementName = name + "[" + i + "]." + elementName;
ClassLoader classLoader1 = elementValue.getClass().getClassLoader();
if (classLoader1 == null) {
if (isCustomObjectCollection(elementValue)) {
// Recursive call
processNameAndValue(elementName, elementValue, fieldNameToValue);
} else {
fieldNameToValue.put(elementName, elementValue);
}
}
}
}
}
} else {
fieldNameToValue.put(name, value);
}
}
private boolean isCustomObjectCollection(Object value) {
return value instanceof Collection
&& !((Collection) value).isEmpty()
&& ((Collection) value).iterator().next().getClass().getClassLoader() != null;
}
private CustomNestedObjectQueryMapEncoder.ObjectParamMetadata getMetadata(Class<?> objectType) {
CustomNestedObjectQueryMapEncoder.ObjectParamMetadata metadata = classToMetadata.get(objectType);
if (metadata == null) {
metadata = CustomNestedObjectQueryMapEncoder.ObjectParamMetadata.parseObjectType(objectType);
classToMetadata.put(objectType, metadata);
}
return metadata;
}
private static class ObjectParamMetadata {
private final List<Field> objectFields;
private ObjectParamMetadata(List<Field> objectFields) {
this.objectFields = Collections.unmodifiableList(objectFields);
}
private static CustomNestedObjectQueryMapEncoder.ObjectParamMetadata parseObjectType(Class<?> type) {
List<Field> allFields = new ArrayList();
for (Class currentClass = type; currentClass != null; currentClass =
currentClass.getSuperclass()) {
Collections.addAll(allFields, currentClass.getDeclaredFields());
}
return new CustomNestedObjectQueryMapEncoder.ObjectParamMetadata(allFields.stream()
.filter(field -> !field.isSynthetic())
.peek(field -> field.setAccessible(true))
.collect(Collectors.toList()));
}
}
}
`
### NestedObjectFeignConfig
`public class NestedObjectFeignConfig {
@Bean
@Scope("prototype")
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
HystrixFeign.Builder encoder = HystrixFeign.builder();
encoder.queryMapEncoder(new CustomNestedObjectQueryMapEncoder());
return encoder;
}
}
public interface TestClient {
@GetMapping("/test-center/tapTest/getTapRecordReqDTO")
CommonResponse<QueryJourneyResponseDTO> queryJourney(@SpringQueryMap HelloDTO HelloDTO);
}
public class HelloDTO {
private String name;
private WorldDTO worldDTO;
private List<String> list;
private List<WorldDTO> worldDTOList;
}`
public class WorldDTO {
private String world;
private String live;
private List<TestDTO> testDTOList;
}
public class TestDTO {
private String testName;
}
@duzhongyuan Would you like to provide a PR?
Needed this in my project with spring data pageable support and i slightly changed @duzhongyuan 's and @OlgaMaciaszek 's solution: fixed other collection types support, fixed enums and enum collections support. In case if someone will need that:
import feign.Param;
import feign.codec.EncodeException;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder;
import java.lang.reflect.Field;
import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static java.util.function.Predicate.not;
/**
* CustomNestedObjectQueryMapEncoder
*/
public class CustomNestedObjectQueryMapEncoder extends PageableSpringQueryMapEncoder {
private final Map<Class<?>, CustomNestedObjectQueryMapEncoder.ObjectParamMetadata> classToMetadata = new HashMap<>();
@Override
public Map<String, Object> encode(Object object) throws EncodeException {
if (super.supports(object)) {
return super.encode(object);
}
return encodeInternal(null, object, null);
}
private Map<String, Object> encodeInternal(String prefixName, Object object, Map<String, Object> fieldNameToValue) {
if (null == fieldNameToValue) {
fieldNameToValue = new HashMap<>();
}
try {
CustomNestedObjectQueryMapEncoder.ObjectParamMetadata metadata = getMetadata(object.getClass());
for (Field field : metadata.objectFields) {
Object value = field.get(object);
if (value != null && value != object) {
Param alias = field.getAnnotation(Param.class);
String name = alias != null ? alias.value() : field.getName();
if (StringUtils.isNotBlank(prefixName)) {
name = prefixName + "." + name;
}
Class<?> aClass = value.getClass();
ClassLoader classLoader = aClass.getClassLoader();
if (classLoader == null || aClass.isEnum()) {
processNameAndValue(name, value, fieldNameToValue);
} else {
// Recursive call
encodeInternal(name, value, fieldNameToValue);
}
}
}
return fieldNameToValue;
} catch (IllegalAccessException e) {
throw new EncodeException("Failure encoding object into query map", e);
}
}
private void processNameAndValue(String name, Object value, Map<String, Object> fieldNameToValue) throws IllegalAccessException {
// Determines whether it is a custom object collection
if (isCustomObjectCollection(value)) {
Collection<?> collection = (Collection<?>) value;
int i = 0;
for (Object element : collection) {
ObjectParamMetadata metadata = getMetadata(element.getClass());
for (Field field : metadata.objectFields) {
Object elementValue = field.get(element);
if (elementValue != null && elementValue != element) {
Param alias1 = field.getAnnotation(Param.class);
String elementName = alias1 != null ? alias1.value() : field.getName();
elementName = name + "[" + i + "]." + elementName;
ClassLoader classLoader1 = elementValue.getClass().getClassLoader();
if (classLoader1 == null) {
if (isCustomObjectCollection(elementValue)) {
// Recursive call
processNameAndValue(elementName, elementValue, fieldNameToValue);
} else {
fieldNameToValue.put(elementName, elementValue);
}
}
}
}
i++;
}
} else {
fieldNameToValue.put(name, value);
}
}
private boolean isCustomObjectCollection(Object value) {
return Optional.ofNullable(value)
.filter(Collection.class::isInstance)
.map(Collection.class::cast)
.filter(not(Collection::isEmpty))
.stream()
.flatMap(collection -> (Stream<?>) collection.stream())
.filter(not(Enum.class::isInstance))
.map(Object::getClass)
.map(Class::getClassLoader)
.anyMatch(Objects::nonNull);
}
private CustomNestedObjectQueryMapEncoder.ObjectParamMetadata getMetadata(Class<?> objectType) {
CustomNestedObjectQueryMapEncoder.ObjectParamMetadata metadata = classToMetadata.get(objectType);
if (metadata == null) {
metadata = CustomNestedObjectQueryMapEncoder.ObjectParamMetadata.parseObjectType(objectType);
classToMetadata.put(objectType, metadata);
}
return metadata;
}
private static class ObjectParamMetadata {
private final List<Field> objectFields;
private ObjectParamMetadata(List<Field> objectFields) {
this.objectFields = Collections.unmodifiableList(objectFields);
}
private static CustomNestedObjectQueryMapEncoder.ObjectParamMetadata parseObjectType(Class<?> type) {
List<Field> allFields = new ArrayList<>();
for (Class<?> currentClass = type; currentClass != null && !currentClass.isEnum(); currentClass =
currentClass.getSuperclass()) {
Collections.addAll(allFields, currentClass.getDeclaredFields());
}
return new CustomNestedObjectQueryMapEncoder.ObjectParamMetadata(allFields.stream()
.filter(field -> !field.isSynthetic())
.peek(field -> field.setAccessible(true))
.collect(Collectors.toList()));
}
}
}
Usage:
@Autowired(required = false)
private SpringDataWebProperties springDataWebProperties;
@Bean
public QueryMapEncoder feignQueryMapEncoderPageable() {
PageableSpringQueryMapEncoder queryMapEncoder = new CustomNestedObjectQueryMapEncoder();
if (springDataWebProperties != null) {
queryMapEncoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
queryMapEncoder.setSizeParameter(springDataWebProperties.getPageable().getSizeParameter());
queryMapEncoder.setSortParameter(springDataWebProperties.getSort().getSortParameter());
}
return queryMapEncoder;
}
Is this fixed in any followup releases?
No, @Wuaner , it's an enhancement that is marked as "help wanted". That means we are not planning to work on it, but we are happy to review community PRs for it if they are submitted.