BT

Disseminando conhecimento e inovação em desenvolvimento de software corporativo.

Contribuir

Tópicos

Escolha a região

Início Artigos HTTP-RPC: Um framework REST leve e independente de plataforma

HTTP-RPC: Um framework REST leve e independente de plataforma

Favoritos

HTTP-RPC é um framework open-source que facilita o desenvolvimento de aplicações baseadas em REST. O framework permite que Web Services baseados em HTTP sejam desenvolvidos de acordo com a metáfora RPC, e ao mesmo tempo, preservam os princípios fundamentais REST como acesso uniforme aos recursos, além de não manter o estado das requisições.

Atualmente, o projeto inclui suporte para a implementação de serviços REST e seus respectivos consumidores nas linguagens de programação Java, Objective-C/Swift e JavaScript. O componente de serviço proporciona uma alternativa leve aos grandes frameworks Java e torna ideal a escolha do framework para o desenvolvimento de microservices e aplicações para "internet das coisas". O cliente da API facilita a interação com os serviços, independentemente do dispositivo alvo ou do sistema operacional.

Visão geral

Serviços HTTP-RPC são acessados através da aplicação de verbos HTTP, tais como GET e POST, para um determinado recurso desejado. O recurso é especificado através de um caminho que representa o seu nome, geralmente descrito por um substantivo como URI, como por exemplo "/calendario" ou "/contatos".

Argumentos são informados através de queries, ou informados diretamente no corpo (body) da requisição, de forma similar a realizada por formulários HTTP. Os resultados são geralmente retornados em JSON, entretanto, operações que não retornam valores também são suportadas.

Como exemplo, a requisição a seguir retorna o somatório de dois números cujos valores são especificados em uma query pelos argumentos "a" e "b".

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

Uma forma alternativa é informar os valores dos argumentos através de uma lista:

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

Em ambos os casos, o serviço deve retornar o valor 6 como resposta. As operações que utilizam os verbos HTTP POST, PUT e DELETE são acessadas de forma semelhante.

Implementação de serviços

A biblioteca de serviço HTTP-RPC é disponibilizada através de um único arquivo JAR de 32 KB sem dependências externas, que inclui os seguintes pacotes/classes:

org.httprpc

WebService - classe abstrata base para serviços RPC - anotação que especifica uma "chamada remota de procedimento" ou um método.

org.httprpc.beans

BeanAdapter - classe adaptadora para representar o conteúdo de uma instância Java Bean através de um mapa, adequado para serialização em JSON.

org.httprpc.sql

ResultSetAdapter - classe adaptadora que representa o result set de uma consulta JDBC como uma lista iterável, adequado para streaming em JSON.

Parameters - classe que simplifica a execução de prepared statements.

org.httprpc.util

IteratorAdapter - classe adaptadora que representa os conteúdos de um iterador em uma lista iterável, adequado para streaming em JSON.

A seguir, cada uma das classes descritas anteriormente são discutidas em detalhes.

Classe WebService

A classe WebService é uma abstração base para Web Services HTTP-RPC. Operações de serviços são definidas por implementações "concretas" de serviços através de métodos públicos.

A anotação @RPC é utilizada para tornar um método acessível remotamente. Esta anotação associa um verbo HTTP a um caminho com o método anotado. Todos os métodos públicos anotados ficam disponíveis para execuções remotas quando o serviço é publicado.

Por exemplo, a classe a seguir pode ser utilizada para implementar a operação de adição apresentada anteriormente:

public class MathService extends WebService {
    @RPC(method="GET", path="math/sum") 
    public double getSum(double a, double b) {
        return a + b; 
    }
    
    @RPC(method="GET", path="math/sum") 
    public double getSum(List values) {
        double total = 0;
        for (double value : values) {
            total += value; 
        }
        return total;
     } 
}

Note que ambos os métodos são mapeados para o caminho "/math/sum". O método de execução mais adequado é selecionado com base nos nomes dos parâmetros informados. Por exemplo, a requisição a seguir resulta na invocação do primeiro método:

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

