1. Overview
In this tutorial, we'll take a quick look at Finagle, Twitter's RPC library.
We'll use it to build a simple client and server.
2. Building Blocks
Before we dig into the implementation, we need to get to know the basic concepts we'll use to build our application. They are widely known but can have a slightly different meaning in Finagle's world.
2.1. Services
Services are functions represented by classes that take requests and return a Future containing the eventual result of the operation or information about the failure.
2.2. Filters
Filters are also functions. They take a request and a service, do some operations on the request, pass it to the service, do some operations on the resulting Future, and finally return the final Future. We can think of them as aspects as they can implement logic that happens around the execution of a function and alter its input and output.
2.3. Futures
Futures represent the eventual results of the asynchronous operations. They may be in one of the three states: pending, succeeded, or failed.
3. Service
First, we'll implement a simple HTTP greeting service. It'll take the name parameter from the request and respond and add the customary “Hello” message.
To do so, we need to create a class that will extend the abstract Service class from the Finagle library, implementing its apply method.
What we're doing looks similar to implementing a functional interface. Interestingly, though, we can't actually use that specific feature because Finagle is written in Scala and we are taking advantage of the Java-Scala interoperability:
public class GreetingService extends Service<Request, Response> { @Override public Future<Response> apply(Request request) { String greeting = "Hello " + request.getParam("name"); Reader<Buf> reader = Reader.fromBuf(new Buf.ByteArray(greeting.getBytes(), 0, greeting.length())); return Future.value(Response.apply(request.version(), Status.Ok(), reader)); } }
4. Filter
Next, we'll write a filter that will log some data about the request to the console. Similar to Service, we will need to implement Filter‘s apply method that'll take request and return a Future response, but this time it'll also take the service as the second parameter.
The basic Filter class has four type-parameters but very often we don't need to change the types of requests and responses inside the filter.
For that, we will use the SimpleFilter that merges the four type-parameters into two. We'll print some information from the request and then simply invoke the apply method from the provided service:
public class LogFilter extends SimpleFilter<Request, Response> { @Override public Future apply(Request request, Service<Request, Response> service) { logger.info("Request host:" + request.host().getOrElse(() -> "")); logger.info("Request params:"); request.getParams().forEach(entry -> logger.info("\t" + entry.getKey() + " : " + entry.getValue())); return service.apply(request); } }
5. Server
Now we can use the service and the filter to build a server that will actually listen for requests and process them.
We'll provision this server with a service that contains both our filter and service chained together with the andThen method:
Service serverService = new LogFilter().andThen(new GreetingService()); Http.serve(":8080", serverService);
6. Client
Finally, we need a client to send a request to our server.
For that, we'll create an HTTP service using the convenient newService method from Finagle's Http class. It'll be directly responsible for sending the request.
Additionally, we'll use the same logging filter we implemented before and chain it with the HTTP service. Then, we'll just need to invoke the apply method.
That last operation is asynchronous and its eventual results are stored in the Future instance. We could wait for this Future to succeed or fail but that would be a blocking operation and we may want to avoid it. Instead, we can implement a callback to be triggered when the Future succeeds:
Service<Request, Response> clientService = new LogFilter().andThen(Http.newService(":8080")); Request request = Request.apply(Method.Get(), "/?name=John"); request.host("localhost"); Future<Response> response = clientService.apply(request); Await.result(response .onSuccess(r -> { assertEquals("Hello John", r.getContentString()); return BoxedUnit.UNIT; }) .onFailure(r -> { throw new RuntimeException(r); }) );
Note that we return BoxedUnit.UNIT. Returning Unit is the Scala's way of coping with void methods, so we do it here to maintain interoperability.
7. Summary
In this tutorial, we learned how to build a simple HTTP server and a client using Finagle as well as how to establish communication between them and exchange messages.
As always, the source code with all the examples can be found over on GitHub.