spring-cloud-openfeign icon indicating copy to clipboard operation
spring-cloud-openfeign copied to clipboard

@SpringQueryMap nested object support

Open zxy-c opened this issue 4 years ago • 6 comments

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 avatar Dec 11 '20 07:12 zxy-c

@zxy-c You can define a custom feign.QueryMapEncoder and wire it through the FeignClient's configuration.

o2-mesmer avatar Feb 11 '21 13:02 o2-mesmer

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 avatar Oct 09 '21 11:10 duzhongyuan

@duzhongyuan Would you like to provide a PR?

OlgaMaciaszek avatar Feb 14 '22 18:02 OlgaMaciaszek

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;
    }

yulgutlin avatar May 31 '22 16:05 yulgutlin

Is this fixed in any followup releases?

Wuaner avatar Jan 12 '24 07:01 Wuaner

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.

OlgaMaciaszek avatar Jan 12 '24 09:01 OlgaMaciaszek