Retour

Mktd#1 Feign Vs Retrofit : 1 Getting Started

Introduction

Early July we organized in Toulouse our first MonkeyTechDays hosted by HarryCow comparing the technologies: Feign vs Retrofit. The goal of a MKTD is to compare and learn new technologies running challenges throughout the day.

We decided to improve our knowledge of REST clients in Java during this first event. We started with Feign which is designed by Netflix and Retrofit written by Square. These two libraries provide an elegant way to speed up the development of REST clients in Java compare to traditional solutions such as JAX-RS clients, Spring Rest Template, etc…

To bootstrap the teams playing around with these technologies, we had made available several REST services around the Monkeys theme. Source code written this day is available at: https://github.com/monkeytechdays

Challenge #0: Forming teams

This non-technical challenge allows us forming balanced teams on each technology while finishing our breakfast: coffee and croissants. The Feign team was lead by Igor and the Retrofit team by Emmanuel.

Challenge #1: Getting started

The goal of this first challenge is to get familiar with the technologies. The principle of Feign and Retrofit is to write an interface describing the REST service and the API will take care of implementing an instance of this interface. At the end of this first challenge, completing the interfaces for the REST services was sufficient to get the unit tests green.

Here are the two interfaces returning JSON format:

public interface MonkeyApi {
   Page<Monkey> getMonkeys(int page);
   Monkey getMonkeyByName(String name);
   Monkey createMonkey(Monkey monkey);
   void deleteMonkey(String id);
}
public interface MonkeyRaceApi {
    List<MonkeyRace> getMonkeyRaces();
}

and the service returning XML data:

public interface MonkeyStatsApi {
    MonkeyStatistics getMonkeyStats();
}

The code is available here: GitHub

To pass this challenge, the following is required:

  • GET request and parse the JSON response
  • GET request with a URL parameter
  • GET request with a path parameter
  • POST request with encoded JSON body
  • DELETE request
  • GET request and parse the XML response

Feign

The Feign documentation is hosted on Github inside the README.md Extensions documentation is also available in the README.md files of these extensions

Even if Feign supports Java 6 by default, we have been using Java 8

Dependencies

To start using Feign inside a project, the following dependencies need to be added to the POM file:

<!-- Feign -->
<dependency>
   <groupId>com.netflix.feign</groupId>
   <artifactId>feign-core</artifactId>
   <version>8.17.0</version>
</dependency>
<!-- Feign: encode/decode JSON with GSON -->
<dependency>
   <groupId>com.netflix.feign</groupId>
   <artifactId>feign-gson</artifactId>
   <version>8.17.0</version>
</dependency>
<!-- Feign: encode/decode XML with JAXB -->
<dependency>
   <groupId>com.netflix.feign</groupId>
   <artifactId>feign-jaxb</artifactId>
   <version>8.17.0</version>
</dependency>
We highly recommend setting a maven property to define the version of Feign to be used

Interfaces Configuration

The next step is to annotate the interfaces so Feign can implement the HTTP requests of each method. Feign comes with its own annotations describing the HTTP requests:

  • @RequestLine: describes the first HTTP line: HTTP verb (GET, POST, PUT, DELETE, …) and path, defining as well the request parameters. Path parameters can be defined using the {name} convention.
  • @Param: binds a variable defined in other annotations (@RequestLine, @Headers, …) and the method parameter. The variable name must be defined by the annotation.
  • @Headers: adds a custom HTTP header, similarly to @RequestLine, the {name} convention can be used to define a variable inside the HTTP header. This annotation is applicable at the interface or method level. If there is no annotation for the body of a POST or PUT request, the parameters without annotation will be converted inside the request body.

So we end up with the following:

public interface MonkeyRaceApi {
    @RequestLine("GET /races")
    List<MonkeyRace> getMonkeyRaces();
}
@Headers("Content-Type: application/json")
public interface MonkeyApi {
    @RequestLine("GET ?page={page}")
    Page<Monkey> getMonkeys(@Param("page") int page);

    @RequestLine("GET /{name}")
    Monkey getMonkeyByName(@Param("name") String name);

    @RequestLine("POST ")
    Monkey createMonkey(Monkey monkey);

    @RequestLine("DELETE /{id}")
    void deleteMonkey(@Param("id") String id);
}
public interface MonkeyStatsApi {
    @RequestLine("GET /stats")
    MonkeyStatistics getMonkeyStats();
}

Instances construction

For the last step, we leverage the API fluent builder provided by Feign to instanciate the interfaces. This is where the encoders/decoders dependencies added by maven earlier on will kick-in:

static MonkeyRaceApi buildRaceApi(String url) {
    return Feign.builder()
            // Decode JSON from response body
            .decoder(new GsonDecoder())
            .target(MonkeyRaceApi.class, url);
}
static MonkeyApi buildMonkeyApi(String url) {
    return Feign.builder()
            // Decode JSON from response body
            .decoder(new GsonDecoder())
            // Encode JSON for request body
            .encoder(new GsonEncoder())
            .target(MonkeyApi.class, url + "/monkeys");
}
static MonkeyStatsApi buildStatsApi(String url) {
    // Create JAXB context factory
    JAXBContextFactory jaxbFactory = new JAXBContextFactory.Builder()
            .withMarshallerJAXBEncoding("UTF-8")
            .build();

    return Feign.builder()
            // Decode XML from response body
            .decoder(new JAXBDecoder(jaxbFactory))
            .target(MonkeyStatsApi.class, url);
}
Feign concatenates the URL with the path defined by the @RequestLine annotation. This provides an easy way to add a prefix to our services if required.

