BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles HTTP-RPC: A Lightweight Cross-Platform REST Framework

HTTP-RPC: A Lightweight Cross-Platform REST Framework

Leia em Português

Bookmarks

Key takeaways

  • HTTP-RPC is a lightweight open-source framework for developing RESTful applications using an RPC metaphor
  • Provides both server and client-side APIs
  • Supports a variety of operating systems and devices
  • Supports multiple languages including Java, Objective-C/Swift, and JavaScript

 

HTTP-RPC is an open-source framework for simplifying development of REST-based applications. It allows developers to create and access HTTP-based web services using a convenient, RPC-like metaphor, while preserving fundamental REST principles such as statelessness and uniform resource access.

The project currently includes support for implementing REST services in Java and consuming services in Java, Objective-C/Swift, or JavaScript. The server component provides a lightweight alternative to other, larger Java-based REST frameworks, making it an ideal choice for microservices and "Internet of Things" (IOT) applications. The consistent cross-platform client API makes it easy to interact with services regardless of target device or operating system.

Overview

HTTP-RPC services are accessed by applying an HTTP verb such as GET or POST to a target resource. The target is specified by a path representing the name of the resource, and is generally expressed as a noun represented as a URI, such as /calendar or /contacts.

Arguments are supplied either via the query string or in the request body, like an HTML form. Results are generally returned as JSON, although operations that do not return a value are also supported.

For example the following request might retrieve the sum of two numbers, whose values are specified by the a and b query arguments:

GET /math/sum?a=2&b=4

Alternatively, the argument values could be specified as a list rather than as two fixed variables:

GET /math/sum?values=1&values=2&values=3

In either case, the service would return the value 6 in response.

POST, PUT, and DELETE operations are accessed similarly.

Implementing Services

The HTTP-RPC server library is distributed as a single 32KB JAR file with no external dependencies. It includes the following packages/classes:

org.httprpc

WebService - abstract base class for HTTP-RPC services RPC - annotation that specifies a "remote procedure call", or service method

org.httprpc.beans

BeanAdapter - adapter class that presents the contents of a Java Bean instance as a map, suitable for serialization to JSON

org.httprpc.sql

ResultSetAdapter - adapter class that presents the contents of a JDBC result set as an iterable list, suitable for streaming to JSON

Parameters - class for simplifying execution of prepared statements

org.httprpc.util

IteratorAdapter - adapter class that presents the contents of an iterator as an iterable list, suitable for streaming to JSON.

Each of these classes is discussed in more detail below.

WebService Class

WebService is an abstract base class for HTTP-RPC web services. Service operations are defined by adding public methods to a concrete service implementation.

The @RPC annotation is used to flag a method as remotely accessible. This annotation associates an HTTP verb and a resource path with the method. All public annotated methods automatically become available for remote execution when the service is published.

For example, the following class might be used to implement the simple addition operations discussed in the previous section:

public class MathService extends WebService {
    @RPC(method="GET", path="sum") 
    public double getSum(double a, double b) {
        return a + b; 
    }
    
    @RPC(method="GET", path="sum") 
    public double getSum(List<Double> values) {
        double total = 0;

        for (double value : values) {
            total += value; 
        }

        return total;
     } 
}

Note that both methods are mapped to the path /math/sum. The best method to execute is selected based on the names of the provided argument values. For example, the following request would cause the first method to be invoked:

GET /math/sum?a=2&b=4

This request would invoke the second method:

GET /math/sum?values=1&values=2&values=3

Method Argument and Return Types

Method arguments may be any numeric primitive type or wrapper class, boolean, java.lang.Boolean, or java.lang.String. Arguments may also be an instance of java.net.URL or java.util.List. URL arguments represent binary content, such as JPEG or PNG images. List arguments represent multi-value arguments. List elements may be any supported simple type; e.g. List<Integer> or List<URL>.

Methods may return any numeric primitive type or wrapper class, boolean, java.lang.Boolean, or java.lang.CharSequence . They may also return instances of java.util.List or java.util.Map.

Results are mapped to their JSON equivalents as follows:

  • java.lang.Number or numeric primitive: number
  • java.lang.Boolean or boolean primitive: true/false
  • java.lang.CharSequence : string 
  • java.util.List : array 
  • java.util.Map : object

Note that List and Map types are not required to support random access; iterability is sufficient. Additionally, List and Map types that implement java.lang.AutoCloseable will be automatically closed after their values have been written to the output stream. This allows service implementations to stream response data rather than buffering it in memory before it is written.

