0

REST Http Client : Feign vs Retrofit 2

AppManager screenshot
Share on LinkedIn
Pocket

I have been recently writing a new REST/Http client for the amazing APM product ManageEngine AppManager. In this context, I had to choose an efficient to build a new HTTP Client. I decided to let Netflix Feign and Retrofit 2 fight in the arena.

Introduction to APM

APM AppManager is working fine. Nice dashboards, plenty of connectors, sure the competition is high and I am quite in love of Instana. However in the context of , creating Monitors, MonitorGroups by hand is not viable neither effective. Since the team has to deal witha large number of machines, I am studying the possibility to automate this work.

In my context, I have to automate the workflow to create the monitors, groups and dashboard when a new machine has been added in the information system.

AppManager is coming with a neatly perfectible REST API and Documentation compared to some well known standards but it’s doing the job and I will have to stick with it.

Let’s enter directly into the subject. I won’t do a tutorial, I will simply give you my tips and surprised I had, when developing this REST Client in .

The test to compare the solutions

Here is the test I used to compare the two solutions.


@RunWith(JUnitPlatform.class)
public class RestClientTest {

    @Test
    public void testFeign() throws MalformedURLException, KeyManagementException, NoSuchAlgorithmException {

        String host = new (Constants.APPMANAGER_).getHost();
        Client client = new Client.Default(new NaiveSSLSocketFactory(host),
            new NaiveHostnameVerifier(host));
        ;
        ListMonitorGroupsFeign listMonitorGroups = Feign.builder()
            .client(client)
            .logger(new Slf4jLogger())
            .encoder(new JacksonEncoder())
            .decoder(new JacksonDecoder())
            .target(ListMonitorGroupsFeign.class, Constants.APPMANAGER_);

        //
        Answer<List> answer = listMonitorGroups.getAllMonitorGroups(Constants.API_KEY, TreeViewEnum.ALL);
        LOGGER.debug("Results from the Feign REST API -> {}", answer);

        Assertions.assertEquals(4, answer.getResponse().getResult().size(), "Number of instances should be four.");
    }

    @Test
    public void testRetrofit() throws IOException {

        // For HTTPS Unsafe
        OkHttpClient okHttpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient();

        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constants.APPMANAGER_)
            .client(okHttpClient)
            .addConverterFactory(JacksonConverterFactory.create())
            .build();

        ListMonitorGroupsRetrofit service = retrofit.create(ListMonitorGroupsRetrofit.class);
        Call<Answer<List>> restCall = service.getAllMonitorGroups(Constants.API_KEY, TreeViewEnum.ALL);

        // SYNC CALL
        Response<Answer<List>> response = restCall.execute();

        Answer<List> body = response.body();
        LOGGER.debug("Results from the RetroFIT REST API -> {}", body);

        Assertions.assertEquals(4, body.getResponse().getResult().size(), "Number of instances should be four.");
    }

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

Few words about this code. I developed it with 5.0.

Resource Design and API

I wrote an endpoint using both frameworks.

Resource using Feign


public interface ListMonitorGroupsFeign {
 @RequestLine("GET /ListMonitorGroups?apikey={apikey}&type=all&outageReports=false&treeview={treeview}&severityDetails=true")
 Answer<List<MonitorGroup>> getAllMonitorGroups(@Param("apikey") final String apikey, @Param("treeview") TreeViewEnum treeview);

 @RequestLine("GET /ListMonitorGroups?apikey={apikey}&groupId={id}&outageReports=false&treeview={treeview}&severityDetails=true")
 Answer<MonitorGroup> findById(@Param("apikey") final String apikey, @Param("id") String id, @Param("treeview") TreeViewEnum treeview);

 @RequestLine("GET /ListMonitorGroups?apikey={apikey}&groupName={name}&outageReports=false&treeview={treeview}&severityDetails=true")
 Answer<MonitorGroup> findByName(@Param("apikey") final String apikey, @Param("name") String id, @Param("treeview") TreeViewEnum treeview);