A requisição a seguir resulta na invocação do segundo método:

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

Argumentos de método e tipos de retorno

Argumentos de método podem ser qualquer tipo primitivo numérico ou classes wrapper, boolean, java.lang.Boolean ou java.lang.String. Argumentos também podem ser instâncias de java.net.URL ou java.util.List. Argumentos na forma de URL representam conteúdo binário, como por exemplo imagens JPEG ou PNG. Argumentos na forma de List representam parâmetros multivalorados, que podem assumir qualquer tipo simples suportado, como List<Integer> ou List<URL>.

Métodos podem retornar qualquer tipo primitivo numérico ou classes wrapper, boolean, java.lang.Boolean ou java.lang.CharSequence. Métodos podem também retornar instâncias de java.util.List ou java.util.Map.

Os resultados são mapeados para seus equivalentes em JSON da seguinte forma:

  • java.lang.Number ou primitivo numérico: number
  • java.lang.Boolean ou primitivo booleano: true/false
  • java.lang.CharSequence: string
  • java.util.List: arrayNumber
  • java.util.Map: object

Note que os tipos List e Map não necessariamente suportam acesso randômico, pois apenas iterações são suficientes. Além disso, os tipos List e Map que implementam java.lang.AutoCloseable serão automaticamente encerrados após seus valores serem escritos no stream de saída. Isso permite que implementações de serviços retornem stream de dados, ao contrário de armazenar a resposta em memória antes da escrita.

Por exemplo, org.httprpc.sql.ResultSetAdapter é uma classe wrap de uma instância de java.sql.ResultSet, que expõe seu conteúdo através de uma lista de mapas de valores forward-scrolling e auto-closeable. Fechar a lista implica no fechamento de seu respectivo result set, assegurando o bom gerenciamento dos recursos do banco de dados. Mais detalhes sobre o ResultSetAdapter são apresentados mais adiante neste artigo.

Requisição de metadados

A classe WebService disponibiliza os seguintes métodos que permitem a uma classe extendida obter informações adicionais a respeito da requisição atual:

getLocale() - retorna o locale da requisição atual.

getUserName() - retorna o nome do usuário da requisição atual, retorna null caso a requisição não seja autenticada.

getUserRoles() - retorna o conjunto de roles que pertencem ao usuário, retorna null caso a requisição não seja autenticada.

Os valores retornados por estes métodos são construídos através de métodos acessores protected, que são invocados durante a requisição do serviço. Uma vez que a invocação destes métodos acessores não é realizada diretamente pelo código da aplicação, eles podem ser utilizados para facilitar o teste unitário das implementações do serviço.

Classe BeanAdapter

A classe BeanAdapter permite que o conteúdo de um objeto Java Bean seja retornado como resposta de serviço. Esta classe implementa a interface Map e expõe todas as propriedades do objeto como entradas em um mapa. Além disso, esta classe permite personalizar os tipos de dados que serão serializados em JSON. Por exemplo, a classe a seguir pode ser utilizada para representar dados estatísticos básicos a respeito de uma coleção de valores:

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; 
    } 
}

A seguir, uma possível implementação do método getStatistics:

@RPC(method="GET", path="statistics")
public Map getStatistics(List 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);
}

Apesar dos valores serem armazenados no objeto Statistics, de forma fortemente tipada, a classe adaptadora converte os dados em um mapa, fato que permite ao serviço retornar um objeto JSON ao consumidor do serviço.

Caso o tipo de uma propriedade seja um Bean aninhado, o valor da propriedade será automaticamente convertido para uma instância de BeanAdapter. Além disso, em caso do tipo de uma propriedade ser um List ou um Map, seu valor será convertido em um adaptador apropriado para conversão dos seus sub-elementos. Sendo assim, é possível que os serviços retornem estruturas recursivas. Por fim, a classe BeanAdapter pode ser facilmente utilizada para transformar resultados de consultas JPA em documentos JSON.

