2024-03-19
AppManager screenshot

REST Http Client : Feign vs Retrofit 2

https://sylvainleroy.com/wp-admin/options-general.php?page=ad-inserter.php#tab-2

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.

Contents

Update 2020-06

Recently, the things have moved a little bit about these two frameworks and I wanted to share you some field experience.

Retrofit 2 is bundling a quite old version of OkHttp (3.x). I tried to use the latest 4.x with Retrofit 2 and at that time, it does not work.

Feign now ships a lot of features, SFL4j logging, JAX-RS compatibility, Java 11 Http/2. It looks more exciting. However there are some caveats with this API like the documentation is quite poor. Spent 15 minutes to understand how to pass the Request body in a POST Mapping. Found the documentation in a bug ticket on Github. Moreover the error handling is not as easy as Retrofit and not as clear. Where is the status code ? How to get an access to a specific response Header ? Much things that you will have to discover and it is pretty annoying.

Conclusion : I really like Retrofit and I am used to the easiness of the API however the library is aging and losing its traction. 

Introduction to APM

APM AppManager tool 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 DevOps, 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 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 Java.

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_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);

        //
        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_URL)
            .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 JUNIT 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 CURL 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 (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_);
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 complained my 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 is not using the Java 8 classes CompletableFuture 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 knowledge and services in a small IT Consulting company : Byoskill and a website www.byoskill.com Currently living in Lausanne (CH)

View all posts by Sylvain Leroy →