OpenFeign / feign

Feign makes writing java http clients easier
Apache License 2.0
9.42k stars 1.92k forks source link

Provide a custom parameter processor #751

Closed sparqueur closed 5 years ago

sparqueur commented 6 years ago

In my use case I wish I could use custom parameter processor.

Currently, there is no way to make feign call an external processor which could deaply process a parameter. For instance, add multiple query params according to the parameter or headers etc...

The only thing we have are Expander for request params, but it only let us serialize a parameter to a String.

Please provide us a "@CustomProcessor(MyCustomProcessor.class)" annotation in order to manually do our stuff directly with the restTemplate :

public @interface CustomProcessor {
   Class<? extends Processor> value();

   interface Processor {
      void process(Object value, RequestTemplate template);
   }
}

public MyCustomProcessor implements Processor {
    void process(Object value, RequestTemplate template) {
        // My stuff (eg. add multiple request params + headers ...)
    }
}

Add a map in MethodMetada, you could then call it the ReflectiveFeign.create function. Voila, easy to say, hard to do :-)

My use case :

I'm using spring-cloud-feign and share a contract between the server and client. One spring mvc annotation is @ModelAttribute => Object are serialized in multiple request params representing the attributes.

Eg : UserRequest :

public List<User> searchUser (@ModelAttribute UserRequest criteria) ==> GET /search?name=toto&age=12

With CustomProcessors I could handle this !

Thank's in advance

sparqueur commented 6 years ago

Here is a working implementation (ReflectiveFeign only) based on feign core [9.5.1] feign.zip Hope you will try to integrate this to a futur release

I managed to make @ModelAttribute work with it (someone might need it) : spring-feign-model-attribute.zip

kdavisk6 commented 6 years ago

Support for expanding objects into Query Parameters was added in Feign 9.7. Take a look at the Dynamic Query Parameters documentation.

As for additional ways to process an interface, that can be done by defining your own Contract. As you've already discovered, this is how Spring OpenFeign works.

If you need to process the request in a different way, you can use a RequestInterceptor to refine the request before sending to the client, including adding/removing/changing headers, decorating the request with more information, and so on. You can use an Encoder to control how your request body is represented to the endpoint. You can use a Decoder to control how to process the results. If you need to manage the low level execution of the request, you can build your own Client. All of these capabilities provide a working model for managing each part of the request lifecycle and are exposed as part of Feign's public interface.

This is the preferred way to extend Feign. HACKING describes the reason behind this philosophy.

Getting back to your original question, I recommend using a RequestInterceptor to handle any special pre-processing requirements and a custom Encoder to do more robust Request Body handling.

As for the @ModelAttribute work, that is better suited for the Spring Cloud OpenFeign project.

sparqueur commented 6 years ago

Thanks for your answer. Sorry, I might be wrong but RequestInterceptor is not a solution as you dont have access to the function call parameters. The encoder is neither usable as it is for body parameter only (which is not my case). The feature i ask is just missing. But maybe I am wrong with something ? Thanks in advance Regards

kdavisk6 commented 6 years ago

If you have a particular use case in mind that could help clarify. Can you provide one?

sparqueur commented 6 years ago

I'm working with Jhipster (https://www.jhipster.tech/). There is a nice Filter mechanism to request for resources.

Here is an example.

Class StringFilter {
  String contains;
  String startsWith;
  String equals;
}

Then I have a global filter object :

Class UserRequestFilter {
  StringFilter firstname;
  StringFilter lastname;
  StringFilter city;
}

My java method corresponding to the REST end points is : public List<User> find(@ModelAttribute UserRequestFilter filters);

My "filters" object is decomposed in request params according to attribute names This let me do such kind of request : GET /user?firstname.contains=antoine&city.equals=lyon

I find this very nice.

I would like to be able to implement a feign client to call my endpoint and use this UserRequestFilter and have a generic object to request param encoder.

Currently, I can implement a custom encoder but it only let me transform an input object to ONE string. => I can for instance use a JSON serialization and obtain such kind of request : GET /user?filters={firstname:{contains:antoine},city:{equals:lyon}} but I realy prefer the following request : GET /user?firstname.contains=antoine&city.equals=lyon

Hope this example is enough understandable.

Thank's in advance

kdavisk6 commented 6 years ago

May I suggest that you look at the documentation for Dynamic Query Parameters again. You can provide a QueryMapEncoder that will allow you to process the QueryMap annotated object and generate the specific Query String you need. This is different from the Param.Encoder in that it can process an entire object and does not generate a string, but the Query String Map.

class FilterEncoder implements QueryMapEncoder {
   @Override
   public Map<String, Object> encode(Object object) throws EncodeException {
       // process your object, creating a map where the key is the query parameter name
       // and the value is the query parameter value
   }
}

To achieve what you are looking for, you will need to build this map accordingly. One possible approach would be to inspect the fields on the object, and then delegating to another component whose responsibility is to encode *Filter objects.

class StringFilterEncoder {
   SimpleImmutableEntry<String, String> encode(StringFilter filter) {
      // return a key, value pair from the string filter
   }
}

class FilterQueryMapEncoder implements QueryMapEncoder {
   public Map<String, Object> encode(Object object) {
      Map<String, Object> queryMap = new LinkedHashMap<>();
      Class<?> type = object.getClass();
      for (Field field : type.getDeclaredFields()) {
         // check to see if it is a StringFilter and delegate
         queryMap.putIfAbsent(StringFilterEncoder.encode(...));
      }
      return queryMap;
   }
}

Your Interface then can look like this

@RequestLine("GET /users")
interface Users {
      public List<User> get(@QueryMap UserRequestFilter filter);
}

Users api = Feign.builder()
              .queryMapEncoder(FilterQueryMapEncoder())
              .target(User.class, "http://endpoint");
kdavisk6 commented 5 years ago

I'm going to close this issue and recommend that questions like this be made on Stack Overflow, allowing more users to help you.