Classes ResultSetAdapter e Parameters

A classe ResultSetAdapter permite que o resultado de uma consulta SQL seja retornado por um serviço. Esta classe implementa a interface List e faz com que cada linha do result set JDBC seja transformada em uma instância de Map, formato adequado para serialização JSON. A classe ainda implementa a interface AutoCloseable, que garante o encerramento do result set após o término da consulta SQL.

A classe ResultSetAdapter é forward-scrolling only, ou seja, não é possível acessar dados do result set anteriormente manipulados. Além disso, os dados não são acessíveis através dos métodos get() e size(). Dessa forma, o conteúdo de um result set pode ser retornado diretamente ao solicitante, sem a necessidade de buferização intermediária. Pode-se simplesmente executar uma consulta JDBC, passar o result set como argumento do construtor de ResultSetAdapter e obter a instância do adaptador:

@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);
}

A classe Parameters oferece meios para a execução de prepared statements através da associação de valores com parâmetros nomeados, ao invés de argumentos indexados. Semelhante a JPQL, os nomes de parâmetros são especificados com o caractere ":", por exemplo:

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

O método parse() é utilizado para criar uma instância da classe Parameters a partir de uma instrução SQL. Esta criação é feita através de um java.io.Reader, que contém a instrução SQL correspondente como argumento do método, por exemplo:

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

O método getSQL() da classe Parameters retorna a instrução SQL na sintaxe padrão JDBC:

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

Este é o valor utilizado para criar o prepared statement:

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

Valores de parâmetros são aplicados no statement pelo método apply(). O primeiro argumento desse método é o próprio prepared statement, enquanto o segundo é um mapa que contém os argumentos do statement:

HashMap arguments = new HashMap<>();
arguments.put("a", "hello");
arguments.put("b", 3);
parameters.apply(statement, arguments);

A criação explícita do mapa de argumentos pode ser massante, entretanto, a classe WebService oferece um método estático conveniente que simplifica este processo:

public static  Map mapOf(Map.Entry... entries) { ... }
public static  Map.Entry entry(K key, Object value) { ... }

Através de métodos convenientes, o código para a aplicação dos valores dos parâmetros pode ser reduzido para:

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

Um vez aplicado, a instrução pode ser executada da seguinte forma:

return new ResultSetAdapter(statement.executeQuery());

IteratorAdapter

A classe IteratorAdapter permite que o conteúdo de um cursor arbitrário seja eficientemente retornado a partir de um método de serviço. Esta classe implementa a interface List e adapta cada elemento produzido pelo iterador para a serialização JSON, incluindo listas aninhadas e estruturas em Map. Da mesma forma que a classe ResultSetAdapter, IteratorAdapter implementa a interface AutoCloseable. Se o respectivo tipo de iterador também implemente AutorCloseable, o IteratorAdapter assegura que o respectivo cursor será fechado, garantindo o uso adequado dos recursos.

Assim como a classe ResultSetAdapter, IteratorAdapter é forward-scrolling only. Dessa forma, o conteúdo do IteratorAdapter não é acessível através dos métodos get() e size(). Isto garante que o conteúdo de um cursor seja diretamente retornado para o solicitante sem buferização intermediária. O IteratorAdapter é geralmente utilizado para serializar os dados resultantes de bases de dados NoSQL, como por exemplo MongoDB.

Interação com serviços

As bibliotecas de cliente para o HTTP-RPC disponibilizam uma interface consistente para a invocação multiplataforma de serviços. O trecho de código a seguir apresenta o uso da classe WebServiceProxy para o acesso dos métodos do exemplo de cálculos matemáticos apresentado anteriormente. Primeiramente, cria-se uma instância da classe WebServiceProxy, da qual configura-se um pool de 10 threads para a execução das requisições. A seguir, invoca-se o método getSum(double, double), passando os valor 2 para o argumento "a" e 4 para "b". Por fim, executa-se o método getSum(List<Double>), passando os valores 1, 2, e 3 como parâmetros do método. Da mesma forma que a classe WebService apresentada anteriormente, a classe WebServiceProxy disponibiliza métodos utilitários estáticos para auxiliar a criação de mapas com argumentos:

// Criar serviço
URL serverURL = new URL("https://localhost:8443");
ExecutorService executorService = Executors.newFixedThreadPool(10);

WebServiceProxy serviceProxy = new WebServiceProxy(serverURL, executorService);

// Obter a soma de a e b
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), new ResultHandler() {
    @Override public void execute(Number result, Exception exception) {
        // o resultado é 6
    } 
});

// Obter a soma de todos os valores
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), new ResultHandler() {
    @Override public void execute(Number result, Exception exception) {
        // o resultado é 6
    } 
});

O result handler é um callback que será executado ao término da requisição. No Java 7, classes internas anônimas são geralmente utilizadas para implementar result handlers, enquanto no Java 8 ou versões mais novas, expressões lambda podem ser utilizadas para reduzir a quantidade de código necessária para implementar a invocação, como mostra o exemplo a seguir:

// Obter a soma de a e b
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("a", 2), entry("b", 4)), (result, exception) -> {
    // o resultado é 6 
});

// Obter a soma de todos os valores 
serviceProxy.invoke("GET", "/math/sum", mapOf(entry("values", listOf(1, 2, 3))), (result, exception) -> {
    // o resultado é 6
});

O exemplo de código a seguir mostra como o serviço de operações matemáticas, exemplo apresentado anteriormente, pode ser acessado de um código Swift. Uma instância da classe WSWebServiceProxy é utilizada para executar os métodos remotos. Neste exemplo, são suportadas 10 operações concorrentes.

// Configuração da sessão
let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() 

configuration.requestCachePolicy = NSURLRequestCachePolicy.ReloadIgnoringLocalAndRemoteCacheData

let delegateQueue = NSOperationQueue() delegateQueue.maxConcurrentOperationCount = 10

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

// Inicializar serviço e invocar métodos 
let serverURL = NSURL(string: "https://localhost:8443")
let serviceProxy = WSWebServiceProxy(session: session, serverURL: serverURL!)

// Obter a soma de a e b
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["a": 2, "b": 4]) {(result, error) in
    // o resultado é 6
}

// Obter a soma de todos os valores
serviceProxy.invoke("GET", path: "/math/sum", arguments: ["values": [1, 2, 3]]) {(result, error) in
    // o resultado é 6
}

Por fim, o exemplo a seguir mostra como o serviço pode ser acessado a partir de um cliente JavaScript. Uma instância da classe WebServiceProxy é utilizada para invocar os métodos, enquanto closures são utilizados para implementar os result handlers.

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

//  Obter a soma de a e b
serviceProxy.invoke("GET", "/math/sum", {a:4, b:2}, function(result, error) {
    // o resultado é 6 
});

// Obter a soma de todos os valores
serviceProxy.invoke("GET", "/math/sum", {values:[1, 2, 3, 4]}, function(result, error) {
    // o resultado é 6 
});

Conclusão

Este artigo apresentou o framework HTTP-RPC e exemplificou como o framework pode ser utilizado para criar Web Services RESTful em Java e aplicações clientes implementados com Objective-C/Swift e JavaScript. O projeto está em ativo desenvolvimento no GitHub e o suporte para mais plataformas pode ser adicionado futuramente. Comentários e contribuições são bem vindos.

Mais informações disponíveis no site do projeto, ou diretamente com o autor em atgk_brown at verizon.net.

Sobre o autor

 Greg Brown é engenheiro de software com mais de 20 anos de experiência em consultoria e desenvolvimento open-source. Seu foco  atualmente está em aplicações móveis e serviços REST.

Avalie esse artigo

Relevância
Estilo/Redação

Conteúdo educacional

BT