 @RequestLine("GET /ListMonitorGroups?apikey={apikey}&groupId={id}&outageReports=true&treeview={treeview}&severityDetails=true")
 Answer<MonitorGroup> statById(@Param("apikey") final String apikey, @Param("id") String id, @Param("treeview") TreeViewEnum treeview);

 @RequestLine("GET /ListMonitorGroups?apikey={apikey}&groupName={name}&outageReports=true&treeview={treeview}&severityDetails=true")
 Answer<MonitorGroup> statByName(@Param("apikey") final String apikey, @Param("name") String id, @Param("treeview") TreeViewEnum treeview);
}

Resource using RetroFit 2


public interface ListMonitorGroupsRetrofit {
 @GET("ListMonitorGroups?type=all&outageReports=false&severityDetails=true")
 Call<Answer<List<MonitorGroup>>> getAllMonitorGroups(@Query("apikey") final String apikey, @Query("treeview") TreeViewEnum treeview);

 @GET("ListMonitorGroups?outageReports=false&severityDetails=true")
 Call<Answer<MonitorGroup>> findById(@Query("apikey") final String apikey, @Query("id") String id, @Query("treeview") TreeViewEnum treeview);

 @GET("ListMonitorGroups?outageReports=false&severityDetails=true")
 Call<Answer<MonitorGroup>> findByName(@Query("apikey") final String apikey, @Query("name") String id, @Query("treeview") TreeViewEnum treeview);

 @GET("ListMonitorGroups?outageReports=true&severityDetails=true")
 Call<Answer<MonitorGroup>> statById(@Query("apikey") final String apikey, @Query("id") String id, @Query("treeview") TreeViewEnum treeview);

 @GET("ListMonitorGroups?outageReports=true&severityDetails=true")
 Call<Answer<MonitorGroup>> statByName(@Query("apikey") final String apikey, @Query("name") String id, @Query("treeview") TreeViewEnum treeview);
}

My Impressions :

+ Both are deadly easy to use

+ Feign is directly returning the payload

Return types are wrapped into a Call in Retrofit 2

String to define the verbs are less maintanable unless you want to write a full command as API Design 🙂

+ Query params are injected with Retrofit, no need to repeat them into the URI

If Feign hs cleaner return types, Retrofit 2 has as a big plus it’s maintenability and some facilities. I don’t have to repeat the Query params in the URL. The annotations are really similar to JaxRS.

For me Retrofit wins this round.

Surprises and usage

Thanks to a very old and nasty SSL Certificate, I had several issues with the frameworks and had to find a solution to bypass the SSL verification

Bypass SSL Trust check in Feign


String host = new URL(Constants.APPMANAGER_URL).getHost();
   Client client = new Client.Default(new NaiveSSLSocketFactory(host),
   new NaiveHostnameVerifier(host));
   ;
   ListMonitorGroupsFeign listMonitorGroups = Feign.builder()
   .client(client)
   .logger(new Slf4jLogger())
   .encoder(new JacksonEncoder())
   .decoder(new JacksonDecoder())
   .target(ListMonitorGroupsFeign.class, Constants.APPMANAGER_URL);
public class NaiveHostnameVerifier implements HostnameVerifier {
private final Set<String> naivelyTrustedHostnames;
private final HostnameVerifier hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier();
 public NaiveHostnameVerifier(final String... naivelyTrustedHostnames) {
  this.naivelyTrustedHostnames = Collections.unmodifiableSet(new HashSet<>(Arrays.asList(naivelyTrustedHostnames)));
 }
 @Override
 public boolean verify(final String hostname, final SSLSession session) {
  return this.naivelyTrustedHostnames.contains(hostname) ||
   this.hostnameVerifier.verify(hostname, session);
 }
}
...

You have to define a new Client and provides a mock SSLSocketFactory and HostName verifier. it ends with quite a lot of  nasty code.

