In this tutorial, we’ll take a first look at Rest.li and learn about some of its most basic features. We’ll construct a server that responds with Fortunes for GET requests and also creates a client that sends a request to the server and prints a fortune returned by the server.
Rest.li uses an inversion of control model in which Rest.li defines the client and server architecture and handles many details of constructing, receiving, and processing RESTful requests. On the server side, Rest.li calls your code at the appropriate time to respond to requests. You only need to worry about your application-specific response to requests. On the client side, Rest.li helps send type-safe requests to the server and receives type-safe responses.
To allow Rest.li to perform its tasks, you need to conform to a simple architecture, in which you define a schema for your data, and classes that support REST operations on that data. Your classes will designate handlers for REST operations using Annotations and return objects that represent your data schema. Rest.li will handle mostly everything else.
We’ll see how Rest.li helps you perform these actions using automatic code generation, supporting base classes and other infrastructure.
Note: You will notice references to ‘Pegasus’ in various places as you work through this tutorial and read other Rest.li documents. Pegasus is the code name for the project that includes Rest.li and some related modules. It is also used in some package names.
If you like to do things yourself, you should be able to enter the code in this tutorial into whatever editor you like and construct each step of the process. You can also follow along using the ready-made source in the repository under examples/quickstart directory. Using the provided source tree frees you from worrying about the build scripts and directory structure until you want to use Rest.li in your own projects.
The example can be built using Gradle. Many of the steps involve code
generation that is automated by Gradle plugins provided as part of
Rest.li. We’ll show you the basic build scripts you need for this
example as we go along. For more details about the build process see
Gradle Build Integration. You will need
Gradle 1.6+ (run gradle --version
to check). If you have a different
Gradle version and do not want to install the version required by this
example globally, we recommend quickly setting up a Gradle
wrapper
for this project).
Before we get started, you’ll need to create a basic directory structure
to hold your classes. At the root of the example source tree, you should
have three sub-directories, api/
, client/
and server/
.
You will also need build.gradle
and settings.gradle
files at the top
level.
The settings.gradle
file just includes the sub-projects:
include 'api'
include 'server'
include 'client'
The file build.gradle
should contain:
buildscript {
repositories {
mavenLocal()
mavenCentral()
maven {
url "https://linkedin.jfrog.io/artifactory/open-source"
}
}
dependencies {
classpath 'com.linkedin.pegasus:gradle-plugins:29.19.2'
}
}
task wrapper(type: Wrapper) {
gradleVersion = '4.6'
}
final pegasusVersion = '29.19.2'
ext.spec = [
'product' : [
'pegasus' : [
'data' : 'com.linkedin.pegasus:data:' + pegasusVersion,
'generator' : 'com.linkedin.pegasus:generator:' + pegasusVersion,
'r2Netty' : 'com.linkedin.pegasus:r2-netty:' + pegasusVersion,
'restliCommon' : 'com.linkedin.pegasus:restli-common:' + pegasusVersion,
'restliClient' : 'com.linkedin.pegasus:restli-client:' + pegasusVersion,
'restliServer' : 'com.linkedin.pegasus:restli-server:' + pegasusVersion,
'restliTools' : 'com.linkedin.pegasus:restli-tools:' + pegasusVersion,
'gradlePlugins' : 'com.linkedin.pegasus:gradle-plugins:' + pegasusVersion,
'restliNettyStandalone' : 'com.linkedin.pegasus:restli-netty-standalone:' + pegasusVersion,
'restliServerStandalone' : 'com.linkedin.pegasus:restli-server-standalone:' + pegasusVersion
]
]
]
allprojects {
apply plugin: 'idea'
apply plugin: 'eclipse'
}
ext.enablePDL=true
subprojects {
apply plugin: 'maven'
afterEvaluate {
// add the standard pegasus dependencies wherever the plugin is used
if (project.plugins.hasPlugin('pegasus')) {
dependencies {
dataTemplateCompile spec.product.pegasus.data
restClientCompile spec.product.pegasus.restliClient
}
}
}
repositories {
mavenLocal()
mavenCentral()
maven {
url "https://linkedin.jfrog.io/artifactory/open-source"
}
}
}
This gradle build file pulls all required jars from a global Maven repository. It also loads some plugins that facilitate the build process and various code generation steps. Notice that plugins are also provided for IntelliJ Idea and Eclipse. For example, executing:
$ gradle idea
will generate an Idea project ready to open in Idea. Using Idea or Eclipse is a handy way to explore and follow along as you read this tutorial.
Here’s how the structure of your top-level project should look as we begin:
example-standalone-app/
+- build.gradle
+- settings.gradle
+- api/
+- client/
+- server/
The first thing we will do is implement a very simple server that responds to GET requests.
The basic steps you will follow to create a Rest.li server are:
Define data schema. Rest.li uses Pegasus Data Schema to define the resource data.
Generate language bindings. Rest.li will generate java class bindings for these data schemas to be used in your server and clients.
Implement resource classes containing methods to act on your data. Rest.li provides a set of base classes and annotations that will map these methods to URIs and REST operations.
Create an HTTP server that instantiates a Rest.li server. The Rest.li server will automatically locate your resource classes and invoke the appropriate methods when a request is received.
Rest.li provides tools to make these steps simple, including code generators that create classes from the data schema, base classes, and annotations that map entry points in your code to REST operations.
Let’s walk through each step of the process.
The first step in creating a Rest.li service is to define a data model
or schema for the data that will be returned by your server. We will
define the data model in the api/
directory, which serves to define
the API or interface between the server and clients.
All Rest.li data models are defined in Pegasus Data Schema files, which
have a .pdl
suffix. We’ll define a Fortune
data model in
Fortune.pdl
. The location of this file is important. Be sure to place
it in a path corresponding to your namespace, under
api/src/main/pegasus/
:
namespace com.example.fortune
/**
* Generate a fortune cookie
*/
record Fortune {
/**
* The Fortune cookie string
*/
fortune: string
}
Fortune.pdl
defines a record named Fortune, with an associated
namespace. The record has one field, a string whose name is fortune
.
Fields as well as the record itself can have optional documentation
strings. This is, of course, a very simple schema. See
Data Schemas for
details on the syntax and more complex examples.
Rest.li uses the data model in .pdl
files to generate java versions
of the model that can be used by the server. The easiest way to generate
these classes is to use the Gradle integration provided as part of
Rest.li. You will need a build.gradle
file in the api/
directory
that looks like this:
apply plugin: 'pegasus'
With Fortune.pdl
and build.gradle
files in place, you can generate
a Java binding for the data model. This Java version is what will
actually be used by your server to return data to calling clients.
Change into the api/
directory and run the following command:
$ gradle build
The pegasus
Gradle plugin will detect the presence of Fortune.pdl
and use the dataTemplateGenerator to generate Fortune.java
. The
generated java classes will be placed under
api/src/mainGeneratedDataTemplate/
directory.
Your file system structure should now look like this:
example-standalone-app/
+- build.gradle
+- settings.gradle
+- api/
| +- build.gradle
| +- src/
| +- main/
| | +- pegasus/
| | +- com/
| | +- example/
| | +- fortune/
| | +- Fortune.pdl
| +- mainGeneratedDataTemplate/
| +- java/
| +- com/
| +- example/
| +- fortune/
| +- Fortune.java
+- client/
+- server/
The generated java file contains a java representation of the data model
defined in the schema, and includes get
and set
methods for each
element of the model, as well as other supporting methods. You can look
at the generated file to see the full implementation if you are curious;
the following excerpt should give you the general idea. This class is
entirely derived from your data model and should not be
modified.
@Generated(...)
public class Fortune extends RecordTemplate {
public String getFortune() {
return getFortune(GetMode.STRICT);
}
public Fortune setFortune(String value) {
putDirect(FIELD_Fortune, String.class, String.class, value, SetMode.DISALLOW_NULL);
return this;
}
// ... other methods
}
Now that we have defined our data model, the next step is to define a
resource
class that will be invoked by the Rest.li server in
response to requests from clients. We’ll create a class named
FortunesResource
. This class is written by hand, and implements any
REST operations you want to support, returning data using the java data
model class generated in the previous step. The file should be placed
according to your package path under
server/src/main/java
.
package com.example.fortune.impl;
import com.linkedin.restli.server.annotations.RestLiCollection;
import com.linkedin.restli.server.resources.CollectionResourceTemplate;
import com.example.fortune.Fortune;
import java.util.HashMap;
import java.util.Map;
/**
* Simple Rest.li Resource that serves up a fortune cookie.
*/
@RestLiCollection(name = "fortunes", namespace = "com.example.fortune")
public class FortunesResource extends CollectionResourceTemplate<Long, Fortune> {
// In-memory store for the fortunes
static Map<Long, String> fortunes = new HashMap<Long, String>();
static {
fortunes.put(1L, "Today is your lucky day.");
fortunes.put(2L, "There's no time like the present.");
fortunes.put(3L, "Don't worry, be happy.");
}
@Override
public Fortune get(Long key) {
// Retrieve the requested fortune
String fortune = fortunes.get(key);
if (fortune == null) {
fortune = "Your luck has run out. No fortune for id = " + key;
}
// return an object that represents the fortune cookie
return new Fortune().setFortune(fortune);
}
}
FortunesResource extends a Rest.li class, CollectionResourceTemplate
and, for this simple example, overrides a single method, get
, which
takes a single argument, an id of a resource to be returned. Rest.li
will call this method when it dispatches a GET request to the Fortune
resource. Additional REST operations could be provided by overriding
other methods. See the Rest.li User
Guide for more details about
supporting additional REST methods and other types of resources.
Notice that if this GET were to perform any IO it would be blocking
,
meaning that the thread handling this request will wait for that IO to
complete. Later we will show how we can build async GET methods that
return ParSeq Promise
and Task
classes so that we do not block while performing IO operations.
The RestLiCollection
annotation at the top of the file marks this class as a REST collection, and declares that this resource handles the /fortunes
URI. The result is that calling http://localhost/fortunes/<id>
(assuming your server is running on localhost) will call FortunesResource.get()
, which should return a Fortune
object corresponding to the given fortune identifier. For this simple implementation, we will create a static HashMap that maps several fortune strings to ids. If a requested id is found in the HashMap, we will construct a Fortune
object, set the message and id, and return the object. If the requested id is not found, we’ll return a default message. Rest.li will handle delivering the result to the calling client as a JSON object. (Recall that Fortune.java was generated in a previous step and is found under the api
directory.)
In a real implementation, you would perform whatever steps are required to retrieve or construct your response to the request. But ultimately, you will return an instance of your data model class that represents the data defined in your schema.
We’ve now completed the bulk of our application specific server side code. We’ve defined our data model, and implemented a Resource class that can respond to a GET request by returning data according to the model. The only thing remaining is to configure a HTTP framework to call our application logic. We will use Netty, an excellent framework that works great with Rest.li to build fully async services. For details on how to configure Rest.li with other servlet containers see Rest.li with Servlet Containers.
Rest.li also includes a Request Response layer (R2) that provides a transport abstraction and other services.
Notice that Rest.li automatically scans all resource classes in the specified package and initializes the REST endpoints/routes without any hard-coded connection. Adding additional resources or operations can be done simply by expanding your data schema and providing additional functionality in your Resource class(es).
To compile and run the server, we need a build.gradle
file in the
server/
directory, which should look like this:
apply plugin: 'pegasus'
ext.apiProject = project(':api')
dependencies {
compile project(path: ':api', configuration: 'dataTemplate')
compile spec.product.pegasus.restliServer
compile spec.product.pegasus.restliNettyStandalone
}
task startFortunesServer(type: JavaExec) {
main = 'com.linkedin.restli.server.NettyStandaloneLauncher'
args = ['-port', '8080', '-packages', 'com.example.fortune.impl']
classpath = sourceSets.main.runtimeClasspath
standardInput = System.in
}
Next, create a gradle.properties
file containing the following line:
rest.model.compatibility=ignore
This disables some compatibility checks on the generated files. You will need these checks in a real project but to keep this example simple we are disabling these checks.
With these files in place, your server directory structure should look like this:
example-standalone-app/
+- build.gradle
+- settings.gradle
+- api/
| ...
+- client/
+- server/
+- build.gradle
+- gradle.properties
+- src/
+- main/
+- java/
+- com/
+- example/
+- fortune/
+- impl/
+- FortuneResource.java
Now you can build the server from the server/
directory with:
$ gradle build
Note: If prompted, run the build command a second time. The first build runs a bootstrapping code generation process, requiring a second build to compile the generated code.
After building the server, you can launch the server using the following command:
$ gradle startFortunesServer
Once the server is running, you can perform tests using curl
:
$ curl -v http://localhost:8080/fortunes/1
* About to connect() to localhost port 8080 (#0)
* Trying ::1... connected
* Connected to localhost (::1) port 8080 (#0)
> GET /fortunes/1 HTTP/1.1
> User-Agent: curl/7.19.7 (x86_64-redhat-linux-gnu) libcurl/7.19.7 NSS/3.12.9.0 zlib/1.2.3 libidn/1.18 libssh2/1.2.2
> Host: localhost:8080
> Accept: */*
>
< HTTP/1.1 200 OK
< X-LinkedIn-Type: com.example.fortune.Fortune
< Content-Type: application/json
< Content-Length: 45
< Server: Jetty(6.1.26)
<
* Connection #0 to host localhost left intact
* Closing connection #0
{"id":1,"fortune":"Today is your lucky day."}[
Here, curl
issued a GET request for /fortunes/1
. Rest.li routed the
request to the FortunesResource
, which interpreted the argument 1
,
found the corresponding string, and constructed a Fortune
object to
return. Rest.li automatically transforms the java data model to JSON and
returns the result to the caller.
Before we move on to look at the Rest.li’s client support, notice that
the process of building the server generated an additional file. If you
look at your directory structure, you should see an IDL file under
server/src/mainGeneratedRest/idl/
. The file is in JSON format and
defines the interface supported by the server. The interface is
generated as a result of the annotations in the Resource class, in this
example, FortunesResource.java
. However if for some reason the file is
not available in your filesystem you can generate it by issuing the
following command
$ cd server
$ gradle publishRestliIdl
The publishRestliIdl
task will first run the generateRestModel
which
creates
server/src/mainGeneratedRest/idl/com.example.fortune.fortunes.restspec.json
.
It will then copy this file into the api
module at
api/src/main/idl/com.example.fortune.fortunes.restspec.json
.
You may also notice a snapshot/
directory next to the idl/
directory. This is used by the compatibility checker to keep track of
changes. You can ignore that for now.
Here is the generated IDL file. Notice that all of this information was
derived from server’s FortunesResource.java
including the
documentation
strings.
{
"name" : "fortune",
"namespace" : "com.example.fortune",
"path" : "/fortunes",
"schema" : "com.linkedin.restli.example.Fortune",
"doc" : "Simple Rest.li Resource that serves up a fortune cookie.\n\ngenerated from: com.example.fortune.impl.FortunesResource",
"collection" : {
"identifier" : {
"name" : "fortuneId",
"type" : "long"
},
"supports" : [ "get" ],
"methods" : [ {
"method" : "get"
} ],
"entity" : {
"path" : "/fortunes/{fortuneId}"
}
}
}
This file represents the contract between the server and the client.
Accordingly, the build also copied the IDL to the api
module, where it
can be accessed by the client code.
Just to verify that everything is in place, this is how your project’s
api/
and server/
directories should look at this point:
example-standalone-app/
+- build.gradle
+- settings.gradle
+- api/
| +- build.gradle
| +- src/
| +- main/
| | +- idl/
| | | +- com.example.fortune.fortunes.restspec.json
| | +- pegasus/
| | | +- com/
| | | +- example/
| | | +- fortune/
| | | +- Fortune.pdl
| | +- snapshot/
| | +- com.example.fortune.fortunes.snapshot.json
| +- mainGeneratedDataTemplate/
| +- java/
| +- com/
| +- example/
| +- fortune/
| +- Fortune.java
+- client/
+- server/
+- build.gradle
+- gradle.properties
+- src/
+- main/
| +- java/
| +- com/
| +- example/
| +- fortune/
| +- impl/
| +- FortuneResource.java
+- mainGeneratedRest/
+- idl/
| +- com.example.fortune.fortunes.restspec.json
+- snapshot/
+- com.example.fortune.fortunes.snapshot.json
Now that we have a server implemented and tested with curl, let’s see how we can use Rest.li to help build a client.
Rest.li uses the IDL published by the server to generate client classes
that can be used to construct requests. The pegasus
Gradle plugin
provides tools to generate these classes. Let’s start by creating a
build.gradle
file in the client/
directory:
apply plugin: 'java'
dependencies {
compile project(path: ':api', configuration: 'restClient')
compile spec.product.pegasus.r2Netty
}
task startFortunesClient(type: JavaExec) {
main = 'com.example.fortune.RestLiFortunesClient'
classpath = sourceSets.main.runtimeClasspath
}
To generate the interface classes used by the client, change to the
client/
directory and type the command:
$ gradle build
Building in the client directory generates java classes that represent
the resources and operations on those resources supported by the server.
These are basically convenience classes that help you build requests
from the client side. In this example, you should see some new java
files, including FortunesRequestBuilders.java
and
FortunesGetRequestBuilder.java
. These files are placed in the api/
module where they can be shared among multiple clients.
FortunesRequestBuilders
is a factory class that instantiates any
request builders you may need. In this example, our server resource only
supports GET requests, so the process has just generated a
FortunesGetRequestBuilder
class. You can look at the generated source
code under the api/src/mainGeneratedRest/
directory, if you’re
interested, but for this tutorial, let’s just go on to creating a client
and see how a builder is used.
Note: You may also see two additional files: FortuneBuilders.java
and FortunesGetBuilder.java
. These are deprecated interfaces that were
used prior to Rest.li v1.24.4. If you are just getting started with
Rest.li and using the latest version you can ignore these files.
Creating a client involves using a few classes to handle connecting to the server, and using the Builder classes generated in the previous step to construct requests. Let’s see how that works before we look at the actual client code.
The following lines of code instantiate a FortunesRequestBuilders
factory, and then call its get()
method to create a
FortunesGetRequestBuilder
object. Finally, the
FortunesGetRequestBuilder
instance lets you supply the information
that needs to be passed in the request and builds a Request
object:
FortunesRequestBuilders fortunesBuilders = new FortunesRequestBuilders();
FortunesGetRequestBuilder getBuilder = fortunesBuilders.get();
Request<Fortune> getRequest = getBuilder.id(fortuneId).build();
The process of sending a request from a client basically consists of
creating a RestClient
object and invoking its sendRequest()
method
to send the request to the
server:
RestClient restClient = new RestClient(r2Client, "http://localhost:8080/");
ResponseFuture<Fortune> getFuture = restClient.sendRequest(getRequest);
Response<Fortune> response = getFuture.getResponse();
RestClient.sendRequest()
returns a Future
, which can be used to wait
on and retrieve the response from the server. Note that the response is
type-safe, and parametrized as type Fortune, so we can use the Fortune
interface to retrieve the results, like
this:
String message = response.getEntity().getFortune();
long id = response.getEntity().getId();
Here is a completed RestLiFortunesClient
class, which uses the R2
library to create the transport mechanisms. For this example, the client
will just generate a random ID between 0 and 5, and print the response.
This file should go under client/src/main/java/
directory with the
appropriate java package
structure.
package com.example.fortune;
import com.linkedin.common.callback.FutureCallback;
import com.linkedin.common.util.None;
import com.linkedin.r2.transport.common.Client;
import com.linkedin.r2.transport.common.bridge.client.TransportClient;
import com.linkedin.r2.transport.common.bridge.client.TransportClientAdapter;
import com.linkedin.r2.transport.http.client.HttpClientFactory;
import com.linkedin.restli.client.Request;
import com.linkedin.restli.client.Response;
import com.linkedin.restli.client.ResponseFuture;
import com.linkedin.restli.client.RestClient;
import com.example.fortune.FortunesRequestBuilders;
import java.util.Collections;
public class RestLiFortunesClient {
/**
* This stand-alone app demos the client-side Rest.li API.
* To see the demo, run the server, then start the client
*/
public static void main(String[] args) throws Exception {
// Create an HttpClient and wrap it in an abstraction layer
final HttpClientFactory http = new HttpClientFactory();
final Client r2Client = new TransportClientAdapter(
http.getClient(Collections.<String, String>emptyMap()));
// Create a RestClient to talk to localhost:8080
RestClient restClient = new RestClient(r2Client, "http://localhost:8080/");
// Generate a random ID for a fortune cookie, in the range 0 - 5
long fortuneId = (long) (Math.random() * 5);
// Construct a request for the specified fortune
FortunesGetRequestBuilder getBuilder = fortuneBuilders.get();
Request<Fortune> getRequest = getBuilder.id(fortuneId).build();
// Send the request and wait for a response
final ResponseFuture<Fortune> getFuture = restClient.sendRequest(getRequest);
final Response<Fortune> response = getFuture.getResponse();
// Print the response
System.out.println(response.getEntity().getFortune());
// Shutdown
restClient.shutdown(new FutureCallback<None>());
http.shutdown(new FutureCallback<None>());
}
private static final FortunesRequestBuilders fortuneBuilders = new FortunesRequestBuilders();
}
With your client code in place, your directory structure should look like this:
example-standalone-app/
+- build.gradle
+- settings.gradle
+- api/
| ...
+- client/
| +- build.gradle
| +- src/
| +- main/
| +- java/
| +- com/
| +- example/
| +- fortune/
| +- RestLiFortunesClient.java
+- server/
...
Build the client by building in the client directory:
$ gradle build
To test our final client/server pair, start the server in one terminal window:
$ gradle startFortunesServer
Then in another window, run:
$ gradle startFortunesClient
You should see a “fortune cookie” printed out from the client before it exits.
If you want to inspect the request being sent by the client, stop the
server, and run netcat
or a similar packet sniffer tool to listen on
port 8080, and then run the client:
$ netcat -l -p 8080
GET /fortunes/1 HTTP/1.1
Host: localhost:8080
X-LI-R2-W-MsgType: REST
Content-Length: 0
We’ve now completed a quick tour of a few of the most basic features of Rest.li. Let’s review the steps we took to create a server and a corresponding client:
Fortune.pdl
)Fortune.java RecordTemplate class
)FortuneResource.java
) by subclassing CollectionResourceTemplate
and using RestLiAnnotations
to define operations and entry pointsfortune.restpec.json
) and Java client request builders
from the server Resource file (FortunesRequestBuilders.java
and
FortunesGetRequestBuilder.java
)RestClient
to send requests
constructed by calling the builder classes
(RestLiFortuneClient.java
)Notice that (ignoring Gradle build files) there are only three files in this example that you had to create:
Fortune.pdl
)FortunesResource.java
)RestLiFortuneClient.java
)Although Rest.li has many more features that can be leveraged when creating the server and client, most of your focus will usually be on defining data models and implementing resource classes that provide and/or manipulate the data.
To learn more about Rest.li, proceed to the more complex examples in the source code and read the Rest.li User’s Guide.