Todavia, o uso de boas práticas de programação aliado a um modelo arquitetural simplista e robusto podem reduzir drasticamente o tempo necessário para construir tais sistemas. Neste sentido, o REST (Representational State Transfer) apresenta-se como um modelo arquitetural poderoso e suficientemente maduro, capaz de atender requisitos complexos de integração sem agregar complexidade e excesso de acoplamento.
Utilizando-se de tecnologias já consagradas e de grande penetração no mercado, o modelo arquitetural REST visa orientar o desenvolvimento de uma aplicação dentro de uma filosofia “WEB-Like”, fortemente baseada nos fundamentos de funcionamento da WEB, em especial, do protocolo HTTP (Hypertext Transfer Protocol). REST ganhou notoriedade por ser um estilo arquitetural simplista e ao mesmo tempo poderoso. Isso despertou o interesse da comunidade Java que, por meio da JCP (Java Community Process), iniciou um processo para padronização técnica deste modelo dentro da plataforma.
Contudo, o uso do modelo REST deve ser adequadamente estudado, suas vantagens e desvantagens devem ser pesadas para que, ao final, os resultados para o projeto sejam realmente positivos.
Neste artigo são abordados os conceitos-chave do modelo arquitetural REST, suas origens e características. Será visto também as principais vantagens e desvantagens da sua aplicação, sua estreita relação com o funcionamento da própria WEB e sua aplicabilidade dentro do mundo Java.
Os conceitos aqui explorados podem ser utilizados em projetos de desenvolvimento de qualquer aplicação com características distribuídas, seja ela Web ou não. Também é possível aplicar tal conhecimento na concepção de arquiteturas de integração de sistemas heterogêneos e desconexos, cujo estilo de integração tenha características de ser do tipo on-line.
A revolução promovida pela rápida expansão do uso da internet no mundo dos negócios, em especial, do seu serviço mais conhecido, o WWW, promoveu mudanças significativas na forma como as organizações trocam informações. Surgiram novos modelos de interação entre os consumidores e organizações empresariais, como o B2C (Business to Customers) e B2B (Business to Business), por exemplo. Tais interações demandam trocas de informações entre os diferentes envolvidos nos mais variados formatos de mídia.
Internamente, as organizações também sofreram grandes mudanças. Houve uma migração de responsabilidade, onde a sustentação do negócio deixou de estar unicamente nas mãos dos colaboradores e passou a ser mantida pela infraestrutura de tecnologia da informação, através de sofisticados recursos de hardware e software. Os processos de negócio foram automatizados e tornaram-se workflows orquestrados via software, interagindo com pessoas e outros sistemas de forma autônoma.
Surgindo um modelo de processamento distribuído, de escala global, interconectado pelas tecnologias oferecidas pela internet. Novos e promissores horizontes se abriam trazendo consigo grandes desafios para a engenharia de software.
Como interligar sistemas essencialmente heterogênios, permitindo a troca de informações e mantendo a mesma semântica dos dados em todas as pontas envolvidas? Como garantir segurança, integridade e consistência nos dados distribuídos? Como construir sistemas essencialmente distribuídos com baixo acoplamento entre as diferentes partes?
Várias soluções foram propostas para tratar destes e de outros problemas característos destes tipos de aplicações. Uma delas, talvez a mais utilizada de todas, é a arquitetura orientada a serviços (Service Oriented Archicteture - SOA). Neste modelo arquitetural, provedores de serviço expõem uma ou mais funcionalidades a diversos consumidores. A comunicação entre as partes é facilitada através de um contrato, chamado WSDL (Web Service Description Language), que especifica o formato das mensagens, as operações suportadas e o modelo de comunicação. Comumente, as mensagens trocadas entre provedores e consumidores está em formato XML, trafegando através do protocolo SOAP (Simple Object Access Protocol).
Outro paradigma proposto para construção de aplicações web distribuídas ou integração de sistemas independentes através da Web é o REST (Representational State Transfer). Como fruto da dissertação de doutorado de Roy T. Fielding, defendida na Universidade da California, em 2000, o REST ganhou rápida aceitação na comunidade como um modelo de desenvolvimento maduro e escalável, de grande aplicabilidade na concepção de modernos sistemas web essencialmente distribuídos.
No modelo arquitetural REST, o protocolo HTTP (Hypertext Transfer Protocol) tem seus recursos amplamente explorados, firmando-se como um modelo de comunicação seguro, amplamente testado e, acima de tudo, padrão. Além do HTTP, outras tecnologias padrão de mercado são utilizadas, como XML (eXtensible Markup Language), JSON (Javascript Object Notation) e URI (Uniform Resource Identifier), por exemplo. Ao unir tais tecnologias amplamente conhecidas, o REST permite construir aplicações web distribuídas de forma rápida, robusta e com investimentos relativamente mais baixos quando comparado com outros modelos arquiteturais, como o SOA, por exemplo.
Na sequência serão apresentadas a origem do modelo REST, suas principais características e ponderações necessárias, antes de optar por sua escolha na concepção de um projeto de software. Ao final, é apresentado um exemplo simples de construção dentro do modelo REST em Java, com intuito de demonstrar a aplicabilidade do modelo em sistemas reais.
As origens do REST
Ao analisar o estilo arquitetural REST, pode-se inferir o quão similar são suas características às encontradas no maior sistema hypermedia distribuído atualmente no mundo, a web.
Na web existe a possibilidade de navegar em portais de notícias, realizar compras em sites de e-commerce, estabelecer comunicação com outras pessoas em redes sociais, assistir vídeos e etc. Nela cada página, vídeo, documento acessível é chamado de recurso. Assim, pode-se entender os recursos como a fundação da arquitetura de sistemas baseados na web. Eles são as peças-chave de interesse nesse modelo de arquitetura “resource-oriented”.
Cada um desses recursos necessita de um mecanismo de identificação para que possa efetivamente ser encontrado e manipulado. Nesse sentido a web provê o padrão URI, que identifica unicamente um recurso ao mesmo tempo em que o torna endereçável e manipulável por um protocolo de aplicação, como o HTTP.
Dessa forma é possível compreender o que é um recurso e como o mesmo é identificado na web. Mas uma dúvida ainda prevalece: como um recurso é efetivamente manipulado na web?
Manipulam-se recursos através de suas representações. Uma representação é uma visão de um estado do recurso em determinado instante do tempo; esta visão é codificada em formatos padronizados e passíveis de transferência através da web, como, XHTML, JSON, XML, PLAIN TEXT, CSV e etc.
Toda interação com um recurso é, portanto, mediada através de uma representação do mesmo, em que tanto o cliente quanto o servidor, convergem no entendimento da codificação daquela representação. Esse “acordo” entre cliente e servidor é obtido em tempo de execução, através de um mecanismo chamado Content Negotiation.
A Figura 1 exemplifica os conceitos e mecanismos relacionados à Web. Observe que a arquitetura cliente servidor é utilizada e que os recursos, disponibilizados para serem acessados pela aplicação, podem ser definidos utilizando diferentes formatos de representações como XML e JSON.
Dessa maneira, evidencia-se que todo o intercâmbio de informações em uma arquitetura Web é realizado através de representações dos recursos, e não do recurso propriamente dito.
Baseado nessas tecnologias, componentes e princípios arquiteturais da web, começaram a haver estudos para compreender o seu sucesso e rápido crescimento em escala global, e como essas “lições arquiteturais” poderiam ser estendidas para suportar o desenvolvimento atual de aplicações distribuídas baseadas na web.
Roy T. Fielding, em sua dissertação de doutorado, analisa os princípios arquiteturais da web e os apresenta sob a forma de um framework de constraints, definindo assim um estilo arquitetural. Este estilo foi denominado REST (REpresentational State Transfer), o qual descreve a web como uma aplicação hypermedia distribuída, onde recursos interligados comunicam-se entre si através da troca de representações de seus estados.
Mas além dessa definição, o trabalho de Fielding, da maneira como foi estabelecido, possibilitou a introdução do HATEOAS (Hypermedia As The Engine Of Application State), o qual é uma das restrições do conjunto que define a Interface Uniforme (conceito que será definido mais à frente neste artigo).
HATEOAS permite estabelecer um padrão em que o progresso de uma aplicação distribuída é realizado através de transições de um estado para outro, de maneira similar a uma máquina de estados. Este modelo remete a maneira como usuários humanos interagem em websites, onde cada página está relacionada à outra por meio de hyperlinks, e os mesmos passam a informação semântica necessária para guiar o usuário pelo processo que o mesmo objetiva.
A ideia do HATEOAS no REST é fazer com que este modelo não se restrinja apenas à interação humano-computador, mas que também seja aplicado na interação entre computadores, já que os mesmos são capazes de seguir protocolos.
Adiante serão demonstrados mais detalhes de cada uma das restrições arquiteturais do REST.
Características do estilo arquitetural REST
As características do REST estão intimamente ligadas às da própria topologia da web, em especial, do protocolo HTTP. O modelo de interação entre os componentes é ditado por uma arquitetura do tipo cliente-servidor sem estado e se dá através da troca de mensagens no formato requisição-resposta. A seguir, cada uma das principais características do modelo arquitetural REST serão detalhadas.
Modelo Cliente-Servidor
Especifica uma separação lógica das atribuições dos componentes dentro da arquitetura. O cliente inicia a comunicação enviando requisições ao servidor. Este, por sua vez, realiza o processamento das requisições e devolve as respostas apropriadas.
Embora cliente e servidor sejam capazes de comunicar-se entre si, não há nenhuma forma de dependência que impeça a evolução das partes de forma independente.
Stateless
Uma implementação que segue os princípios REST, não deve preocupar-se com a manutenção de estado no lado servidor. O único componente que pode manter o estado dos objetos entre diferentes requisições é o próprio cliente. O servidor deve receber todas as informações necessárias para realizar o processamento requerido, sem depender de qualquer informação de estado armazenada previamente.
Assim, é acrescido ao modelo cliente-servidor tradicional a importante característica de ser indiferente a estados, eliminando qualquer forma de acoplamento entre o servidor e seus clientes. Esta característica também facilita o processo de escalar horizontalmente a aplicação, pois cada novo servidor instalado independe do estado de outros, bastando apenas realizar a correta configuração do ambiente.
Cacheable
Uma vez que o modelo arquitetural REST tem sua fundamentação no protocolo HTTP, toda a comunicação efetuada entre clientes e servidor pode ser “cacheada”, ou seja, armazenada temporariamente no cliente e/ou em serviços intermediários, reduzindo o tempo necessário para resposta e aumentando a eficiência global do sistema em termos de consumo de recursos.
Sistema em Camadas (Layered System)
Dividir um sistema complexo em camadas estruturalmente independentes uma das outras, apresenta-se como uma boa prática de engenharia. Cada camada liga-se às adjacentes, através de interfaces padrões, reduzindo assim o nível de acoplamento ao conhecimento de tais interfaces.
Sistemas RESTful têm seu desenvolvimento direcionado à construção em camadas, visando desacoplamento e encapsulamento das funcionalidades providas.
Interface Uniforme
A comunicação entre os componentes de uma arquitetura REST se dá através de uma interface padronizada e genérica, a qual baseia-se no uso dos verbos HTTP e na orientação à recursos. Interfaces REST devem atender a quatro requisitos básicos:
- Identificação dos recursos;
- Manipulação dos recursos através de representações;
- Mensagens autodescritivas;
- Hipermídia como único mecanismo de transição de estados.
Basicamente, numa arquitetura REST existe um mapeamento um-para-um entre as operações básicas de criação, edição, leitura e exclusão (CRUD) e os verbos HTTP POST, PUT, GET e DELETE. Esta relação é definida por:
- POST: Deve ser utilizado para criar um recurso no servidor;
- GET: Deve ser utilizado para recuperar a representação de um recurso. A operação deve ser somente-leitura, idempotente, não realizando qualquer alteração de estado no lado servidor;
- PUT: Altera o estado de um recurso. Realiza uma atualização;
- DELETE: Elimina um recurso.
Code on Demand
É uma característica arquitetural opcional, onde o cliente pode receber um trecho de código do servidor e executá-lo localmente, possibilitando a expansão da funcionalidade do cliente em tempo de execução. Embora seja uma característica à primeira vista muito interessante, deve ser utilizada com cuidado, pois ela reduz a visibilidade do que está sendo realizado no ecossistema REST.
Benefícios da adoção do REST
O desenvolvimento de sistemas é uma atividade que envolve inúmeros riscos. Muitas vezes, o projeto em questão é estratégico para o negócio, o que torna qualquer problema ou falha dispendioso e, muitas vezes, fatal.
A escolha pela adoção da filosofia de desenvolvimento A ou B é própria de cada projeto. Nem sempre a escolha é justificada pelo que há de melhor no mercado, mas sim pelo que melhor se ajusta às necessidades do projeto em questão.
Assim como as demais tecnologias, o REST tem seus prós e contras, que devem ser pesados no momento da escolha do caminho seguir.
As vantagens do uso do REST residem nos seguintes pontos:
- Todos os recursos são identificados de forma única através de URIs. Tal característica gera maior consistência no ambiente, pois uma vez que determinado recurso é endereçado por um URI, este estará vinculado ao recurso durante todo seu ciclo de vida;
- O uso dos verbos e códigos HTTP de forma consistente garante uniformidade arquitetural. Qualquer cliente que entenda HTTP é capaz de interagir com a aplicação REST. Essa uniformidade gera padronização, o que é um fator sempre positivo no desenvolvimento de aplicações;
- O tempo de aprendizado para aplicar o modelo REST, bem como a baixa complexidade de codificação contribuem para reduzir o tempo necessário entre a construção da aplicação e a sua disponibilização no mercado;
- O consumo de recursos de rede e necessidade de hardware é reduzido, quando se compara o REST com a arquitetura orientada a serviço (SOA). Há menor fluxo de dados pela rede, menor quantidade de metainformação e se pode utilizar os recursos do próprio HTTP para realizar cache dos dados;
- Uma aplicação REST é facilmente escalável. Sua topologia pode ser modificada de forma transparente sem grandes impactos;
- Serviços RESTFul são facilmente integrados;
- Os recursos do cliente podem ser melhor utilizados, reduzindo consideravelmente a carga computacional do servidor. Em termos práticos, significa que a necessidade de upgrade de hardware do servidor é menor, gerando menos custos com infraestrutura;
- Pode-se implementar REST utilizando qualquer linguagem que forneça suporte ao protocolo HTTP;
- Aplicações REST são nativamente portáveis, herdando tal característica da própria WEB.
Por outro lado, tem-se também algumas desvantagens. Algumas delas são:
- Ecossistemas REST estão sujeitos a todos os males que podem afetar uma aplicação web qualquer;
- Existência de bibliotecas e frameworks que não dão suporte pleno ao modelo REST, induzindo à construção de sistemas em tese RESTFul, quando na verdade não o são. Muitas vezes tais bibliotecas dificultam o trabalho de desenvolvimento ao invés de otimizá-lo;
- Existe pouco ferramental REST. Isso, algumas vezes, leva o desenvolvedor a se preocupar com aspectos indiretos ao “negócio” da aplicação, que com outras tecnologias já estaria padronizado e disponibilizado nativamente;
- Tratando-se de sistemas críticos, nos quais o alto desempenho seja um requisito não funcional latente, a restrição da interface uniforme, bem como o mecanismo de cache provido pelo HTTP podem não atender de maneira satisfatória.
Rest no mundo Java
REST ganhou notoriedade por ser um estilo arquitetural simplista e ao mesmo tempo poderoso. Isso despertou o interesse da comunidade Java que, por meio da JCP (Java Community Process), iniciou um processo para padronização técnica deste modelo dentro da plataforma. Surgia então o projeto JAX-RS (Java API for RESTful Services) (ler Nota do DevMan 1).
A primeira especificação – JSR 311 (ler Nota do DevMan 2) – teve seu primeiro rascunho definido em 30 de março de 2007. Este tinha como objetivos definir meios de expor os recursos de uma maneira “POJO-based” (Plain Old Java Object) (ler Nota do DevMan 3) por meio de um conjunto de anotações, classes e interfaces, levando em consideração um estilo centrado no HTTP, utilizando-o não apenas como um protocolo de transporte. A especificação também já previa independência de formato nas representações dos recursos web, permitindo uma maneira padronizada de definições, de novos formatos de acordo com a necessidade de cada aplicação. Outro objetivo importante era incorporar a especificação no Java EE, além de torná-la passível de instalação em diversos contêineres de aplicação, como Servlet e Java EE.
JAX-RS é a solução definida pelo JCP para o desenvolvimento de aplicações Java seguindo o estilo de programação REST. Para isso, ela define um conjunto de APIs que fornece uma série de anotações, classes e interfaces para a elaboração de classes POJO.
JSR são documentos formais que descrevem especificações propostas e tecnologias que se pretende adicionar à plataforma Java.
POJO são objetos definidos na linguagem Java que possuem um projeto simplificado.
Houve muitos rascunhos até a consolidação da versão 1.0 da JAX-RS, por meio da JSR 311 - Final Release. Porém, ao mesmo tempo em que o REST foi padronizado no ecossistema Java, foi, de certa forma, decepcionante observar que algumas restrições muito importantes de seu estilo arquitetural não haviam sido contempladas nessa primeira versão. Dentre elas pode-se destacar o hypermedia.
Este cenário fez com que surgisse, cerca de um ano após, a finalização da JAX-RS 1.0, uma nova revisão visando à manutenção da primeira versão. Esta nova versão – JAX-RS 1.1 – trouxe algumas melhorias e correções, porém, a lacuna do hypermedia permaneceu, o que levou seu adiamento para o próximo grande release, do projeto JAX-RS: a versão 2.0, por meio da JSR 339.
A JSR 339 ainda está em processo de definição. Na data do presente artigo, o estágio “Proposed Final Draft” está em andamento, no entanto, as principais implementações dessa especificação, como Jersey e RESTEasy, já estão disponibilizando versões BETA para avaliação parcial das novas funcionalidades.
Dentro do escopo das mudanças que a versão 2.0 trará, pode-se destacar:
- Melhor suporte a Hypermedia;
- API cliente para o consumo de Recursos;
- Validação de parâmetros recebidos (integração com Bean Validation API);
- Filtros e interceptadores.
Nas seções seguintes são apresentados exemplos práticos do uso da JAX-RS na concepção de sistemas REST.
Exemplificando o uso da JAX-RS
Foi apresentado um pouco da história e da conceituação do estilo arquitetural REST. A partir de agora, será visto um simples exemplo de como empregar REST em uma aplicação Java. O exemplo se dará com a utilização da JAX-RS através da implementação RESTEasy, da JBoss. Para este artigo, foi utilizada a versão 3.0-beta-3, que é a versão mais recente disponibilizada em conformidade com a JSR 339.
Para o desenvolvimento desse exemplo foi utilizado um estudo de caso, que consiste na disponibilização de uma interface REST para criação, deleção, consulta, ativação e desativação de clusters de computadores (ler Nota do DevMan 4).
Um cluster define um sistema distribuído composto por um aglomerado de computadores que atuam em conjunto para disponibilizar serviços ou realizar tarefas específicas que exigem alto processamento.
A fim de facilitar a gestão das dependências do projeto, será feito o uso da ferramenta Apache Maven (ler Nota do DevMan 5), cuja finalidade é facilitar o gerenciamento do projeto de software, em termos de código, por meio da simplificação e padronização dos processos de construção, relatórios e documentação do mesmo.
O Apache Maven é uma ferramenta de automação de compilação utilizada em projetos desenvolvidos na linguagem Java. O Maven permite definir através de um XML o projeto de software que está sendo construído, suas dependências sobre módulos e componentes externos, a ordem de compilação, diretórios e plug-ins necessários.
Declarando um Recurso
Os recursos são as principais “peças” de um ambiente arquitetural REST, pois eles é que são expostos publicamente pela aplicação. Logo, deve-se identificá-los dentro do domínio da aplicação para então modelar sua disponibilização.
Dentro do estudo de caso escolhido, o recurso disponibilizado será o cluster. Para isso, será definida, primeiramente, a classe Cluster, que representará o recurso propriamente dito e também a classe Computador, a qual é diretamente relacionada a um cluster. A Listagem 1 ilustra este procedimento.
@XmlRootElement(name = "cluster")
public class Cluster {
@NotNull
private String nome;
private boolean ativo;
@XmlElementWrapper(name = "computadores")
@XmlElement(name = "computador")
@NotNull
@Size(min = 1)
private List<Computador> computadores = new ArrayList<Computador>();
public Cluster() {}
public Cluster(String nome, boolean ativo) {
this.nome = nome;
this.ativo = ativo;
}
public void adicionarComputador(Computador computador) {
computadores.add(computador);
}
public void removerComputador(Computador computador) {
computadores.remove(computador);
}
...
@Override
public String toString() {
return getNome() + " - " + getComputadores();
}
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Cluster))
return false;
Cluster that = (Cluster) obj;
return this.getNome().equals(that.getNome());
}
}
public class Computador {
private String nome;
private short numeroDeCPUs;
private int capacidadeDeRAM;
private int capacidadeDeHD;
public Computador() {
}
public Computador(String nome) {
this.nome = nome;
}
public String getNome() {
return nome;
}
public void setNome(String nome) {
this.nome = nome;
}
…
@Override
public boolean equals(Object obj) {
if (!(obj instanceof Computador))
return false;
Computador that = (Computador) obj;
return this.getNome().equals(that.getNome());
}
}
A classe Cluster é um POJO, e, como pode ser visto, além dos métodos getters e setters, ela possui algumas anotações Java. Estas anotações atendem a dois objetivos:
- Configurar a classe para que seus objetos possam ser serializados em XML pela engine JAXB (Java Architecture for XML Binding), a qual será utilizada para representações em XML dos recursos nesse exemplo;
- Configurar a classe, para que seus atributos possam ser validados durante as requisições ao serviço REST, que alteraram o estado dos objetos. Essas validações são baseadas na Bean Validation API.
Uma vez definida a classe Cluster e Computador, é necessário realizar a criação da classe que será responsável por disponibilizar os clusters como um serviço web. Esta classe se chamará ClusterResource e definirá métodos públicos que serão traduzidos em URIs para que possam ser acessados através da web. A Listagem 2 define esta classe.
@Path("/clusters")
public class ClusterResource {
private ClusterDatabase db = new ClusterDatabase();
@GET
@Path("{nome}")
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
public Response getByNome(@PathParam("nome") String nome) {
Response response;
try {
Cluster cluster = db.getByNome(nome);
response = Response.ok(cluster)
.header("Link", generateLink(cluster))
.build();
} catch (ClusterInexistenteException e) {
response = Response.status(Status.NOT_FOUND).build();
}
return response;
}
private String generateLink(Cluster cluster) {
String linkHeader = "<%s>; rel=\"%s\";";
URI uri;
if (cluster.isAtivo()) {
uri = UriBuilder.fromUri("/clusters/" + cluster.getNome() +
"/ativo/false").build();
linkHeader = String.format(linkHeader, uri.toString(), "desativar");
} else {
uri = UriBuilder.fromUri("/clusters/" + cluster.getNome() +
"/ativo/true").build();
linkHeader = String.format(linkHeader, uri.toString(), "ativar");
}
return linkHeader;
}
@PUT
@Path("{nome}/ativo/{ativar}")
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
public Response ativarDesativarCluster(@PathParam("nome") String nome,
@PathParam("ativar") boolean ativar) {
Cluster cluster;
try {
cluster = db.getByNome(nome);
cluster.setAtivo(ativar);
db.atualizar(cluster);
return Response.ok(cluster).build();
} catch (ClusterInexistenteException e) {
return Response.status(Status.NOT_FOUND).build();
}
}
@GET
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@Wrapped(element = "clusters")
public List<Cluster> listaTodos() {
return db.getTodos();
}
@POST
@Consumes({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@ValidateRequest
public Response criaCluster(@Valid Cluster cluster) {
db.criar(cluster);
return Response.created(
UriBuilder.fromUri("/clusters/" + cluster.getNome()).build()
).build();
}
@PUT
@Consumes({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@ValidateRequest
public Response atualizaCluster(@Valid Cluster cluster) {
db.atualizar(cluster);
return Response.ok(cluster).build();
}
@DELETE
@Path("{nome}")
public Response deletaCluster(@PathParam("nome") String nome) {
Response response;
try {
Cluster cluster;
cluster = db.getByNome(nome);
db.remover(cluster);
response = Response.noContent().build();
} catch (ClusterInexistenteException e) {
response = Response.status(Status.NOT_FOUND).build();
}
return response;
}
}
Nessa classe existem várias anotações Java pertencentes à JAX-RS. São elas que permitem a exposição do recurso. O papel de cada uma dessas anotações no exemplo anterior é:
- @Path: Esta anotação visa definir o endereço (URI) pelo qual o recurso será acessado, pode-se declará-la tanto sobre classes quanto métodos;
- @PathParam: Esta anotação faz a vinculação de um valor dinâmico vindo no URI template de uma anotação @Path à uma variável Java declarada como argumento de método;
- @GET, @POST, @PUT, @DELETE: São anotações que representam o verbo HTTP ao qual um determinado método será vinculado. Pode-se declará-las apenas em métodos;
- @Produces: Tem a finalidade de especificar os media types de representações de recursos suportados pelo serviço. No exemplo, o serviço disponibiliza recursos em dois formatos: XML e JSON. Caso o cliente solicite no cabeçalho HTTP um media type não suportado, a runtime JAX-RS retornará um erro HTTP “406 Not Acceptable”. Esta anotação pode ser declarada sobre classes e métodos.
- @Consumes: Tem por objetivo especificar os media types de representações de recursos que o serviço aceita, ou consome, do cliente. No exemplo, o serviço consome recursos em dois formatos: XML e JSON. Caso o cliente envie o request com um media type não suportado, a runtime JAX-RS retornará um erro HTTP “415 Unsupported Media Type”. Esta anotação pode ser declarada sobre classes e métodos.
- @Valid: Esta anotação é pertencente à especificação Bean Validation API, porém, pode ser utilizada em conjunto com a JAX-RS. Sua função no exemplo é validar a entidade Cluster, ao recebê-la em algum método que irá alterar o estado da aplicação. Para isso, a runtime irá validar as regras definidas na classe Cluster (@NotNull, @Size). Na implementação JAX-RS RESTEasy, é mandatório que a anotação @ValidateRequest esteja sobre o método cujo(s) argumento(s) deva(m) ser validado(s).
Existem ainda duas anotações específicas da implementação RESTEasy: @ValidateRequest e @Wrapped. A primeira foi explicada anteriormente, a segunda anotação é utilizada para sinalizar a runtime JAX-RS RESTEasy que a mesma deve sobrescrever o nome do elemento root em uma coleção de registros para o media type XML de maneira customizada. No exemplo, o elemento root da representação XML é configurado para chamar-se “clusters”. O nome padrão é “collection”.
Métodos públicos: a interface de um recurso
Na classe ClusterResource existem vários métodos públicos. Cada um desses métodos, somados às anotações que definem o endereço URI (@Path), o verbo HTTP (@GET, @POST, @PUT, @DELETE entre outros) e os media types suportados (@Produces), tornam-se interfaces públicas de acesso. Nesse ponto, percebe-se na prática a constraint REST da Interface Uniforme.
Uma vez que a interface está definida, o recurso vinculado à mesma pode ser acessado e manipulado pelos clientes nos formatos suportados.
A partir de agora analisaremos o funcionamento de cada um dos métodos definidos na classe.
A Listagem 3 define um método que é responsável por retornar um cluster específico de acordo com seu nome. Para acessá-lo, o URI a ser utilizado é o http://dominio.com/clusters/nome-do-cluster e o verbo HTTP da requisição deve ser GET. É importante ressaltar que o trecho “domínio.com” no URI é variável de acordo com o domínio em que a aplicação foi implantada. Outro ponto a destacar é que o URI para este método foi composto de duas anotações @Path, uma definida na classe e a outra sobre o método, isso é chamado de sub-recurso, de acordo com a JAX-RS.
@GET
@Path("{nome}")
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
public Response getByNome(@PathParam("nome") String nome) {
Response response;
try {
Cluster cluster = db.getByNome(nome);
response = Response.ok(cluster).header("Link",
generateLink(cluster)).build();
} catch (ClusterInexistenteException e) {
response = Response.status(Status.NOT_FOUND).build();
}
return response;
}
Ao chegar uma requisição para este método, a engine JAX-RS extrai o nome do cluster do URI informado e o repassa para o mesmo. O retorno deste método é do tipo Response, o qual é um tipo definido pela JAX-RS. É por esta classe e suas inner-classes, como ResponseBuilder, por exemplo, que toda a mensagem de reposta ao cliente é construída.
No exemplo, ao encontrar com sucesso um cluster com o nome solicitado, é retornada uma mensagem HTTP “200 OK” contendo uma representação do cluster em questão. Esta representação é por padrão XML, caso o cliente não informe o desejo de outro media type, como JSON, por exemplo. Caso não exista um cluster com o nome informado, o banco de dados dos clusters lança uma exceção ClusterInexistenteException, a qual é capturada e tratada, retornando uma mensagem HTTP “404 Not Found”.
Percebe-se um detalhe importante na linha de código responsável por construir uma resposta HTTP de sucesso. Está sendo definida uma propriedade chamada “Link” no cabeçalho HTTP. A finalidade é proporcionar ao cliente a possibilidade de captura e utilização do mesmo. Neste caso específico do exemplo, é retornado um Link Header que permite ativar ou desativar o cluster, de acordo com seu atual estado. Este link segue o padrão definido na RFC 5988. É importante mencionar que, para esta definição de link, foi utilizado o método header(String, Object) da classe ResponseBuilder ao invés da utilização do método links(Link...) e da classe utilitária LinkBuilder, pois os mesmos não foram implementados na versão beta utilizada do RESTEasy. Contudo, é altamente recomendado utilizá-los após a especificação JAX-RS 2.0 ser efetivamente concluída. Este exemplo do link header demonstra o uso de hypermedia na aplicação.
O método definido na Listagem 4 tem a responsabilidade de ativar ou desativar um cluster na aplicação. Ele recebe dois parâmetros: nome do cluster e uma flag indicando a ação a ser tomada. A requisição vinda do cliente não precisa conter payload, o URI é obtido pelo cliente através do link header definido no método explicado anteriormente. A construção da resposta HTTP segue o mesmo procedimento utilizado no método “getByNome”. Um detalhe importante é que a ação de ativação ou desativação do cluster é executada apenas quando a requisição do cliente for com o método HTTP PUT, pois o intuito é alterar o estado do cluster em questão.
@PUT
@Path("{nome}/ativo/{ativar}")
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
public Response ativarDesativarCluster(@PathParam("nome") String nome,
@PathParam("ativar") boolean ativar) {
Cluster cluster;
try {
cluster = db.getByNome(nome);
cluster.setAtivo(ativar);
db.atualizar(cluster);
return Response.ok(cluster).build();
} catch (ClusterInexistenteException e) {
return Response.status(Status.NOT_FOUND).build();
}
}
A Listagem 5 define um método simples, cuja finalidade é retornar uma coleção de recursos cluster. Os pontos de atenção se referem ao tipo de retorno e à anotação @Wrapper, que já teve sua funcionalidade explicada.
O retorno deste método é a própria representação da entidade que deve ser inserida no corpo da resposta HTTP, uma lista de clusters. A runtime JAX-RS é capaz de fazer esta conversão e gerar uma resposta HTTP adequada, cujo código é o “200 OK”.
@GET
@Produces({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@Wrapped(element = "clusters")
public List<Cluster> listaTodos() {
return db.getTodos();
}
O método cuja definição está apresentada na Listagem 6 é responsável pela criação de novos clusters na aplicação. Ele responde por requisições HTTP POST e aceita entidades Cluster representadas em XML ou JSON. Para este método, destaca-se o uso das anotações responsáveis pela validação da entidade no serviço (@ValidateRequest e @Valid), além da classe UriBuilder, que é utilitária para criação dinâmica de URIs. Nesse exemplo, após a criação do cluster no banco de dados, uma resposta HTTP “201 Created” é enviada juntamente com a localização do recurso recém-criado, isso é realizado através da propriedade “Location” do cabeçalho HTTP.
@POST
@Consumes({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@ValidateRequest
public Response criaCluster(@Valid Cluster cluster) {
db.criar(cluster);
return Response.created(
UriBuilder.fromUri("/clusters/" + cluster.getNome()).build()
).build();
}
O método definido na Listagem 7 é similar ao método “criaCluster” visto anteriormente. A diferença crucial é que o mesmo responde por requisições HTTP PUT e, quando o recurso é atualizado com sucesso na aplicação, uma resposta HTTP “200 OK” é retornada juntamente com a representação do recurso recém atualizado.
@PUT
@Consumes({MediaType.APPLICATION_XML,
MediaType.APPLICATION_JSON})
@ValidateRequest
public Response atualizaCluster(@Valid Cluster cluster) {
db.atualizar(cluster);
return Response.ok(cluster).build();
}
Por fim, o método definido na Listagem 8 tem a responsabilidade de realizar a deleção de um determinado cluster da aplicação. O URI vinculado a este é exatamente o mesmo do método “getByNome”, com a diferença do verbo HTTP utilizado, no “deletaCluster” é feito o uso do HTTP DELETE. No exemplo, o método identifica pelo URI o cluster que deve ser excluído e então realiza esta operação junto ao banco de dados. Após a deleção, é retornada uma resposta HTTP “204 No Content” indicando que o processamento da requisição foi realizado e que a resposta não possui entidade associada, ou seja, não possui conteúdo. Por outro lado, caso o nome do cluster passado na URI não remeta a um cluster existente, o método retorna uma resposta HTTP “404 Not Found”.
@DELETE
@Path("{nome}")
public Response deletaCluster(@PathParam("nome") String nome) {
Response response;
try {
Cluster cluster;
cluster = db.getByNome(nome);
db.remover(cluster);
response = Response.noContent().build();
} catch (ClusterInexistenteException e) {
response = Response.status(Status.NOT_FOUND).build();
}
return response;
}
Definindo o cliente da aplicação
Foi visto que uma das novidades da JAX-RS 2.0 é a inclusão de uma API cliente para facilitar o desenvolvimento de aplicações RESTful. Nesse estudo de caso, foi criada uma série de testes unitários que realizam chamadas aos recursos por meio desta API. Esses testes unitários são escritos com o auxílio do JUnit, uma conhecida ferramenta de testes amplamente difundida na comunidade Java.
Antes da execução de cada teste unitário, é realizada a preparação do ambiente por meio do método “setUp”, onde são configurados a instância do cliente REST além do objeto representando um endpoint para a URI base da aplicação. Também é realizada a inicialização e paralização do servidor servlet embutido através do método “iniciarServidor” e “pararServidor” respectivamente. Esses métodos são invocados uma única vez pelo JUnit durante a execução da suíte de testes.
A Listagem 9 ilustra o código Java que realiza essa tarefa.
public class ClusterResourceTeste {
private Client cliente;
private WebTarget app;
@BeforeClass
public static void iniciarServidor() {
Server.iniciar();
}
@AfterClass
public static void pararServidor() {
Server.parar();
}
@Before
public void setUp() {
cliente = ClientBuilder.newClient();
app = cliente.target(Server.BASE_URI);
}
// restante do código omitido
}
Apresentado isso, vejamos alguns exemplos em código da utilização desta API.
Na Listagem 10 se observa um teste unitário cuja finalidade é buscar um recurso externo por meio de uma requisição HTTP GET. O código em destaque nesse teste realiza uma requisição para o recurso identificado pelo URI “http://<dominio>/clusters/cluster-1”. A requisição sinaliza que aguarda uma representação XML desse recurso.
Há uma maneira alternativa e mais concisa de se fazer a mesma requisição, conforme ilustrado na Listagem 11. Nesse exemplo, a necessidade da invocação do método “readEntity” em Response é eliminada, pois o consumo da representação do recurso é realizado diretamente pelo método “get”.
@Test
public void deveRecuperarUmClusterEmXML() {
Response resp = app.path("/clusters/cluster-1")
.request(MediaType.APPLICATION_XML)
.get();
Cluster c = resp.readEntity(Cluster.class);
Assert.assertEquals(200, resp.getStatus());
Assert.assertEquals("cluster-1", c.getNome());
Assert.assertEquals(1, c.getComputadores().size());
Assert.assertEquals("pc1-ximango",
c.getComputadores().get(0).getNome());
}
Cluster c = app.path("/clusters/cluster-1")
.request(MediaType.APPLICATION_XML)
.get(Cluster.class);
A Listagem 12 ilustra a definição de um teste unitário que tem por objetivo criar um novo recurso na aplicação. Destaque novamente para o código no qual é realizada uma requisição HTTP POST, cujo corpo contém uma representação em XML do cluster (recurso) a ser criado. A conversão do objeto Java para a representação desejada e vice-versa é realizada automaticamente pela runtime JAX-RS, desde que o provedor adequado esteja no classpath da aplicação.
Outros pontos importantes nesse teste unitário referem-se à obtenção da localização do recurso recém-criado e utilização da mesma para recuperar este recurso.
@Test
public void deveCriarUmNovoClusterEmXML() {
Cluster cluster = new Cluster("novo-cluster-xml", true);
cluster.adicionarComputador(new Computador("pc1"));
cluster.adicionarComputador(new Computador("pc2"));
cluster.adicionarComputador(new Computador("pc3"));
Response resp = app.path("/clusters")
.request(MediaType.APPLICATION_XML)
.post(Entity.entity(cluster, MediaType.APPLICATION_XML));
Assert.assertEquals(201, resp.getStatus());
String createdClusterPath = resp.getLocation().toString();
resp.close();
resp = cliente.target(createdClusterPath)
.request(MediaType.APPLICATION_XML)
.get();
Cluster created = resp.readEntity(Cluster.class);
Assert.assertEquals("novo-cluster-xml", created.getNome());
}
O código da Listagem 13 é um teste unitário que realiza a atualização de um recurso na aplicação. É realizada uma requisição HTTP PUT, cujo corpo é preenchido com a representação em XML do recurso que deve ser atualizado. Além disso, esse teste introduz uma funcionalidade presente na JAX-RS 2.0, até então não demonstrada, a capacidade de construir invocações para um determinado recurso permitindo sua execução em um momento futuro. Além dessa possibilidade, a construção de invocações encoraja a reutilização, evitando assim que código possa ser duplicado.
@Test
public void deveAtualizarUmClusterEmXML() {
Invocation clusterGetInvoc = app.path("/clusters/cluster-2")
.request(MediaType.APPLICATION_XML)
.buildGet();
Cluster cluster = clusterGetInvoc.invoke().readEntity(Cluster.class);
Assert.assertEquals(0, cluster.getComputadores().size());
cluster.adicionarComputador(new Computador("new-machine"));
app.path("/clusters")
.request()
.put(Entity.entity(cluster, MediaType.APPLICATION_XML))
.close();
cluster = null;
cluster = clusterGetInvoc.invoke().readEntity(Cluster.class);
Assert.assertEquals(1, cluster.getComputadores().size());
}
Por fim, na Listagem 14 observa-se um teste unitário cujo objetivo é excluir determinado recurso da aplicação. É realizada uma requisição HTTP DELETE para a aplicação, a mesma recupera o recurso definido no URI para realizar sua deleção e retorna uma resposta HTTP “204 No Content” sinalizado o sucesso da operação.
@Test
public void deveDeletarUmClusterExistente() {
Response resp = app.path("clusters/cluster-2").request().delete();
Assert.assertEquals(204, resp.getStatus()); // "204 No content" -
Deletado porém sem conteúdo
}
Testando o Hypermedia na aplicação
Conforme apresentado na definição do método “getByNome” da classe ClusterResource, recursos hypermedia foram adicionados na parte servidora, o que torna a API cliente apta a usar todo o poder do HATEOAS. Porém, em virtude da versão beta do RESTEasy utilizada neste artigo ainda não possuir implementação para tratamento de hypermedia no cliente, foi considerado utilizar uma solução própria e simples para suportar Link Headers. Todavia, é reforçado, mais uma vez, que a API JAX-RS 2.0 deverá ser utilizada para este fim após a finalização da especificação.
Para o controle hypermedia no cliente foi criada uma classe utilitária, conforme demonstrado na Listagem 15.
public class ExtractLinks {
public static class Links {
private final List<Link> jaxLinks;
Links(List<Link> jaxLinks) {
this.jaxLinks = jaxLinks;
}
public Link getByRel(String rel) {
for (Link link : jaxLinks)
if (rel.equals(link.getRel()))
return link;
return null;
}
}
static public Links from(Response response) {
String[] links = response.getHeaderString("Link").split(",");
if (links == null) return null;
List<Link> jaxLinks = new ArrayList<Link>();
for (String link : links) {
final String uri = link.split(";")[0].trim()
.replaceAll("[<>]", "");
final String rel =
link.split(";")[1].trim().replaceAll("(rel=)|(\")", "");
Link jaxLink = createJaxRSLink(uri, rel);
jaxLinks.add(jaxLink);
}
return new Links(jaxLinks);
}
private static Link createJaxRSLink(final String uri, final String rel)
{
return new Link() {
@Override
public String toString() { return uri + " - " + rel; }
@Override
public UriBuilder getUriBuilder() { return null; }
@Override
public URI getUri() {
try {
return new URI(uri);
} catch (URISyntaxException e) {
return null;
}
...
};
}
}
A classe ExtractLinks basicamente realiza a extração dos Link Headers do cabeçalho HTTP oriundo da resposta do servidor. Esses links extraídos são então convertidos em objetos Link da especificação JAX-RS 2.0 e disponibilizados para uso.
A Listagem 16 demonstra um exemplo no qual a classe ExtractLinks é utilizada. Observa-se no código que a resposta HTTP, obtida da requisição que buscou o cluster de nome “cluster-1” é repassada ao método “from” da classe ExtractLinks. A classe então recupera o link definido na resposta pelo servidor e o disponibiliza para utilização em uma requisição HTTP PUT, objetivando, efetivamente, a desativação do cluster.
@Test
public void deveDesativarUmCluster() {
String clusterName = "cluster-1";
Response r = app.path("/clusters/" + clusterName).request().get();
Cluster cluster = r.readEntity(Cluster.class);
Assert.assertTrue(cluster.isAtivo());
Links links = ExtractLinks.from(r);
Link desativar = links.getByRel("desativar");
r.close();
r = app.path(desativar.getUri().toString()).request().put(null);
Assert.assertEquals(200, r.getStatus());
r.close();
cluster = app.path("/clusters/" +
clusterName).request().get(Cluster.class);
Assert.assertFalse(cluster.isAtivo());
}
O exemplo apresentado, embora simples, expõe grande parte dos recursos providos para JAX-RS e pode ser utilizado seguramente como uma base introdutória à API.
Conclusão
Como pode se observar no decorrer do artigo, a aplicação do modelo REST traz grandes benefícios para projetos de software. Provavelmente o principal deles seja o reuso de toda uma infraestrutura de comunicação já consolidada, que é o protocolo HTTP. Como reuso é normalmente sinônimo de redução de tempo de desenvolvimento, pode-se deduzir que aplicações construídas nas bases do REST tem redução dos custos finais envolvidos com a concepção do projeto.
Outros benefícios diretos da adoção do REST são a facilidade de integração posterior do sistema, escalabilidade, menor exigência por infraestrutura de hardware e tempo para entrega do projeto reduzidos em relação a outras tecnologias.
Torna-se fundamental, avaliar a real aplicabilidade do REST ao projeto em questão. REST não é a solução para todos os problemas de engenharia e seus benefícios estão intimamente ligados à adequação de suas características às características do projeto. Bom senso e estudo prévio das necessidades mostram-se essenciais para se obter bons resultados.
Por fim, foi apresentado o que é oferecido dentro do mundo Java para dar suporte à concepção de sistemas REST. A JAX-RS, conforme visto, é a API padrão, concebida para este fim. As primeiras versões deixaram muito a desejar em relação ao atendimento das características pregadas pelo REST, o que vem sido melhorado com as novas releases da mesma.
O uso da JAX-RS é relativamente simples, envolvendo um nível médio de complexidade. Seu uso justifica-se principalmente por estar em conformidade aos demais padrões estabelecidos pelo Java. E como é sabido, no mundo Java, seguir padrões é sempre importante.
Links
- Architectural Styles and the Design of Network-based Software Architecture
- Java API for RESTful Services (JAX-RS)
- Java Community Process
- JSR-000311 JAX-RS: The JavaTM API for RESTful Web Services 1.0 Final Release
- JSR-000311 JAX-RS: The JavaTM API for RESTful Web Services 1.1 Maintenance Release
- JSR-000339 JAX-RS 2.0 Early Draft Review 3
- What's New in JAX-RS 2.0
- Rest in Practice
Robinson ,Ian; Webber, Jim; Parastatidis, Savas. Rest in Practice. O'Reilly Media, 2010.