Bypass SSL Trust check in Retrofit 2

The code to ignore SSL Certificate errors is simpler in Retrofit 2.

You need to modifiy the OKHttpClient on which Retrofit2 is relying.


     OkHttpClient okHttpClient = UnsafeOkHttpClient.getUnsafeOkHttpClient();
        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constants.APPMANAGER_URL)
            .client(okHttpClient)

public class UnsafeOkHttpClient {
 public static OkHttpClient getUnsafeOkHttpClient() {
   try {
      // Create a trust manager that does not validate certificate chains
      final TrustManager[] trustAllCerts = new TrustManager[] {
            new UnsafeX509TrustManager()
      };
      
      // Install the all-trusting trust manager
      final SSLContext sslContext = SSLContext.getInstance("SSL");
      sslContext.init(null, trustAllCerts, new java.security.SecureRandom());
      
      // Create an ssl socket factory with our all-trusting manager
      final SSLSocketFactory sslSocketFactory = sslContext.getSocketFactory();
      
      // For Logging
      HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
      interceptor.setLevel(HttpLoggingInterceptor.Level.NONE);
      
      OkHttpClient.Builder builder = new OkHttpClient.Builder();
      builder.addInterceptor(interceptor);
      builder.sslSocketFactory(sslSocketFactory, (X509TrustManager) trustAllCerts[0]);
      builder.hostnameVerifier(new HostnameVerifier() {
         @Override
         public boolean verify(final String hostname, final SSLSession session) {
         return true;
      }
   });

   OkHttpClient okHttpClient = builder.build();
   return okHttpClient;
   } catch (Exception e) {
   throw new RuntimeException(e);
   }
 }
}

 


The problem was easier to fix with Retrofit 2

Retrofit, you have a problem with my URL ?

I had a different behaviour using Retrofit2. The framework complained my URL wa not ending by a trailing slash.

testRetrofit(com.byoskill.restclient.RestClientTest)
java.lang.IllegalArgumentException: baseUrl must end in /: https://example:9443/AppManager/json 	
 at retrofit2.Retrofit$Builder.baseUrl(Retrofit.java:515) 	
 at retrofit2.Retrofit$Builder.baseUrl(Retrofit.java:458) 	
 at com.byoskill.restclient.RestClientTest.testRetrofit(RestClientTest.java:56) 	
 at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) 	
 at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) 	
 at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) 

Retrofit does not know Jackson, OMG ?

Albeit I had follow the Retrofit documentation, I completely missed the fact Jackson was not embedded inside the library and I had to complete my installation with a Retrofit addon library. I obtained therefore this gentle exception.

java.lang.IllegalArgumentException: Unable to create converter for example.model.Answer<java.util.List>
    for method ListMonitorGroupsRetrofit.getAllMonitorGroups
	at retrofit2.ServiceMethod$Builder.methodError(ServiceMethod.java:755)
	at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:741)
	at retrofit2.ServiceMethod$Builder.build(ServiceMethod.java:172)
	at retrofit2.Retrofit.loadServiceMethod(Retrofit.java:170)
	at retrofit2.Retrofit$1.invoke(Retrofit.java:147)
	at com.sun.proxy.$Proxy8.getAllMonitorGroups(Unknown Source)
	at com.byoskill.restclient.RestClientTest.testRetrofit(RestClientTest.java:60)

The funny thing is that the documnentation is not providing the example of code to use it :<:p>

        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constants.APPMANAGER_URL)
            .client(okHttpClient)
            .addConverterFactory(JacksonConverterFactory.create()) //JACKSON
            .build();
Caused by: java.lang.IllegalArgumentException: Could not locate ResponseBody converter for example.Answer<java.util.List>.
  Tried:
   * retrofit2.BuiltInConverters
	at retrofit2.Retrofit.nextResponseBodyConverter(Retrofit.java:351)
	at retrofit2.Retrofit.responseBodyConverter(Retrofit.java:313)
	at retrofit2.ServiceMethod$Builder.createResponseConverter(ServiceMethod.java:739)
	... 28 more