Conclusion

Very few drawbacks using Feign during this experiment:

  • To get the XML decoding to work, we had to tweak the object so JAXB can correctly deserialise the XML response. But this is a more general issue with JAXB and XML parsing.
  • Error messages are not always easy to decrypt, but with a bit more experience and basic knowledge of the HTTP protocol, this is not really an issue. A typical mistake is to forget the HTTP verb inside the @RequestLine annotation.

A lot of benefits:

  • Ease of use and close to HTTP protocol
  • Very lightweight without any transitive dependencies for feign-core
  • Easy to extend, eg: swapping encoders/decoders GSON, Jackson, JAXB
  • Good support of Java 8, the instances methods static and default are supported

General comments

  • There are a few more annotations @Body, @HeaderMap, @QueryMap
  • it is possible to configure how variables (@Param) are converted into String via Expanders
  • The root path of all the interface methods can be added to the URL used by the builder
There is no dark magic inside Feign: it relies on the JDK: java.net.HttpURLConnection, java.lang.reflect.Proxy, java.lang.reflect.InvocationHandler, …

Retrofit

The first step is to add the Retrofit dependencies:

<!-- Retrofit dependency -->
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>retrofit</artifactId>
    <version>${retrofit.version}</version>
</dependency>
<!-- Jackson converter for JSON -->
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-jackson</artifactId>
    <version>${retrofit.version}</version>
</dependency>
<!-- Simple converter for XML -->
<dependency>
    <groupId>com.squareup.retrofit2</groupId>
    <artifactId>converter-simplexml</artifactId>
    <version>${retrofit.version}</version>
</dependency>

Next step is to add the Retrofit specific annotations to the interface The rules of the game being to not modify the interface signature, we had to add a new interface to CallFactory used by Retrofit by default.

public interface MonkeyRaceService {
   @GET("races")
    Call<List<MonkeyRace>> getMonkeyRaces();
}
public interface MonkeyService {
    @GET("monkeys")
    Call<Page<Monkey>> getMonkeys();

    @GET("monkeys/{name}")
    Call<Monkey> getMonkeyByName(@Path("name") String name);

    @POST("monkeys")
    Call<Monkey> create(@Body Monkey monkey);

    @DELETE("monkeys/{id}")
    Call<ResponseBody> delete(@Path("id") String monkeyId);
}
public interface MonkeyStatsService {
    @GET("/stats")
    Call<MonkeyStatistics> getMonkeyStats();
}

Then we implement the interfaces MonkeyApi, MonkeyRaceApi, MonkeyStatsApi using the Retrofit specific interfaces

public class RetrofitMonkeyApi implements MonkeyApi, RetrofitApi {
    private MonkeyService monkeyService;

    public void setBaseUrl(String baseUrl) {
        monkeyService = createRetrofit(baseUrl, false)
          .create(MonkeyService.class);
    }

    @Override
    public Page<Monkey> getMonkeys(int page) {
        return executeCall(monkeyService::getMonkeys);
    }

    @Override
    public Monkey getMonkeyByName(String name) {
        return executeCall(() -> monkeyService.getMonkeyByName(name));
    }

    @Override
    public Monkey createMonkey(Monkey monkey) {
        return executeCall(() -> monkeyService.create(monkey));
    }

    @Override
    public void deleteMonkey(String id) {
        executeCall(() -> monkeyService.delete(id));
    }
}
public interface RetrofitApi {

    default Retrofit createRetrofit(String baseUrl, boolean useXml) {
        Retrofit.Builder builder = new Retrofit.Builder()
                .baseUrl(baseUrl);
        if (useXml) {
            builder.addConverterFactory(SimpleXmlConverterFactory.create());
        }
        builder.addConverterFactory(JacksonConverterFactory.create());
        return builder.build();
    }

    default <T> T executeCall(Supplier<Call<T>> supplier) {
        try {
            Call<T> call = supplier.get();
            return call.execute().body();
        } catch (IOException e) {
            return null;
        }
    }
}

Conclusion

Drawbacks

We found quite annoying that there is no way with CallFactory to issue a synchronous call returning an object without having to use the Call object - as this can be done with Feign

It is also possible to create our own CallAdapterFactory. Here is an example from Retrofit test source code:

static class DirectCallIOException extends RuntimeException {
    DirectCallIOException(String message, IOException e) {
      super(message, e);
    }
}

static class DirectCallAdapterFactory extends CallAdapter.Factory {
    @Override
    public CallAdapter<?> get(final Type returnType, Annotation[] annotations, Retrofit retrofit) {
        return new CallAdapter<Object>() {
            @Override public Type responseType() {
                return returnType;
            }

            @Override public Object adapt(Call call) {
                try {
                    return call.execute().body();
                } catch (IOException e) {
                    throw new DirectCallIOException(e.getMessage(), e);
                }
            }
        };
    }
}

This enforces the handling of exceptions of type DirectCallIOException.

Another pain point encountered with Retrofit is to have to explicitly catch the IOException that can raise from method calls. Maybe Retrofit could provide exception management in a Feign fashion? We will find this out during the next part.

The last drawback we found, Retrofit requires several dependencies to run: OkHttp and at least 1 converter which makes the size of the executable much larger that the one generated with Feign (1.5Mo vs 0.5Mo).

Benefits

Retrofit is developer friendly. Having the main converters available out of the box is very handy.

Retrofit has its own annotations avoiding typical typo, which is a good thing. Even if Feign has improved a lot on error messages management, we still prefer the way it has been designed by the Retrofit team.