For example, the org.httprpc.sql.ResultSetAdapter class wraps an instance of java.sql.ResultSet and exposes its contents as a forward-scrolling, auto-closeable list of map values. Closing the list also closes the underlying result set, ensuring that database resources are not leaked.

ResultSetAdapter is discussed in more detail later.

Request Metadata

WebService provides the following methods that allow an extending class to obtain additional information about the current request:

getLocale() - returns the locale associated with the current request

getUserName() - returns the user name associated with the current request, or null if the request was not authenticated

getUserRoles() - returns a set representing the roles the user belongs to, or null if the request was not authenticated

The values returned by these methods are populated via protected setters, which are called once per service request. While these setters are not meant to be called by application code, they can be used to facilitate unit testing of service implementations.

BeanAdapter Class

The BeanAdapter class allows the contents of a Java Bean object to be returned from a service method. This class implements the Map interface and exposes any properties defined by the Bean as entries in the map, allowing custom data types to be serialized to JSON.

For example, the following Bean class might be used to represent basic statistical data about a collection of values:

public class Statistics {
    private int count = 0; 
    private double sum = 0; 
    private double average = 0;

    public int getCount() {
        return count; 
    }

    public void setCount(int count) {
        this.count = count; 
    }

    public double getSum() {
        return sum; 
    }

    public void setSum(double sum) {
        this.sum = sum; 
    }

    public double getAverage() {
        return average; 
    }

    public void setAverage(double average) {
        this.average = average; 
    } 
}

Using this class, an implementation of a getStatistics() method might look like this:

@RPC(method="GET", path="statistics")
public Map<String, ?> getStatistics(List<Double> values) {
    Statistics statistics = new Statistics();

    int n = values.size();

    statistics.setCount(n);

    for (int i = 0; i < n; i++) {
        statistics.setSum(statistics.getSum() + values.get(i)); 
    }

    statistics.setAverage(statistics.getSum() / n);

    return new BeanAdapter(statistics);
}

Although the values are actually stored in the strongly typed Statistics object, the adapter makes the data appear as a map, allowing it to be returned to the caller as a JSON object.

Note that, if a property returns a nested Bean type, the property's value will be automatically wrapped in a BeanAdapter instance. Additionally, if a property returns a List or Map type, the value will be wrapped in an adapter of the appropriate type that automatically adapts its sub-elements. This allows service methods to return recursive structures such as trees.

BeanAdapter can be used to easily transform the result of JPA queries to JSON. An example of using BeanAdapter with Hibernate can be found here.

ResultSetAdapter and Parameters Classes

The ResultSetAdapter class allows the result of a SQL query to be efficiently returned from a service method. This class implements the List interface and makes each row in a JDBC result set appear as an instance of Map, rendering the data suitable for serialization to JSON. It also implements AutoCloseable interface, to ensure that the underlying result set is closed and database resources are not leaked.

ResultSetAdapter is forward-scrolling only; its contents are not accessible via the get() and size() methods. This allows the contents of a result set to be returned directly to the caller without any intermediate buffering. The caller can simply execute a JDBC query, pass the resulting result set to the ResultSetAdapter constructor, and return the adapter instance:

@RPC(method="GET", path="data")
public ResultSetAdapter getData() throws SQLException {
    Statement statement = connection.createStatement();

    ResultSet resultSet = statement.executeQuery("select * from some_table");

    return new ResultSetAdapter(resultSet);
}

The Parameters class provides a means for executing prepared statements using named parameter values rather than indexed arguments. As in JPQL, parameter names are specified by a leading : character. For example:

SELECT * FROM some_table
WHERE column_a = :a OR column_b = :b OR column_c = COALESCE(:c, 4.0)

The parse() method is used to create a Parameters instance from a SQL statement. It takes a java.io.Reader containing the SQL text as an argument; for example:

Parameters parameters = Parameters.parse(new StringReader(sql));

The getSQL() method of the Parameters class returns the parsed SQL in standard JDBC syntax:

SELECT * FROM some_table
WHERE column_a = ? OR column_b = ? OR column_c = COALESCE(?, 4.0)

This value is used to create the actual prepared statement:

PreparedStatement statement = DriverManager.getConnection(url).prepareStatement(parameters.getSQL());

Parameter values are applied to the statement using the apply() method. The first argument to this method is the prepared statement, and the second is a map containing the statement arguments:

HashMap<String, Object> arguments = new HashMap<>();

arguments.put("a", "hello");
arguments.put("b", 3);

parameters.apply(statement, arguments);

Since explicit creation and population of the argument map can be cumbersome, the WebService class provides the following static convenience methods to help simplify map creation:

public static <K> Map<K, ?> mapOf(Map.Entry<K, ?>... entries) { ... }

public static <K> Map.Entry<K, ?> entry(K key, Object value) { ... }

Using the convenience methods, the code that applies the parameter values can be reduced to the following:

parameters.apply(statement, mapOf(entry("a", "hello"), entry("b", 3)));

Once applied, the statement can be executed:

return new ResultSetAdapter(statement.executeQuery());

An example demonstrating the use of the ResultSetAdapter and Parameters classes to access a MySQL database can be found here.

IteratorAdapter Class

The IteratorAdapter class allows the content of an arbitrary cursor to be efficiently returned from a service method. This class implements the List interface and adapts each element produced by the iterator for serialization to JSON, including nested List and Map structures. Like ResultSetAdapter, IteratorAdapter implements the AutoCloseable interface. If the underlying iterator type also implements AutoCloseable, IteratorAdapter will ensure that the underlying cursor is closed so that resources are not leaked.

As with ResultSetAdapter, IteratorAdapter is forward-scrolling only, so its contents are not accessible via the get() and size() methods. This allows the contents of a cursor to be returned directly to the caller without any intermediate buffering.

IteratorAdapter is typically used to serialize result data produced by NoSQL databases such as MongoDB. An example of using IteratorAdapter with Mongo can be found here.

Consuming Services

The HTTP-RPC client libraries provide a consistent interface for invoking service operations across multiple platforms. For example, the following code snippet demonstrates how the Java client's WebServiceProxy class can be used to access the methods of the math service discussed earlier. It first creates an instance of WebServiceProxy and configures it with a pool of ten threads for executing requests. It then invokes the getSum(double, double) method of the service, passing a value of 2 for "a" and 4 for "b". Finally, it executes the getSum(List<Double>) method, passing the values 1, 2, and 3 as arguments. Like the WebService class discussed in the previous section, WebServiceProxy provides static utility methods to help simplify argument map creation:

// Create service
URL serverURL = new URL("https://localhost:8443");
ExecutorService executorService = Executors.newFixedThreadPool(10);

WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService);

// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), new ResultHandler<Number>() {
    @Override public void execute(Number result, Exception exception) {
        // result is 6 
    } 
});

// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), new ResultHandler<Number>() {
    @Override public void execute(Number result, Exception exception) {
        // result is 6 
    } 
});

The result handler is a callback that will be executed upon completion of the request. In Java 7, anonymous inner classes are typically used to implement result handers. In Java 8 or later, lambda expressions can be used instead, reducing the invocation code to the following:

// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
    // result is 6 
});

// Get sum of all values 
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), (result, exception) -> {
    // result is 6 
});

The following example demonstrates how the math service can be accessed from Swift code. An instance of WSWebServiceProxy backed by the default URL session and a delegate queue supporting ten concurrent operations is used to execute the remote methods. Result handlers are implemented using closures:

// Configure session 
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() 

configuration.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData

let delegateQueue = NSOperationQueue() delegateQueue.maxConcurrentOperationCount = 10

let session = NSURLSession(configuration: configuration, delegate: self, delegateQueue: delegateQueue)

// Initialize service proxy and invoke methods 
let serverURL = NSURL(string: "https://localhost:8443")
let serviceProxy = WSWebServiceProxy(session: session, serverURL: serverURL!)

// Get sum of "a" and "b" 
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
    // result is 6
}

// Get sum of all values
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["values": [1, 2, 3]]) {(result, error) in
    // result is 6
}

Finally, this example demonstrates how the service can be accessed from a JavaScript client. An instance of WebServiceProxy is used to invoke the methods, and closures are used to implement the result handlers:

// Create service proxy 
var serviceProxy = new WebServiceProxy();

// Get sum of "a" and "b"
serviceProxy.invoke("GET", "/math/sum", {a:4, b:2}, function(result, error) {
    // result is 6 
});

// Get sum of all values
serviceProxy.invoke("GET", "/math/sum", {values:[1, 2, 3, 4]}, function(result, error) {
    // result is 6 
});

More Information

This article introduced the HTTP-RPC framework and provided some examples of how it can be used to easily create and consume RESTful web services in Java, Objective-C/Swift, and JavaScript. The project is being actively developed on GitHub, and support for additional platforms may be added in the future. Feedback is encouraged, and contributions are welcome.

For more information, please see the project README or contact the author at gk_brown@verizon.net.

About the Author

Greg Brown is a software engineer with over 20 years of experience in consulting, product, and open-source development. His current focus is  on mobile applications and REST services.

Rate this Article

Adoption
Style

BT