I added the required dependencies :

	
			com.squareup.retrofit2
			converter-jackson
			2.4.0	

Jackson does not have the same behaviour with Retrofit 2

JacksonConfiguration is by default ignoring unmapped fields in Feign when in RetroFit it’s generating an exception.

I want Logs..

In Feign, it’s quite straigthforward :


ListMonitorGroupsFeign listMonitorGroups = Feign.builder()
            .client(client)
            .logger(new Slf4jLogger()) //LOOOOGSSS
            .encoder(new JacksonEncoder())
            .decoder(new JacksonDecoder())
            .target(ListMonitorGroupsFeign.class, Constants.APPMANAGER_URL);

In Retrofit, it’s not so easy :


// You need a custom client
        Retrofit retrofit = new Retrofit.Builder()
            .baseUrl(Constants.APPMANAGER_URL)
            .client(okHttpClient)
            .addConverterFactory(JacksonConverterFactory.create())
            .build();

// This client has to define an HttpInterceptor

HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
interceptor.setLevel(HttpLoggingInterceptor.Level.NONE);

  	
// You set the interceptor

OkHttpClient.Builder builder = new OkHttpClient.Builder();
builder.addInterceptor(interceptor);

I did some very small checks about the performance and the execution time to ensure that I wasn’t doing a big mistake.

The initialization time of the Feign client is higher than with Retofit. Pay attention to cache or store it before

The running execution time was globally similar :

14:41:19.759 INFO  c.b.r.RestClientBenchTest - Execution time for feign : 43.30 s
14:42:06.903 INFO  c.b.r.RestClientBenchTest - Execution time for retroFit : 46.96 s

14:52:34.296 INFO  c.b.r.RestClientBenchTest - Execution time for feign : 46.73 s
14:53:19.242 INFO  c.b.r.RestClientBenchTest - Execution time for retroFit : 44.73 

And the memory, slightly lower for Retrofit but it’s marginal. (First spikes are for Feign, second spikes for Retrofit). It does not have a real value, it’s not a microbenchmark. I wanted to detect poteanormal behaviours in my use.

 

 

 

Feign / Retrofit 2 Memory usage

Feign / Retrofit 2 Memory usage

Call meh effect

As we have seen during the resource client, Retrofit is using a Call facade. This face is allowing the use to program easy in a or synchronous way.

It implies an additional effort fo the synchronous coder. In Feign, he could obtain directly the payload and here he has a double indirection to obtain its body.


// With Feign
Answer<List> answer = listMonitorGroups.getAllMonitorGroups(Constants.API_KEY, TreeViewEnum.ALL);

// With Retrofit
Call<Answer<List>> restCall = service.getAllMonitorGroups(Constants.API_KEY, TreeViewEnum.ALL);

// SYNC CALL
Response<Answer<List>> response = restCall.execute();

Answer<List> body = response.body();

Some other drawbacks of Retrofit 2

Some other annoyances with Retrofit, to my opinion are :

  • The framework is not using the Java 8 classes for async calls
  • The pesky IOException when I invoke a REST Client method. I don’t want the checked exception….

 

Conclusion

From my small usecase, I have decided to stay with Retrofit 2, since the design of the API was a huge plus and I really hate to deal with the native layer of Java. OkHttp seems a pretty decent wrapper to ease the coder job. But I admit that the last drawbacks (readiness for Java 8 and the IOException) are really annoying me.

References

Retrofit 2 Unsafe ssl certificates

 

Reddit Feign vs Retrofit/

OKHttp Library

Sylvain Leroy

Senior Software Quality Manager and Solution Architect in Switzerland, I have previously created my own company, Tocea, in Software Quality Assurance. Now I am offering my knowledges and services in a small IT Consulting company : Byoskill and a website www.byoskill.com Currently living in Lausanne (CH)