8

Spring cloud openFeign can't create dynamic query parameters. It throws below exception because SpringMvcContract tries to find the RequestParam value attribute which doesn't exist.

java.lang.IllegalStateException: RequestParam.value() was empty on parameter 0


@RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(@RequestParam CustomObject customObject);

I tried using @QueryMap instead of @RequestParam but @QueryMap does not generate query parameters.

Btw @RequestParam Map<String, Object> params method parameter works fine to generate a dynamic query parameter.

But I want to use a custom object in which the feign client can generate dynamic query parameters from the object's attributes.

rohanagarwal
  • 771
  • 9
  • 30
mstzn
  • 2,881
  • 3
  • 25
  • 37
  • 1
    AFAIK, Spring doesn't need RequestParam annotation while binding query parameters to a custom DTO. ([Ref](https://stackoverflow.com/a/16942352/381897)). Have you tried without RequestParam annotation? Does this contract work fine on server side? If this interface created just for client generation in Feign side, did you try any other format? And What do you mean by dynamic query parameters? AFAIK, http specification doesn't have such thing as dynamic query parameter. – bhdrkn Nov 30 '18 at 17:29
  • Thanks for helping @bhdrkn . Please see my answer. – mstzn Dec 03 '18 at 06:32

2 Answers2

12

From Spring Cloud OpenFeign Docs:

Spring Cloud OpenFeign provides an equivalent @SpringQueryMap annotation, which is used to annotate a POJO or Map parameter as a query parameter map

So your code should be:

@RequestMapping(method = RequestMethod.GET, value = "/orders")
Pageable<Order> searchOrder2(@SpringQueryMap @ModelAttribute CustomObject customObject);
StasKolodyuk
  • 4,256
  • 2
  • 32
  • 41
5

spring-cloud-starter-feign has a open issue for supporting pojo object as request parameter. Therefore I used a request interceptor that take object from feign method and create query part of url from its fields. Thanks to @charlesvhe

public class DynamicQueryRequestInterceptor implements RequestInterceptor {

private static final Logger LOGGER = LoggerFactory.getLogger(DynamicQueryRequestInterceptor.class);

private static final String EMPTY = "";

@Autowired
private ObjectMapper objectMapper;

@Override
public void apply(RequestTemplate template) {
    if ("GET".equals(template.method()) && Objects.nonNull(template.body())) {
        try {
            JsonNode jsonNode = objectMapper.readTree(template.body());
            template.body(null);

            Map<String, Collection<String>> queries = new HashMap<>();
            buildQuery(jsonNode, EMPTY, queries);
            template.queries(queries);
        } catch (IOException e) {
            LOGGER.error("IOException occurred while try to create http query");
        }
    }
}

private void buildQuery(JsonNode jsonNode, String path, Map<String, Collection<String>> queries) {
    if (!jsonNode.isContainerNode()) {
        if (jsonNode.isNull()) {
            return;
        }
        Collection<String> values = queries.computeIfAbsent(path, k -> new ArrayList<>());
        values.add(jsonNode.asText());
        return;
    }
    if (jsonNode.isArray()) {
        Iterator<JsonNode> it = jsonNode.elements();
        while (it.hasNext()) {
            buildQuery(it.next(), path, queries);
        }
    } else {
        Iterator<Map.Entry<String, JsonNode>> it = jsonNode.fields();
        while (it.hasNext()) {
            Map.Entry<String, JsonNode> entry = it.next();
            if (StringUtils.hasText(path)) {
                buildQuery(entry.getValue(), path + "." + entry.getKey(), queries);
            } else {
                buildQuery(entry.getValue(), entry.getKey(), queries);
            }
        }
    }
}

}

mstzn
  • 2,881
  • 3
  • 25
  • 37