The REST/RPC (RPC over REST) API allows the creation of client-side remote proxy interfaces for calling methods on server-side POJOs using entirely REST.
-
This is not to be confused with REST Proxies which are entirely client-side driven Java interfaces
against arbitrary backend REST interfaces.
Remote Interfaces
The following example shows a remote interface:
| @RemoteInterface // Annotation is optional
| public interface IAddressBook {
|
| void init() throws Exception;
|
| List<Person> getPeople();
|
| List<Address> getAddresses();
|
| int createPerson(CreatePerson cp) throws Exception;
|
| Person findPerson(int id);
|
| Address findAddress(int id);
|
| Person findPersonWithAddress(int id);
|
| Person removePerson(int id);
| }
The requirements for a remote interface method are:
Throwables with public no-arg or single-arg-string constructors are automatically recreated on the client side
when thrown on the server side.
Client side
Remote Interface proxies are instantiated on the client side using one of the following methods:
- {@link oajr.client.RestClient}
- {@link oajr.client.RestClient#getRrpcInterface(Class) getRrpcInterface(Class)}
- {@link oajr.client.RestClient#getRrpcInterface(Class,Object) getRrpcInterface(Class,Object)}
- {@link oajr.client.RestClient#getRrpcInterface(Class,Object,Serializer,Parser) getRrpcInterface(Class,Object,Serializer,Parser)}
Since we build upon the existing RestClient API, we inherit all of it's features.
For example, convenience methods for setting POJO filters and properties to customize the behavior of the
serializers and parsers, and the ability to provide your own customized Apache HttpClient for
handling various scenarios involving authentication and Internet proxies.
Here's an example of the above interface being used:
| // Create a RestClient using JSON for serialization, and point to the server-side remote interface servlet.
| RestClient client = RestClient.create()
| .json()
| .rootUrl("http://localhost:10000/remote")
| .build();
|
| // Create a proxy interface.
| IAddressBook ab = client.getRrpcInterface(IAddressBook.class);
|
| // Invoke a method on the server side and get the returned result.
| Person p = ab.createPerson(
| new Person(
| "John Smith",
| "Aug 1, 1999",
| new Address("My street", "My city", "My state", 12345, true)
| )
| );
Under the covers, this method call gets converted to a REST POST.
| HTTP POST http://localhost:10000/remote/org.apache.juneau.examples.addressbook.IAddressBook/createPerson(org.apache.juneau.examples.addressbook.Person)
| Accept: application/json
| Content-Type: application/json
|
| [
| {
| "name":"John Smith",
| "birthDate":"Aug 1, 1999",
| "addresses":[
| {
| "street":"My street",
| "city":"My city",
| "state":"My state",
| "zip":12345,
| "isCurrent":true
| }
| ]
| }
| ]
Note that the body of the request is an array.
This array contains the serialized arguments of the method.
The object returned by the method is then serialized as the body of the response.
Server side
There are two ways to expose remote interfaces on the server side:
-
Extending from {@link oajr.remote.RrpcServlet}.
-
Using a @RestOp(method=RRPC) annotation on a Java method.
In either case, the proxy communications layer is pure REST.
Therefore, in cases where the interface classes are not available on the client side, the same method calls can
be made through pure REST calls.
This can also aid significantly in debugging, since calls to the remote interface service can be made directly from
a browser with no coding involved.
RrpcServlet
The {@link oajr.remote.RrpcServlet} class is a simple specialized servlet with an abstract
getServiceMap() method to define the server-side POJOs:
| @Rest(
| path="/remote"
| )
| public class SampleRrpcServlet extends RrpcServlet {
|
| // Our server-side POJO.
| private AddressBook addressBook = new AddressBook();
|
| @Override /* RrpcServlet */
| protected Map<Class<?>,Object> getServiceMap() throws Exception {
| Map<Class<?>,Object> map = new LinkedHashMap<Class<?>,Object>();
|
| // In this simplified example, we expose the same POJO service under two different interfaces.
| // One is IAddressBook which only exposes methods defined on that interface, and
| // the other is AddressBook itself which exposes all methods defined on the class itself (dangerous!).
| map.put(IAddressBook.class, addressBook);
| map.put(AddressBook.class, addressBook);
| return map;
| }
| }
@RestOp(method=RRPC)
The @RestOp(method=RRPC) approach is easier if you only have a single
interface you want to expose.
You simply define a Java method whose return type is an interface, and return the implementation of that
interface:
| // Our exposed interface.
| @RestOp(method=RRPC, path="/addressbookproxy/*")
| public IAddressBook getProxy() {
| return addressBook;
| }
RrpcServlet in a browser
If you point your browser to the servlet above, you get a list of available interfaces:
| http://localhost:10000/remote
Clicking the hyperlinks on each shows you the list of methods that can be invoked on that service.
Note that the IAddressBook link shows that you can only invoke methods defined on that
interface, whereas the AddressBook link shows ALL public methods defined on that class.
| http://localhost:10000/remote/org.apache.juneau.examples.addressbook.IAddressBook
Since AddressBook extends from LinkedList, you may notice familiar collections
framework methods listed.
| http://localhost:10000/remote/org.apache.juneau.examples.addressbook.AddressBook
Let's see how we can interact with this interface through nothing more than REST calls to get a better idea on
how this works.
We'll use the same method call as in the introduction.
First, we need to create the serialized form of the arguments:
| Object[] args = new Object[] {
| new CreatePerson("Test Person",
| AddressBook.toCalendar("Aug 1, 1999"),
| new CreateAddress("Test street", "Test city", "Test state", 12345, true))
| };
| String asJson = Json5Serializer.DEFAULT_READABLE.toString(args);
| System.err.println(asJson);
That produces the following JSON output:
| [
| {
| name: 'Test Person',
| birthDate: 'Aug 1, 1999',
| addresses: [
| {
| street: 'Test street',
| city: 'Test city',
| state: 'Test state',
| zip: 12345,
| isCurrent: true
| }
| ]
| }
| ]
Note that in this example we're using JSON.
However, various other content types can also be used such as XML, URL-Encoding, UON, or HTML.
In practice however, JSON will preferred since it is often the most efficient.
Next, we can use a tool such as Poster to make the REST call.
Methods are invoked by POSTing the serialized object array to the URI of the interface method.
In this case, we want to POST our JSON to the following URL:
| http://localhost:10000/remote/org.apache.juneau.examples.addressbook.IAddressBook/createPerson(org.apache.juneau.examples.addressbook.CreatePerson)
Make sure that we specify the Content-Type of the body as text/json.
We also want the results to be returned as JSON, so we set the Accept header to
text/json as well.
When we execute the POST, we should see the following successful response whose body contains the returned
Person bean serialized to JSON:
From there, we could use the following code snippet to reconstruct the response object from JSON:
| String response = "output from above";
| Person p = JsonParser.DEFAULT.parse(response, Person.class);
If we alter our servlet to allow overloaded GET requests, we can invoke methods using nothing more than a
browser...
| @Rest(
| path="/remote",
|
| // Allow us to use method=POST from a browser.
| allowedMethodParams="*"
| )
| public class SampleRrpcServlet extends RrpcServlet {
For example, to invoke the getPeople() method on our bean:
| http://localhost:10000/remote/org.apache.juneau.examples.addressbook.IAddressBook/getPeople?method=POST
Here we call the findPerson(int) method to retrieve a person and get the
returned POJO (in this case as HTML since that's what's in the Accept header when calling from a
browser):
| http://localhost:10000/remote/org.apache.juneau.examples.addressbook.IAddressBook/findPerson(int)?method=POST&body=@(3)
When specifying the POST body as a &body parameter, the method arguments should be in UON
notation.
See {@link oaj.uon.UonSerializer} for more information about this encoding.
Usually you can also pass in JSON if you specify &Content-Type=text/json in the URL parameters
but passing in unencoded JSON in a URL may not work in all browsers.
Therefore, UON is preferred.
The hyperlinks on the method names above lead you to a simple form-entry page where you can test
passing parameters in UON notation as URL-encoded form posts.